Chapter 10. Bundling and deploying applications with Webpack

This chapter covers

  • Bundling apps for deployment using Webpack
  • Configuring Webpack for bundling Angular apps in dev and prod
  • Integrating the Karma test runner into the automated build process
  • Creating a prod build for the online auction
  • Automating project generation and bundling with Anguar CLI

Over the course of this book, you’ve written and deployed multiple versions of the online auction and lots of smaller applications. Web servers properly served your applications to the user. So why not just copy all the application files to the production server, run npm install, and be done with deployment?

No matter which programming language or framework you use, you’ll want to achieve two goals:

  • The deployed web application should be small in size (so it can load faster).
  • The browser should make a minimal number of requests to the server on startup (so it can load faster).

When a browser makes requests to the server, it gets HTML documents, which may include additional files like CSS, images, videos, and so on. Let’s take the online auction application as an example. On startup, it makes hundreds of requests to the server just to load Angular with its dependencies and the TypeScript compiler, which weigh 5.5 MB combined. Add to this the code you wrote, which is a couple of dozen HTML, TypeScript, and CSS files, let alone images! It’s a lot of code to download, and way too many server requests for such a small application. Look at figure 10.1, which shows the content of the Network tab in Chrome Developer Tools after the auction was loaded into our browser: there are lots of network requests, and the app size is huge.

Figure 10.1. Monitoring the development version of the online auction application

Real-world applications consist of hundreds or even thousands of files, and you want to minimize, optimize, and bundle them together during deployment. In addition, for production, you can precompile the code into JavaScript, so you don’t need to load the 3 MB TypeScript compiler in the browser.

Several popular tools are used to deploy JavaScript web applications. All of them run using Node and are available as npm packages. These tools fit into two main categories:

  • Task runners
  • Module loaders and bundlers

Grunt (http://gruntjs.com) and Gulp (http://gulpjs.com) are widely used general-purpose task runners. They know nothing about JavaScript applications, but they allow you to configure and run the tasks required for deploying the applications. Grunt and Gulp aren’t easy to maintain for build processes, because their configuration files are several hundred lines long.

In this book, you used npm scripts for running tasks, and a task is a script or a binary file that can be executed from a command line. Configuring npm scripts is simpler than using Grunt and Gulp, and you’ll continue using them in this chapter. If the complexity of your project increases and the number of npm scripts becomes unmanageable, consider using Grunt or Gulp to run builds.

So far you’ve used SystemJS to load modules. Browserify (http://browserify.org), Webpack (http://webpack.github.io), Broccoli (www.npmjs.com/package/broccoli-system-builder), and Rollup (http://rollupjs.org) are all popular bundlers. Each of them can create code bundles to be consumed by the browsers. The simplest is Webpack, which allows you to convert and combine all your application assets into bundles with minimal configuration. A concise comparison of various bundlers is available on the Webpack site: http://mng.bz/136m.

The Webpack bundler was created specifically for web applications running in a browser, and many typical tasks required for preparing web application builds are supported out of the box with minimal configuration and without the need to install additional plugins. This chapter starts by introducing Webpack, and then you’ll prepare two separate builds (dev and prod) for the online auction. Finally, you’ll run an optimized version of the online auction and compare the size of the application with what’s shown in figure 10.1.

You won’t use SystemJS in this chapter—Webpack will invoke the TypeScript compiler while bundling the apps. The compilation will be controlled by a special loader that uses tsc internally to transpile TypeScript into JavaScript.

Note

The Angular team has created Angular CLI (https://github.com/angular/angular-cli), which is a command-line interface for automating an application’s creation, testing, and deployment. Angular CLI uses the Webpack bundler internally. We’ll introduce Angular CLI later in this chapter.

10.1. Getting to know Webpack

While preparing for a trip, you may pack dozens of items into a couple of suitcases. Savvy travelers use special vacuum-seal bags that allow them to squeeze even more clothes into the same suitcase. Webpack is an equivalent tool. It’s a module loader and a bundler that lets you group your application files into bundles; you can also optimize their sizes to fit more into the same bundle.

For example, you can prepare two bundles for deployment: all your application files are merged into one bundle, and all required third-party frameworks and libraries are in another. With Webpack, you can prepare separate bundles for development and production deployment, as shown in figure 10.2. In dev mode, you’ll create the bundles in memory, whereas in production mode Webpack will generate actual files on disk.

Figure 10.2. Dev and prod deployments

It’s convenient to write an application as a set of small modules where one file is one module, but for deployment you’ll need a tool to pack all of these files into a small number of bundles. This tool should know how to build the module dependencies tree, sparing you from manually maintaining the order of the loaded modules. Webpack is such a tool, and in the Webpack philosophy, everything can be a module, including CSS, images, and HTML.

The process of deployment with Webpack consists of two major steps:

  1. Build the bundles (this step can include code optimization).
  2. Copy the bundles to the desired server.

Webpack is distributed as an npm package, and, like all tools, you can install it either globally or locally. Let’s start by installing Webpack globally:

npm install webpack -g
Note

This chapter uses Webpack 2.1.0, which at the time of this writing is in beta. To install it globally, we used the command npm i [email protected] -g.

A bit later, you’ll install Webpack locally by adding it into the devDependencies section of the package.json file. But installing it globally will let you quickly see the process of turning an application into a bundle.

Tip

A curated list of Webpack resources (documentation, videos, libraries, and so on) is available on GitHub. Take a look at awesome-webpack: https://github.com/d3viant0ne/awesome-webpack.

10.1.1. Hello World with Webpack

Let’s get familiar with Webpack via a very basic Hello World example consisting of two files: index.html and main.js. Here’s the index.html file.

Listing 10.1. index.html
<!DOCTYPE html>
<html>
<body>
  <script src="main.js"></script>
</body>
</html>

The main.js file is even shorter.

Listing 10.2. main.js
document.write('Hello World!');

Open a command-prompt window in the directory where these file are located, and run the following command:

webpack main.js bundle.js

The main.js file is a source file, and bundle.js is the output file in the same directory. We usually include the word bundle in the output filename. Figure 10.3 shows the result of running the preceding command.

Figure 10.3. Creating the first bundle

Note that the size of the generated bundle.js is larger than that of main.js because Webpack didn’t just copy one file into another but added other code required by this bundler. Creating a bundle from a single file isn’t overly useful, because it increases the file size; but in a multi-file application, bundling files together makes sense. You’ll see this as you read this chapter.

Now you need to modify the <script> tag in the HTML file to include bundle.js instead of main.js. This tiny application will render the same “Hello World!” message as the original.

Webpack allows you to specify various options on the command line, but it’s better to configure the Webpack bundling process in webpack.config.js, which is a JavaScript file. A simple configuration file is shown here.

Listing 10.3. webpack.config.js
const path = require('path');

module.exports = {
  entry: "./main",
  output: {
    path: './dist',
    filename: 'bundle.js'
  }
};

To create a bundle, Webpack needs to know the main module (the entry point) of your application, which may have dependencies on other modules or third-party libraries (other entry points). By default, Webpack adds the .js extension to the name of the entry point specified in the entry property. Webpack loads the entry-point module and builds a memory tree of all dependent modules. By reading the configuration file in listing 10.3, Webpack will know that the application entry is located in the ./main.js file and that the resulting bundle.js file has to be saved in the ./dist directory, which is a common name for the distribution bundles.

Tip

Storing the output files in a separate directory will allow you to configure your version-control system to exclude the generated files. If you use a Git version-control system, add the dist directory to the .gitignore file.

You could specify more than one entry point by providing an array as a value for the entry property:

entry: ["./other-module", "./main"]

In this case, each of these modules will be loaded on startup.

Note

To create multiple bundles, you need to specify the values of the entry property not as strings, but as objects. You’ll see such an example later in this chapter, when you instruct Webpack to put the Angular code in one bundle and the application code in another.

If the Webpack configuration file is present in the current directory, you don’t need to provide any command-line parameters; you can run the webpack command to create your bundles. The other choice is to run Webpack in watch mode using the --watch or -w command-line option, so whenever you make a change to your application files, Webpack will automatically rebuild the bundle:

webpack --watch

You can also instruct Webpack to run in watch mode by adding the following entry to webpack.config.js:

watch: true
Using webpack-dev-server

In previous chapters, you used live-server to serve your applications, but Webpack comes with its own webpack-dev-server that has to be installed separately. Usually you add Webpack to the existing npm project and install both Webpack and its development server locally by running the following command:

npm install webpack webpack-dev-server --save-dev

This command will install all required files in the node_modules subdirectory and will add webpack and webpack-dev-server to the devDependencies section of package.json.

The next version of Hello World is located in the directory hello-world-devserver and includes the index.html file shown next.

Listing 10.4. hello-world-devserver/index.html
<!DOCTYPE html>
<html>
<body>
  <script src="/bundle.js"></script>
</body>
</html>

The main.js JavaScript file remains the same:

document.write('Hello World!');

The package.json file in the hello-world-devserver project looks like this.

Listing 10.5. hello-world-devserver/package.json
{
  "name": "first-project",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "start": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^2.1.0-beta.25",
    "webpack-dev-server": "^2.1.0-beta.0"
  }
}

Note that you’ve configured the npm start command for running the local webpack-dev-server.

Note

When you serve your application with webpack-dev-server, it’ll run on the default port 8080 and will generate the bundles in memory without saving them in a file. Then webpack-dev-server will recompile and serve the new versions of the bundles every time you modify the code.

You can add the configuration section of webpack-dev-server in the devServer section of the webpack.config.js file. There you can put any options that webpack-dev-server allows on the command line (see the Webpack product documentation at http://mng.bz/gn4r). This is how you could specify that the files should be served from the current directory:

devServer: {
  contentBase: '.'
}

The complete configuration file for the hello-world-devserver project is shown here and can be reused by both the webpack and webpack-dev-server commands.

Listing 10.6. hello-world-devserver/webpack.config.js
const path = require('path');

module.exports = {
  entry: {
    'main': './main.js'
  },
  output: {
    path: './dist',
    filename: 'bundle.js'
  },
  watch: true,
  devServer: {
    contentBase: '.'
  }
};

In listing 10.6, two of the options are needed only if you’re planning to run the webpack command in watch mode and generate output files on disk:

  • The Node module path resolves relative paths in your project (in this case, it specifies the ./dist directory).
  • watch: true starts Webpack in watch mode.

If you run the webpack-dev-server command, the preceding two options aren’t used. webpack-dev-server always runs in watch mode, doesn’t output files on disk, and builds bundles in memory. The contentBase property lets webpack-dev-server know where your index.html file is located.

Let’s try to run the Hello World application by serving the application with webpack-dev-server. In the command window, run npm start to start webpack-dev-server. On the console, webpack-dev-server will log the output, which starts with the URL you can use with the browser, which by default is http://localhost:8080.

Open your browser to this URL, and you’ll see a window that displays the message “Hello World”. Modify the text of the message in main.js: Webpack will automatically rebuild the bundle, and the server will reload the fresh content.

Resolving filenames

This is all good, but you’ve been writing code in TypeScript, which means you need to let Webpack know that your modules can be located not only in .js files, but in .ts files as well. In the webpack.config.js file in listing 10.6, you specified the filename with the extension: main.js. But you can specify just the filenames without any extensions as long webpack.config.js has the resolve section. The following code snippet shows how you can let Webpack know that your modules can be located in files with extension .js or .ts:

resolve: {
  extensions: ['.js', '.ts']
}

The TypeScript files also have to be preprocessed (transpiled). You need to tell Webpack to transpile your application’s .ts files into .js files before creating bundles; you’ll see how to do that in the next section.

Usually, build-automation tools provide you with a way to specify additional tasks that need to be performed during the build process. Webpack offers loaders and plugins that allow you to customize builds.

10.1.2. How to use loaders

Loaders are transformers that take a source file as input and produce another file as output (in memory or on disk), one at a time. A loader is a small JavaScript module with an exported function that performs a certain transformation. For example, json-loader takes an input file and parses it as JSON. base64-loader converts its input into a base64-encoded string. Loaders play a role similar to tasks in other build tools. Some loaders are included with Webpack so you don’t need to install them separately; others can be installed from public repositories. Check the list of loaders in the Webpack docs on GitHub (http://mng.bz/U0Yv) to see how to install and use the loaders you need.

In essence, a loader is a function written in Node-compatible JavaScript. To use a loader that’s not included with the Webpack distribution, you’ll need to install it using npm and include it in your project’s package.json file. You can either manually add the required loader to the devDependencies section of package.json or run the npm install command with the --save-dev option. In the case of ts-loader, the command would look like this:

npm install ts-loader --save-dev

Loaders are listed in the webpack.config.js file in the module section. For example, you can add the ts-loader as follows:

module: {
  loaders: [
    {
      test: /.ts$/,
      exclude: /node_modules/,
      loader: 'ts-loader'
    },
  ]
}

This configuration tells Webpack to check (test) each filename and, if it matches the regular expression .ts$, to preprocess it with ts-loader. In the syntax of regular expressions, the dollar sign at the end indicates that you’re interested only in files having names that end with .ts. Because you don’t want to include Angular’s .ts files in the bundle, you exclude the node_modules directory. You can either reference loaders by their full name (such as ts-loader), or by their shorthand name, omitting the -loader suffix (for example, ts). If you use relative paths in your templates (for example, template: "./home.html"), you need to use angular2-template-loader.

Note

The SystemJS loader isn’t used in any of the projects presented in this chapter. Webpack loads and transforms all project files using one or more loaders configured in webpack.config.js based on the file type.

Using loaders for HTML and CSS files

In the previous chapters, Angular components that stored HTML and CSS in separate files were specified in the @Component annotation as templateUrl and styleUrls, respectively. Here’s an example:

@Component({
  selector: 'my-home',
  styleUrls: ['app/components/home.css')],
  templateUrl: 'app/components/home.html'
})

We usually keep the HTML and CSS files in the same directory where the component code is located. Can you specify the path relative to the current directory?

Webpack allows you to do this:

@Component({
  selector: 'my-home',
  styles: [home.css'],
  templateUrl: 'home.html'
})

While creating bundles, Webpack automatically adds the require() statements for loading CSS and HTML files, replacing the preceding code with the following:

@Component({
  selector: 'my-home',
  styles: [require('./home.css')],
  templateUrl: require('./home.html')
})

Then Webpack checks every require() statement and replaces it with the content of the required file, applying the loaders specified for the respective file types. The require() statement used here isn’t the one from CommonJS: it’s the internal Webpack function that makes Webpack aware that these files are dependencies. Webpack’s require() not only loads the files, but also can reload them when modified (if you run it in watch mode or use webpack-dev-server).

Relative paths in templates with SystemJS

It’s great that Webpack supports relative paths. But what if you want to be able to load the same app with either SystemJS or Webpack?

By default, in Angular you have to use the full path to external files, starting from the app root directory. This would require code changes if you decided to move the component into a different directory.

But if you use SystemJS and keep the component’s code and its HTML and CSS files in the same directory, you can use a special moduleId property. If you assign to this property a special __moduleName binding, SystemJS will load files relative to the current module without the need to specify the full path:

declare var __moduleName: string;
@Component({
  selector: 'my-home',
  moduleId:__moduleName,
  templateUrl: './home.html',
  styleUrls: ['./home.css']
})

You can read more about relative paths in the Angular documentation in the “Component-Relative Paths” section at http://mng.bz/47w0.

In dev mode, for HTML processing, you’ll use raw-loader, which transforms .html files into strings. To install this loader and save it in the devDependencies section of package.json, run the following command:

npm install raw-loader --save-dev

In prod, you’ll use html-loader, which removes extra spaces, newline characters, and comments from HTML files:

npm install html-loader --save-dev

For CSS processing, you use the loaders css-loader and style-loader; during the building process, all related CSS files will be inlined. css-loader parses CSS files and minifies the styles. style-loader inserts CSS as a <style> tag on the page; it does so dynamically in the runtime. To install these loaders and save them in the devDependencies section of package.json, run the following command:

npm install css-loader style-loader --save-dev

You can chain loaders using an exclamation point as a piping symbol. The following fragment is from a webpack-config.js file that includes an array of loaders. When loaders are specified as an array, they’re executed from the bottom to the top (so the ts loader will be executed first in this example). This extract is from a sample project in the next section, where CSS files are located in two folders (src and node_modules):

First you exclude the CSS files located in the node_modules directory, so this transformation will apply only to application components. You chain the to-string and css loaders here. The css loader is executed first, turning the CSS into a JavaScript module, and then its output is piped to the to-string loader to extract the string from the generated JavaScript. The resulting string is inlined into the corresponding components in the @Component annotation, in place of require(), so Angular can apply the proper ViewEncapsulation strategy.

Then you want Webpack to inline the third-party CSS files located in node_modules (not in src). css-loader reads the CSS, generates a JavaScript module, and passes it to style-loader, which generates <style> tags with the loaded CSS and inserts them into the <head> section of the HTML document. Finally, you turn the HTML files into strings and transpile the TypeScript code.

Tip

In Angular, you want the CSS to be encapsulated in the components to gain the benefits of ViewEncapsulation, as explained in chapter 6. That’s why you inline CSS into the JavaScript code. But there’s a way to build a separate bundle that contains just the CSS by using the ExtractTextPlugin plugin. If you use CSS preprocessors, install and use the sass-loader or less-loader.

What preloaders and postloaders are for

Sometimes you may want to perform additional file processing even before the loaders start their transformations. For example, you may want to run your TypeScript files through a TSLint tool to check the code for readability, maintainability, and functional errors. For that, you need to add the preLoaders section in the Webpack configuration file:

preLoaders: [
  {
    test: /.ts$/,
    exclude: /node_modules/,
    loader: "tslint"
  }
]

Preloaders always run before loaders; and if they run into any errors, those are reported on the command line. You can also configure some postprocessing by adding a postLoaders section to webpack.config.js.

10.1.3. How to use plugins

If Webpack loaders transform files one at a time, plugins have access to all files, and they can process them before or after the loaders kick in. For example, the Commons-ChunkPlugin plugin allows you to create a separate bundle for common modules required by various scripts in your application. The CopyWebpackPlugin plugin can copy either individual files or entire directories to the build directory. The UglifyJS-Plugin plugin performs code minification of all transpiled files.

Say you want to split your application code into two bundles, main and admin, and each of these modules uses the Angular framework. If you just specify two entry points (main and admin), each bundle will include the application code as well as its own copy of Angular. To prevent this from happening, you can process the code with the CommonsChunkPlugin. With this plugin, Webpack won’t include any of the Angular code in the main and admin bundles; it will create a separate shareable bundle with the Angular code only. This will lower the total size of your application because it includes only one copy of Angular shared between two application modules. In this case, the HTML file should include the vendor bundle first, followed by the application bundle.

UglifyJSPlugin is a wrapper for the UglifyJS minifier, which takes the JavaScript code and performs various optimizations. For example, it compresses the code by joining consecutive var statements, removes unused variables and unreachable code, and optimizes if statements. Its mangler tool renames local variables to single letters. For a full description of UglifyJS, visit its GitHub page (https://github.com/mishoo/UglifyJS). You’ll use these and other plugins in the following sections.

10.2. Creating a basic Webpack configuration for Angular

Now that we’ve covered the Webpack basics, let’s see how to bundle a simple Angular application written in TypeScript. We’ve created a small application that consists of Home and About components and doesn’t use external templates or CSS files. This project is located in the basic-webpack-starter directory, and its structure is shown in figure 10.4.

Figure 10.4. The basic-webpack-starter project

The main.ts script bootstraps the AppModule and MyApp components, which configure the router and have two links for navigating to either HomeComponent or AboutComponent. Each of these components displays a simple message—the actual functionality is irrelevant in the context of this chapter. You’ll focus on creating two bundles: one for Angular and its dependencies, and the other for the application code.

The vendor.ts file is small—it just uses the import statements required by Angular. We did this to create a situation where there are two entry points (main.ts and vendor.ts) that contain common Angular code, which you’ll put into a separate bundle.

Listing 10.7. basic-webpack-starter/vendor.ts
import 'zone.js/dist/zone';
import 'reflect-metadata/Reflect.js';
import '@angular/http';
import '@angular/router';
import '@angular/core';
import '@angular/common';

Because these import statements can also be used in main.ts, you’ll use the Commons-ChunkPlugin to avoid including Angular code in both bundles. Instead, you’ll build a separate Angular bundle that’s shared by the main entry point and any other entry point if you decide to split the application code into smaller chunks.

Note

The vendor.ts file should import all the modules that you want included in the common bundle and removed from the application code. Webpack will inline into the common bundle all the code required for the imported modules.

The content of the webpack.config.js configuration file is shown here.

Listing 10.8. basic-webpack-starter/webpack.config.js

Because CommonsChunkPlugin comes with Webpack, there’s no need to install it separately. After you install copy-webpack-plugin, Webpack will find it in the node_modules directory.

Listing 10.8 has two entry points: main.ts contains the app code plus Angular, and vendor.ts has only the Angular code. So the Angular code is common to both of these entry points, and this plugin will extract it from main and will keep it only in vendor.bundle.js.

Although it’s nice to bundle all of your app code into one JavaScript file for deployment, it’s easier to debug code in its original form of separate files. By adding source-map to webpack.config.js, you tell Webpack to generate source maps so you can see the sources of the JavaScript, CSS, and HTML files, even though the browser executes the code from bundle.js.

You use the option "sourceMap": true in the tsconfig.json file so the TypeScript source maps are generated. Web browsers load the source map files only if you have the developer console open, so generating source maps is a good idea even for production deployments. Keep in mind that ts-loader will perform the code transpilation, so you turn off tsc code generation by setting "noEmit": true in tsconfig.json.

Now let’s see how npm’s package.json file will change to include the Webpack-related content. In the basic version of package.json, you’ll add two lines in the scripts section. The devDependencies section will include webpack, webpack-dev-server, and the required loaders and plugins.

Listing 10.9. basic-webpack-starter/package.json

Note

We use NPM packages from the @types namespace and the TypeScript compiler option @types to handle type-definition files. You need TypeScript 2.0. or later installed to use the @types option.

Both the start and build scripts use the same Webpack configuration from the webpack.config.js file. Let’s see how these scripts differ.

10.2.1. npm run build

After running npm run build, the command window looks like what you see in figure 10.5. The application weighs 2.5 MB and makes just three network requests to load. Webpack built two bundles (bundle.js and vendor.bundle.js) and two corresponding source map files (bundle.js.map and vendor.bundle.js.map), and it copied index.html into the dist output directory shown in figure 10.6.

Figure 10.5. Running npm run build

Figure 10.6. The content of the dist directory

The index.html file doesn’t include any <script> tags for loading Angular. Everything the application needs is located in two bundles included in two <script> tags.

Listing 10.10. basic-webpack-starter/index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset=UTF-8>
  <title>Basic Webpack Starter</title>
  <base href="/">
</head>
<body>
  <my-app>Loading...</my-app>
  <script src="vendor.bundle.js"></script>
  <script src="bundle.js"></script>
</body>
</html>

You can open the command window in the dist directory and run the familiar live-server from there to see this simple application running. We took the screenshot in figure 10.7 after the application stopped at the breakpoint in main.ts to illustrate source maps in action. Even though the browser executes code from JavaScript bundles, you can still debug the code of the specific TypeScript module thanks to generated source maps.

Figure 10.7. Placing a breakpoint in the TypeScript module

10.2.2. npm start

If instead of npm run build you run the npm start command, the dist directory won’t be created, and webpack-dev-server will do the build (including source map generation) and serve the application from memory. Just open your browser to localhost://8080, and your app will be served. The application weighs 2.7 MB, but you’ll do a lot better by the end of this chapter.

The basic Webpack project presented in this section is good for demo purposes. But real-world applications require more advanced work with Webpack, which we’ll discuss in the next section.

10.3. Creating development and production configurations

In this section, we’ll show you two versions of Webpack configuration files (one for development and one for production) that you can use as a starting point in your real-world Angular projects. All the code shown in this section is located in the angular2-webpack-starter directory. The application is the same as in the previous section and consists of two components: Home and About.

This project has more npm scripts in package.json and includes two configuration files: webpack.config.js for the development build and webpack.prod.config.js for production.

10.3.1. Development configuration

Let’s start with the development configuration file, webpack.config.js, which gets some minor additions compared to the file in the previous section. You add one new plugin, DefinePlugin, that allows you to create variables that are visible from your application code and can be used by Webpack during the build.

Listing 10.11. angular2-webpack-starter/webpack.config.js

The NODE_ENV variable is used by Node.js. To access the value of NODE_ENV from JavaScript, you use a special process.env.NODE_ENV variable. In listing 10.11, you set the value of the ENV constant to the value of the NODE_ENV environment variable if defined, or to development if not. The use of the constants HOST and PORT is similar, and the metadata object will store all these values.

The ENV variable is used in main.ts to invoke the Angular function if (webpack.ENV === 'production') enableProdMode();. When production mode is enabled, Angular’s change-detection module doesn’t perform an additional pass to ensure that the UI isn’t modified in the lifecycle hooks of the components.

Note

Even if you don’t set the Node environment variable in the command window, it has a default value of development, set in the webpack.config.js file.

10.3.2. Production configuration

Now let’s take a look at the webpack.prod.config.js production configuration file, which uses additional plugins: CompressionPlugin, DedupePlugin, OccurrenceOrderPlugin, and UglifyJsPlugin.

Listing 10.12. angular2-webpack-starter/webpack.prod.config.js

You’ll start the build process using the npm script commands included in the package.json file, which has more commands than those we discussed in section 10.2. Note that the build command explicitly specifies the webpack.prod.config.js file with the production configuration, and the start command will use the development configuration from webpack.config.js, which is a default name used by the Webpack development server.

Listing 10.13. angular2-webpack-starter/package.json

Typically, after running npm install, you’ll use the following two commands (you won’t set the NODE_ENV environment variable in the command line):

  • npm start—Starts the Webpack development server in development mode, and serves a non-optimized application. If you open the Developer Tools in the browser, you’ll see that the application started in development mode because the variable ENV has the value development, as set in webpack.config.js.
  • npm run serve:dist—Runs npm run build to create optimized bundles in the dist directory, and starts a static web server and serves the optimized version of the application. If you open the Developer Tools in the browser, you won’t see a message saying that the application started in development mode, because it runs in production mode; the ENV variable has the value production, as set in webpack.prod.config.js.
  • npm run serve:dist:aot—Invokes the Angular ngc compiler for ahead-of-time (AoT) compilation before building the bundles. This removes the need to include ngc in the application code and further optimizes the bundle size.
Tip

We keep development and production scripts in separate files, even though it’s possible to reuse the same file by selectively applying certain sections of the configuration based on the value of the environment variable. Some people define two files and reuse the development configuration in the production one (for example, var devWebpackConfig = require('./webpack.config.js';)). In our experience, this hurts the readability of the configuration script, so we keep complete build configurations in the separate files.

Note

The example webpack.config.js and webpack.prod.config.js have less than 60 lines each. If you were to use Gulp to prepare a similar build configuration, it would include a couple of hundred lines of code.

10.3.3. A custom type-definition file

You need to add a custom type-definition file to the app to prevent tsc compiler errors. There are two reasons you may get compilation errors in the app:

  • Webpack will load and transform all of your CSS and HTML files. During the transformation, Webpack will replace all occurrences like the following:
    styles: ['home.css'],
    template: require('./home.html')
    with the transformed content:
    styles: [require('./home.css')],
    template: require('./home.html')
    This is Webpack’s own require() function, not the one from Node.js. If you’ll be running the tsc compiler, the preceding code will result in compilation errors because it doesn’t recognize require() with such a signature.
  • The app uses the ENV constant defined in the Webpack configuration files:
    if (webpack.ENV === 'production') {
      enableProdMode();
    }

To make sure the compiler won’t complain about this variable, create a custom typings.d.ts type definitions file with the following content.

Listing 10.14. angular2-webpack-starter/typings.d.ts
declare function require(path: string);

declare const webpack: {
  ENV: string
};

Type definitions that start with the declare keyword don’t have a direct link to the actual implementations of the variables (such as the require() function and the ENV constant), and it’s your responsibility to update the preceding file if the code changes (for example, if you decide to rename ENV as ENVIRONMENT).

Figure 10.8 shows a screenshot taken after we ran the command npm run serve:dist and opened the app in the browser. Note that this version of the application made just three requests to the server, and the total size of the application is 180 KB. This application is a little smaller than the auction, but you can still compare the number of the server requests and the size shown in figure 10.1 to see the difference and appreciate the job done by Webpack.

Figure 10.8. After running npm run serve:dist

Now let’s see how ahead-of-time compilation will affect the size of the app. Run the command npm run serve:dist:aot. The app size is only 100 KB! This is pretty impressive, isn’t it?

Note

At the time of this writing (Angular 2.1.0), AoT compilation produces footprints smaller than JIT only for small applications.

Establishing continuity

If you’re a project manager, you need to make sure the main processes of your project are automated. The biggest mistake a project manager can make is to allow John Smith, an expert developer, to manually build and deploy applications on request. John is a human being, and he can get sick, take a vacation, or even quit one day. Automating the build and deployment processes serves as a guarantee of continuity for the business of software development. The following processes have to be established during the early stages of your project:

  • Continuous integration (CI)— This is an established process that runs build scripts multiple times a day, such as after each code merge in the source code repository. The build scripts include unit tests, minification, and bundling. You need to install and configure a CI server to guarantee that the master branch of the application code is always in a working condition, and you’ll never hear the question, “Who broke the build?”
  • Continuous delivery (CD)— This is a process that prepares your application for deployment. CD is about offering your users additional features and bug fixes.
  • Continuous deployment— This is the process of deploying the new version of an application that was prepared during the CD phase. Continuous deployment allows you to get frequent feedback from your users, ensuring that your team is working on something that users really need.

Front-end developers often work in collaboration with a team that works on the server side of the application. That team may already have CI and CD processes in place, and you’ll need to learn how to integrate your build with whatever tools are used for the server side.

If you still believe that manual deployment isn’t a crime, read about what happened to Knight Capital Group, which went bankrupt in 45 minutes because of a human error during deployment. In 2014, Doug Seven wrote an article, “Knightmare: A DevOps Cautionary Tale,” describing this incident (http://mng.bz/1kDr). The bottom line is that the build and deployment process should be automated and repeatable, and it can’t depend on any single technician.

Tip

If you’re building a large application with megabytes of JavaScript code, you may want to split your application code into multiple modules (entry points) and turn each of them into a bundle. Say your web application has a user-profile module that’s not used very often. Removing the code that implements the user profile will lower the initial size of the home page of your app, and the code for the user profile will be loaded only when needed. A popular Instagram web application defines more than a dozen entry points.

10.4. What’s Angular CLI?

Initially the entry barrier into the world of Angular development was pretty high because of the need to learn and manually configure multiple tools. Even to get started with a simple application, you needed to know and use the TypeScript language, the TypeScript compiler, ES6 modules, SystemJS, npm, and a development web server. To work on a real-world project, you also needed to learn how to test and bundle your app.

To jump-start the development process, the Angular team created a tool called Angular CLI (https://github.com/angular/angular-cli), which is a command-line tool that covers all the stages of the Angular application lifecycle, from scaffolding and generating an initial app to generating boilerplate for your components, modules, services, and so on. The generated code also includes preconfigured files for unit tests and bundling with Webpack.

You can install Angular CLI globally by using following command:

npm install -g angular-cli
Angular CLI and Webpack

Angular CLI is powered by Webpack. Internally, the CLI-generated project includes Webpack configuration files similar to the ones discussed in this chapter.

Even though Angular CLI uses Webpack internally, it doesn’t allow you to modify the Webpack configuration, which may prevent you from implementing specific build requirements (such as a custom bundling strategy) using CLI. In that case, you can configure your project manually (without CLI) using the knowledge gained in this chapter.

10.4.1. Starting a new project with Angular CLI

After CLI is installed, the executable binary ng becomes available on your PATH. You’ll use the ng command to invoke Angular CLI commands.

To create an Angular application, use the new command:

ng new basic-cli

CLI will generate a new directory called basic-cli, which will include all the files required for a simple app. To run this app, enter the command ng serve, and open your browser at http://localhost:4200. You’ll see a page that renders the message “app works!”. CLI automatically installs all the required dependencies and creates a local git repository in the folder. Figure 10.9 shows the generated project structure.

Figure 10.9. CLI project structure

Let’s take a quick look at the main project files and directories:

  • e2e— The directory for end-to-end tests.
  • src/app— The main directory for the application code. For routes and child components, you usually create subdirectories here, but the CLI doesn’t mandate this.
  • src/assets— Everything in this folder will be copied as is in the dist directory during the build process.
  • src/environments— Here you specify environment-specific settings. You can create an arbitrary number of custom environments, such as QA, staging, and production.
  • angular-cli.json— The main Angular CLI configuration file. Here you can customize the locations for files and directories that CLI relies on, such as global CSS files and assets.

10.4.2. CLI commands

CLI provides a few commands you can use to manage an Angular application. Table 10.1 lists the most commonly used commands that come in handy while developing and preparing a production version of an application.

Typically, you’ll run the ng build command with the options -prod --aot to build an optimized version of your app. Without these options, the size of the generated bundle of a basic generated app is about 2.5 MB. Creating bundles using ng build -prod reduces the bundle size to 800 KB and also generates the gzipped version of the bundle (190 KB).

Table 10.1. Frequently used CLI commands

Command

Description

ng serve Builds the bundles in memory and starts the included webpack-dev-server, which will run and rebuild the bundles each time you make changes in your app code.
ng generate Generates various artifacts for your project. You can also use a shorter version of this command: ng g. To list all the available options, invoke ng help generate. Examples:
  • ng g c <component-name>—Generates four files for the component: a TypeScript file for the source code, a TypeScript file for the component unit tests, an HTML file for the template, and the CSS file for styling the component’s view. If you want to keep CSS and templates inline in the generated component, run this command with options: for example, ng g c product --inline-styles --inline-template. If you don’t want to generate the spec file, use the option --spec=false.
  • ng g s <service-name>—Generates two TypeScript files: one for the source code and another one for the unit tests.
ng test Runs unit tests using the Karma test runner.
ng build Produces JavaScript bundles with the transpiled application code and all the dependencies inlined. Saves the bundles in the dist directory.

To optimize the bundle for production deployment, turn on ahead-of-time compilation by running ng build --prod --aot. Now the size of the bundle is about 450 KB, and its gzipped version is as small as 100 KB. Figure 10.10 shows the content of the dist directory of the basic-cli app after running this command.

Figure 10.10. The dist directory after production build

Note

To see this 100 KB bundle loaded in the browser, you need to use a web server that supports serving prebuilt gzipped files. For example, you can use the static node server that you used in section 10.3.2.

This concludes our brief overview of Angular CLI, which eliminates the need to manually create multiple configuration files and produces highly optimized production builds. You can find a description of all the Angular CLI commands at https://cli.angular.io/reference.pdf.

You may ask, “Why did we have to learn the internals of Webpack if Angular CLI will configure Webpack for us?” We wanted you to know how things work under the hood in case you decide to manually create and fine-tune your builds without Angular CLI. Also, in the future Angular CLI may allow you to change the autogenerated config files of Webpack, and it’s good to know how to do this.

Tip

To speed up installing dependencies of Angular CLI projects, use the Yarn package manager instead of npm. See https://yarnpkg.com for details.

10.5. Hands-on: deploying the online auction with Webpack

In this exercise, you won’t be developing any new application code. The goal is to use Webpack to build and deploy an optimized version of the online auction. You’ll also integrate the Karma test runner into the build process.

For this chapter, we refactored the auction project from chapter 8, which used the same package.json file between the client and server portions of the application. Now the client and server will be separate applications with their own package.json files. Keeping the client and the server code separate simplifies build automation.

Angular and security

Angular has built-in protection against common web application vulnerabilities and attacks. In particular, to prevent cross-site scripting attacks, it blocks malicious code from entering the DOM. With images, an attacker can replace the image with malicious code in the src attribute of an <img> tag.

The auction application uses images from the http://placehold.it website, which will be blocked during bundling unless you specifically write that you trust this site. This chapter’s auction adds code stating that you trust the images from http://placehold.it and they don’t need to be blocked.

That’s why you add the code to prevent image-sanitizing in the auction components that use images coming from a third-party server. For example, a constructor for ProductItemComponent looks like this:

constructor(private sanitizer: DomSanitizer) {
  this.imgHtml = sanitizer.bypassSecurityTrustHtml(`
    <img src="http://placehold.it/320x150">`);
}

You can find more details about Angular security in the Angular documentation at http://mng.bz/pk57.

The package.json file from the client directory has the npm scripts required to build the production bundle as well as run webpack-dev-server in development mode. The server directory has its own package.json file with npm scripts to start the auction’s Node server—no bundling is needed there. Technically, you have two independent applications with their own dependencies configured separately. You’ll start this hands-on project using the source code located in the auction directory from chapter 10.

10.5.1. Starting the Node server

The server’s package.json file looks like this.

Listing 10.15. auction/package.json
{
  "name": "ng2-webpack-starter",
  "description": "Angular 2 Webpack starter project suitable for a production
  grade application",
  "homepage": "https://www.manning.com/books/angular-2-development-with-
  typescript",
  "private": true,
  "scripts": {
    "tsc": "tsc",
    "startServer": "node build/auction.js",
    "dev": "nodemon build/auction.js"
  },
  "dependencies": {
    "express": "^4.13.3",
    "ws": "^1.0.1"
  },
  "devDependencies": {
    "@types/compression": "0.0.29",
    "@types/es6-shim": "0.0.27-alpha",
    "@types/express": "^4.0.28-alpha",
    "@types/ws": "0.0.26-alpha",
    "compression": "^1.6.1",
    "nodemon": "^1.8.1",
    "typescript": "^2.0.0"
  }
}

Note that you define the tsc script here to ensure that the local version of TypeScript 2.0 will be used even if you have an older version of the compiler installed globally. In the command line, change to the server directory and run npm install to get all the required dependencies for the server’s portion of the application.

To use the local compiler, run the command npm run tsc, which will transpile the server’s code and create auction.js and model.js (and their source maps) in the build directory, as configured in tsconfig.json. This is the code for the auction’s server.

Start the server by running the command npm run startServer. It’ll print the message “Listening on 127.0.0.1:8000” on the console.

10.5.2. Starting the auction client

You can start the auction client in either development or production mode using different npm script commands. The npm scripts section of the client’s package.json has the following commands:

"scripts": {
  "clean": "rimraf dist",
  "prebuild": "npm run clean && npm run test",
  "build": "webpack --config webpack.prod.config.js --progress --profile",
  "startWebpackDevServer": "webpack-dev-server --inline --progress --port 8080",
  "test": "karma start karma.conf.js",
  "predeploy": "npm run build && rimraf ../server/build/public && mkdirp
  ../server/build/public",
  "deploy": "copyup dist/* ../server/build/public"
}

Most of the commands should look familiar to you, because you used them in webpack.prod.config.js in listing 10.12. You add a new deploy command, which uses the copyup command to copy files from the client’s dist directory to the server’s build/public directory. Here you use the copyup command from the copyfiles npm package (https://www.npmjs.com/package/copyfiles). You use this package for cross-platform compatibility when it comes to copying files. You also add the test command to run tests with Karma (see section 10.5.3).

Because there’s a predeploy command, it’ll run automatically each time you run npm run deploy. In turn, predeploy will run the build command, which will automatically run prebuild. The latter will run clean and test, and only after all these commands succeed will the build command do the build. Finally, the copyup command will copy the bundles from the dist directory into the server/build/public directory.

Before starting the client portion of the auction, you need to open a separate command window, change to the client directory, and run the command npm install. Then start the auction app in development mode by running npm run startWebpackDevServer. webpack-dev-server will bundle your Angular app and begin listening for the browser’s requests on port 8080. Enter http://localhost:8080 in your browser, and you’ll see the familiar UI of the auction app.

Note

The development build is done in memory, and the auction application is available on port 8080, which is the port configured in the webpack.config.js file.

Open the Network tab in Chrome Developer Tools. You’ll see the application load the freshly built bundles—and the size of the application is pretty large.

Check Webpack’s log on the console, and you’ll see which files went to which bundle (or chunk). In this case, you built two chunks: bundle.js and vendor.js. Figure 10.11 shows a small fragment of the Webpack log, but you can see the size of each file. The bundled application (bundle.js) weighs 285 KB, whereas the vendor code (vendor.bundle.js) is 3.81 MB.

Figure 10.11. Console output fragment

At the top, you’ll see some font files that weren’t bundled, because in the webpack.config.js file you specified the limit parameters to avoid inlining large fonts into the bundle:

{test: /.woff$/,  loader: "url?limit=10000&minetype=application/font-woff"},
{test: /.woff2$/, loader: "url?limit=10000&minetype=application/font-woff"},
{test: /.ttf$/,   loader: "url?limit=10000&minetype=application/
 octet-stream"},
{test: /.svg$/,   loader: "url?limit=10000&minetype=image/svg+xml"},
{test: /.eot$/,   loader: "file"}

The last line instructs the file-loader to copy fonts with a .eot extension into the build directory. If you scroll the console output, you’ll see that all the application code went to chunk {0}, and vendor-related code went to chunk {1}.

Note

In development mode, you didn’t deploy the Angular app under the Node server. The Node server ran on port 8000, and the auction client was served on port 8080 and communicated with the Node server using the HTTP and WebSocket protocols. You’ll deploy the Angular app under the Node server next.

Now, stop the webpack-dev-server (not the Node server) by pressing Ctrl-C in the command window from which you started the client. Start the production build by running the command npm run deploy. This command will prepare the optimized build and copy its files into the ../server/build/public directory, which is where all the static content of the Node server belongs.

There’s no need to restart the Node server, because you only deployed the static code there. But to see the production version of the auction, you need to use port 8000, where the Node server runs.

Open your browser to the URL http://localhost:8000. You’ll see the auction application served by the Node server. Open the Chrome Developer Tools panel in the Network tab, and refresh the application. You’ll see that the size of the optimized application is drastically smaller. Figure 10.12 shows that the total size of the application is 349 KB (compared with 5.5 MB in the unbundled version shown earlier in figure 10.1).

Figure 10.12. What’s loaded in the prod version of the auction

The browser made nine requests to the server to load index.html, two bundles, and the gray images that represent products. You can also see the request for data about products that the client made using Angular’s Http request. The line that ends with .woff2 is the font loaded by Twitter’s Bootstrap framework.

url-loader works like file-loader, but it can inline files smaller than the specified limit into the CSS where they’re defined. You specified 10,000 bytes as a limit for the files with names ending with .woff, .woff2, .ttf, and .svg. One larger file (17.9 KB) wasn’t inlined.

Each font is represented in multiple formats, such as .eot, .woff, .woff2, .ttf, and .svg, and there are several non-exclusive options for dealing with fonts:

  • Inline all of them into the bundle, and let the browser choose which one it wants to use.
  • Inline the font format supported by the oldest browser you want your app to support. This means newer browsers will download files two to three times larger than they need to.
  • Inline none of them, and let the browser choose and download the one it supports best.

The strategy here was to inline only the selected fonts that meet certain criteria and to copy the others into the build folder, which can be considered a combination of the first and last options.

10.5.3. Running tests with Karma

In chapter 9, you developed three specs for unit testing ApplicationComponent, StarsComponent, and ProductService. You’ll reuse the same specs here, but you’ll run them with Karma as a part of the building process.

Because the client and server are separate npm projects now, the Karma configuration files karma.conf.js and karma-test-runner.js are located in the client directory.

Listing 10.16. auction/client/karma.conf.js

The karma.conf.js file is a lot shorter than the one from chapter 9, because you don’t need to configure files for SystemJS anymore—Webpack is the loader now, and the files are already configured in webpack.test.config.js. Following is the karma-test-runner.js script for running Karma.

Listing 10.17. auction/client/karma-test-runner.js
Error.stackTraceLimit = Infinity;

require('reflect-metadata/Reflect.js');
require('zone.js/dist/zone.js');
require('zone.js/dist/long-stack-trace-zone.js');
require('zone.js/dist/proxy.js');
require('zone.js/dist/sync-test.js');
require('zone.js/dist/jasmine-patch.js');
require('zone.js/dist/async-test.js');
require('zone.js/dist/fake-async-test.js');

var testing = require('@angular/core/testing');
var browser = require('@angular/platform-browser-dynamic/testing');

testing.TestBed.initTestEnvironment(
    browser.BrowserDynamicTestingModule,
    browser.platformBrowserDynamicTesting());

Object.assign(global, testing);


var testContext = require.context('./src', true, /.spec.ts/);

function requireAll(requireContext) {
  return requireContext.keys().map(requireContext);
}

var modules = requireAll(testContext);

The webpack.test.config.js file is shown next. It’s simplified for testing because you don’t need to create bundles during testing. The Webpack dev server isn’t needed because Karma acts as a server.

Listing 10.18. auction/client/webpack.test.config.js
const path         = require('path');
const DefinePlugin = require('webpack/lib/DefinePlugin');

const ENV  = process.env.NODE_ENV = 'development';
const HOST = process.env.HOST || 'localhost';
const PORT = process.env.PORT || 8080;

const metadata = {
  env : ENV,
  host: HOST,
  port: PORT
};

module.exports = {
  debug: true,
  devtool: 'source-map',
  module: {
    loaders: [
      {test: /.css$/,  loader: 'raw', exclude: /node_modules/},
      {test: /.css$/,  loader: 'style!css?-minimize', exclude: /src/},
      {test: /.html$/, loader: 'raw'},
      {test: /.ts$/,   loaders: [
        {loader: 'ts', query: {compilerOptions: {noEmit: false}}},
        {loader: 'angular2-template'}
      ]}
    ]
  },
  plugins: [
    new DefinePlugin({'webpack': {'ENV': JSON.stringify(metadata.env)}})
  ],
  resolve: { extensions: ['.ts', '.js']}
  }
};

The client/package.json file has the following Karma-related content.

Listing 10.19. auction/client/package.json
"scripts": {
...
"test": "karma start karma.conf.js"
}
...
"devDependencies": {
...
     "karma": "^1.2.0",
    "karma-chrome-launcher": "^2.0.0",
    "karma-firefox-launcher": "^1.0.0",
    "karma-jasmine": "^1.0.2",
    "karma-mocha-reporter": "^2.1.0",
    "karma-webpack": "^1.8.0",
}

To run the tests manually, run the npm test command in the command window from the client directory. You’ll see output like that shown in figure 10.13.

Figure 10.13. Running Karma

To integrate the Karma run into the build process, you can modify the npm prebuild command to look like this:

"prebuild": "npm run clean && npm run test"
"build": "webpack ...",

Now, if you run the command npm run build, it’ll run prebuild, which will clean the output directory, run the tests, and then do the build. If any of the tests fail, the build command won’t run.

You’re done with the final hands-on project for this book. If this was a real-world application, you’d continue fine-tuning the build configuration, cherry-picking the files you want to include or exclude from the build. Webpack is a sophisticated tool, and it offers endless possibilities for optimizing the bundles. The Angular team is working on an optimization of the Angular code using the ahead-of-time compilation, and we wouldn’t be surprised if the Angular framework will add only 50 KB to your application’s code.

Note

The source code that comes with this chapter includes a directory called extras that contains another implementation of the auction in which the Angular portion has been generated with Angular CLI. Check the scripts section in angular-cli.json to see how to add third-party libraries to an Angular CLI project.

10.6. Summary

This chapter wasn’t about writing code. The goal was to optimize and bundle code for deployment. The JavaScript community has a few popular tools for build automation and bundling of web applications; our choice is the combination of Webpack and npm scripts.

Webpack is an intelligent and sophisticated tool, but we presented a small combination of loaders and plugins that work for us. If you’re looking for a more complete Webpack configuration, try angular2-webpack-starter (http://mng.bz/fS4T).

These are the main takeaways from this chapter:

  • The process of preparing deployment bundles and running builds should be automated in the early phases of development.
  • To minimize the number of requests the browser makes to load your application, combine the code into a small number of bundles for deployment.
  • Avoid packaging the same code into more than one bundle. Keep third-party frameworks in a separate bundle so other bundles of your application can share them.
  • Always generate source maps, because they allow you to debug the source code in TypeScript. Source maps don’t increase the size of the application code and are generated only if your browser has the Developer Tools open, so using source maps even for production builds is encouraged.
  • To run build and deployment tasks, use npm scripts, because they’re simple to write and you already have npm installed. If you’re working on preparing a build for a large and complex project, and you feel the need for a scripting language to describe various build scenarios, introduce Gulp into your project workflow.
  • To quickly start your development in Angular and TypeScript, generate your first projects with Angular CLI.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.54.63