Wednesday, April 6, 2016

Packaging Web Apps with Node.js

There comes a time when you're at the end of developing your awesome TypeScript/JavaScript web app that you need to think about releasing it into the wild. If you've been using good programming practices you should have split your app into multiple files to make it easier to manage and maintain. Only problem is, it's not a good practice to serve up a ton of files when the user runs your app. The best practice in this scenario is to package up your JavaScript files into one file and only serve that one file.

In this post I will show you how to use TypeScript/ES6's module loading system and Node.js to make it easy to package your web app's files into one file for release while allowing you to develop without packaging. We will use TypeScript's module system to set up file dependencies. Then we will use Require.js to load those modules in the browser. Finally we will use Webpack to package up all of the files into a single JS file and compress it for release.

Prerequisites: I'm assuming you know TypeScript and Node.js basics, but even if you don't you should be able to understand the concepts and use them as a jumping off point to learn more.

The Modules


First of all lets look at how external modules work in TypeScript/ES6. For this example we are going to create three separate TS files, each of which depends on the next one. Two of the files will be modules and the other will be the main application file.

The first file module2.ts exports a class named MyClass2 with a public method getText().

export class MyClass2
{
    public getText(): string
    {
        return "Hello from module 2";
    }
}

Nothing special yet. We just have a class with a method.

The next file is module1.ts. It is a bit more interesting.

import * as module2 from "./module2";
 
export class MyClass1
{
    public getText(): string
    {
        let c = new module2.MyClass2();
        return "Hello from module 1 " + c.getText();
    }
}

At the top of the file we import our module2 module that was defined in module2.ts. Now this file depends on module2.ts. This allows us to use the exported members of module2.ts in module1.ts. This syntax follows the ES6 standard for importing modules.

Next we define the contents of module1. We export a class called MyClass1. In the getText() of this class it creates and instance of MyClass2 from module2 then calls its getText() method and concatenates the messages together.

That's how we do external modules in TypeScript and ES6. Pretty simple, right?

Now we need a main app file for our starting point, which in this case is app.ts.

import {MyClass1} from "./module1";
 
namespace WebpackModules
{
    export class MyApp
    {
        constructor()
        {
            let c1 = new MyClass1();
            var el = document.getElementById('content');
            el.innerText = "MyApp started " + c1.getText();
 
        }
    }
}
 
var app = new WebpackModules.MyApp();

We begin with an import of module1.ts. Now this file depends on module1.ts and by association module2.ts. Notice that the import is a little different from the previous one. Instead of importing everything from module1.ts and assigning a name for the module, we are only importing the MyClass1 class.

Next we wrap the MyApp class in a namespace just because this is what I would do in a real app to keep the global scope clean. If you're not familiar with it, namespace is the new keyword to create internal modules in TypeScript and is equivalent to the old keyword of module.

Inside the namespace we export the main application class, MyApp, so we can use it outside of the namespace. Inside the constructor is where we use the MyClass1 that we imported. We create a new instance of MyClass1, then call getText() on that object and output it to the web page.

The Script Tag


Now we need to set up our web page and get a module loader that can load the modules for us. At this time most browsers don't support the loading of modules using the ES6 specification. Therefore we are going to have to use something else. A well known module loading framework is RequireJS (http://requirejs.org/). So let's use that.

In order to use RequireJS we need to tell the TypeScript compiler which kind of package manager we are using. RequireJS implements the AMD standard so we must tell the TS compiler to use AMD. If you're in Visual Studio you can change it in the project properties. Otherwise change it in the tsconfig.json file ("module": "amd"). The TypeScript compiler will output the correct code that works with the package manager of your choice.

In the html file we only need one script tag that references the require.js file.

<script src="../Scripts/require.js" data-main="app"></script>


Notice that it has a data attribute called data-main. This is where you tell it what file is your entry point. In this case it's app.js, or just app (you don't need to specify the file extension). When the web page is loaded Require will load app.js first, then fulfill all of the imports specified inside the application's modules (module1.js and module2.js).

At this point the application is running and it is loading all the files separately. This great for our development environment. If all our files were packaged up in development it would make it a lot harder to debug.

Packaging the App


When it's time to release the app we need to package it up. For this we will be using a Node package called Webpack. You can easily install it using "npm install webpack -g". Then you can execute it from the command line telling it where your entry point file is and where to save the packaged file.

$ webpack ./src/app.js ./app.package.js


Here we tell it to start with app.js and save the result to app.package.js. Webpack will go through all of the imports just like Require did and load all the dependencies then save all of the code in the correct order in the output file along with any code it needs to bootstrap the app.

At this point you can change the require.js script tag to set data-main="app.package.js" and the application should run. Congrats, you now have a packaged up web application.

One thing to note about using Require, it's a very comprehensive library and therefore not a small file. The minified version is about 85kb. So you're making the user download 85kb just to run your app. If you don't need all of that functionality there is another, much smaller option, Require1k.

Require1k is a minimal library for loading CommonJS modules. If all you want to do is use modules in a web application it's perfect. And, as you can tell from the name, the file is only 1kb. That is a BIG difference. If you can use it instead you probably should.

To use Require1k we will need to change the TS compiler module type option to CommonJS ("module": "commonjs"). Other than that it works exactly the same. The script tag would look like this now:

<script src="require1k.js" data-main="./app"></script>


The only thing that changed was the name of the source file. From here we can use webpack just as we did before and everything should run, but with only 1k of bootstrap code.

Bonus Points: Minification


OK, all of our JS files are packaged up into one file. That's great but it could be better. We could minify the file to make it as small as possible. To do that we can use a Node package called Uglify. Install it using "npm install uglify-js -g".

It's very simple to use. Just point it at the file you want to minify.

$ uglifyjs ./app.package.js -o ./release/app.js -c -m


Use the -o option to specify where to save the minified file to. The -c option tells it to compress. The -m option tells it to mangle names which will rename everything it can to single letter names for more space savings.

Conclusion


Here we have seen how to use external modules in TypeScript/ES6 and how to use Require to load modules in the browser. Then we learned how to use the Node webpack tool to help us package up our TypeScript app for release while keeping them unpackaged for development. Then we seen how to use uglify to compress our application's code.

No comments:

Post a Comment