Last Updated: 2/24/2016

After watching Pete Hunt’s OSCON 2014 video on how Instagram.com was built, I felt inspired to learn more about Webpack. Up to this point, I had been using RequireJS with Grunt. While this duo has worked quite well, flexibility was lacking in terms of module support. Being tied to AMD was less than ideal, especially as I began to see more and more value in using CommonJS modules. Another motivation was to part ways with r.js, the optimization tool bundled with RequireJS. Sifting through r.js’ seemingly mile-long list of options was daunting. Needless to say, arriving at a successful configuration was arduous and nothing short of dumb luck.

Webpack is akin to RequireJS and Browserify, except that its module support system is far more expansive. Suppose your development team wants to interweave AMD, CommonJS, and ES 2015 modules. This module loader can effortlessly support that scenario. Or perhaps you’re fed up with Grunt, Gulp, or {insert JavaScript task runner-of-the-week here}. Webpack can also assume the responsibilities of your task runner. Here’s an opportunity to evict your gruntfile/gulpfile! This could all be wonderful news for a developer seeking a reduction in his or her project’s overall concept count. Modern web applications are only growing in complexity, and the arsenal of tools a web developer is expected to grasp continues to churn at an unreasonable tempo. Any form of simplification is a step in the right direction.

While on the topic of simplification, one desire I had for my next SPA project was to discontinue the use of Bower. Bower is a robust package manager for installing flattened front-end dependencies; however, much of what’s available there now also lives in npm. A common misconception is that npm doesn’t offer browser-ready packages. That fallacy is slowly being put to rest as npm strives to be the premier JavaScript package manager. I may still use Bower for any ASP.NET Core 1.0 applications I develop with Visual Studio 2015, since Microsoft has decided to strip NuGet of its Microsoft authored client-side package responsibility. See the ASP.NET MVC Core 1.0 “StarterWeb” project template for an example. Where I’m going with this is that Webpack works with both Bower and npm; however, npm modules are preferred. See the Bower integration page for more detail.

So how do we get started with Webpack? More importantly, how does this tool allow us to take advantage of the latest JavaScript language enhancements in an Angular 1.x app? What follows is an overview of how the major pieces of the sample application were built and how they work together. I used Microsoft’s new Visual Studio Code editor; and, the finished sample code is available in a GitHub repository here.

Project Setup

project structure
Figure A: project structure

My assumption is that you’ve already installed Node.js with npm. If not, head over to nodejs.org and do so now. Node has become my crutch for modern web development, and I often wonder how I produced anything of reasonable quality without it. If you haven’t taken the time to learn it, do yourself that favor now. Pluralsight is a great starting point.

Now that my Node plug is out-of-the-way, let’s install Webpack. The CLI is installed globally via an npm package:

npm install –g webpack

Next, create a new project which mimics the file/directory structure depicted in Figure A. There’s much debate over the best way to structure an Angular 1.x application. If you’re looking for some direction, take a look at John Papa’s style guide. On the other hand, if you have a preferred way of structuring things, stick with it. Before proceeding, note the file name containing your main Angular application module. That file name will be referenced in the next step.

Create a JavaScript file at the project’s root, named webpack.config.js. Much like the popular JavaScript task runners Grunt and Gulp, Webpack too demands a specific naming convention for its configuration file. The CLI will scan your project directory for a file with this exact name.

This file will do the heavy lifting, and you’ll use a mix of loaders and plugins here if Webpack is replacing your JavaScript task runner. It’s essential to export the configuration object to instruct Webpack how to behave. For available configuration options, go here. The contents of your file should look like this:


'use strict';
// import Webpack plugins
var cleanPlugin = require('clean-webpack-plugin');
var ngAnnotatePlugin = require('ng-annotate-webpack-plugin');
var webpack = require('webpack');
// define Webpack configuration object to be exported
var config = {
context: __dirname + '/app',
entry: './app.module.js',
output: {
path: __dirname + '/dist',
filename: 'bundle.js'
},
resolve: {
alias: {
'npm': __dirname + '/node_modules'
}
},
module: {
loaders: [
{
test: /\.css$/,
loader: 'style!css'
},
{
test: /\.(woff|woff2)$/,
loader: 'url?limit=10000&mimetype=application/font-woff'
},
{
test: /\.(eot|svg|ttf)$/,
loader: 'file'
},
{
test: /\.js?$/,
include: __dirname + '/app',
loader: 'babel'
}
],
preLoaders: [
{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'jshint'
}
]
},
plugins: [
new cleanPlugin(['dist']),
new ngAnnotatePlugin({
add: true
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
]
};
module.exports = config;

This code snippet sets a pointer to the app directory, instructing Webpack where to start looking. It’s the context option’s absolute path which aids in resolving the relative path located in the entry option. The app.module.js file is where Webpack will begin its processing, and the output will be sent to a bundle.js file within the dist directory. As your application grows, it’s common to have multiple entry points. To support that scenario, the entry option can accept an object literal. See the documentation for an example.

Another powerful feature Webpack supports is the creation of path aliases. The thought of typing node_modules over and over again makes me cringe. Consequently, I’ve setup an alias for it called npm, which maps to the absolute path of the node_modules directory. From this point forward, importing npm modules requires fewer keystrokes.

Note that the creation of the aforementioned npm alias is completely optional. I prefer to be explicit with my module import / require statements, so I choose to include the npm alias to eliminate ambiguity among developers who are less familiar with Webpack.

What about ES 2015 support?

For the Webpack configuration file

By renaming the webpack.config.js file to webpack.config.babel.js, one can make use of ES 2015 features within the Webpack configuration file. This, of course, assumes that the Babel npm module is installed locally in this project. Install the aforementioned module as follows:

npm install --save-dev babel

The Webpack configuration file which was provided above could now look like the file below. Note the use of template strings, along with the new let and const keywords.


'use strict';
// import Webpack plugins
const cleanPlugin = require('clean-webpack-plugin');
const ngAnnotatePlugin = require('ng-annotate-webpack-plugin');
const webpack = require('webpack');
// define Webpack configuration object to be exported
let config = {
context: `${__dirname}/app`,
entry: './app.module.js',
output: {
path: `${__dirname}/dist`,
filename: 'bundle.js'
},
resolve: {
alias: {
'npm': `${__dirname}/node_modules`
}
},
module: {
loaders: [
{
test: /\.css$/,
loader: 'style!css'
},
{
test: /\.(woff|woff2)$/,
loader: 'url?limit=10000&mimetype=application/font-woff'
},
{
test: /\.(eot|svg|ttf)$/,
loader: 'file'
},
{
test: /\.js?$/,
include: `${__dirname}/app`,
loader: 'babel'
}
],
preLoaders: [
{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'jshint'
}
]
},
plugins: [
new cleanPlugin(['dist']),
new ngAnnotatePlugin({
add: true
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
]
};
module.exports = config;

For the application files

Looking back at webpack.config.js, we see a loaders array. Loaders are equivalent to plugins for JavaScript task runners, such as Grunt or Gulp. An npm package called babel-loader performs the transpilation of the ES 2015 code back to ES5. Add a .babelrc file at the project root resembling the following gist:


{
"presets": [
"es2015"
]
}

view raw

.babelrc

hosted with ❤ by GitHub

Since we want support only for the finalized ES 2015 specification features, the Babel ES 2015 preset is used here (see babel-preset-es2015 on npm).

There’s also a jshint-loader npm package being used here. Although, note the presence of this particular loader in the preLoaders array. Preloaders execute before loaders. This makes sense only because we want to perform static code analysis on the ES 2015 code before it’s transpiled to ES5 by Babel. JSHint will search for a .jshintrc file at the project root and then perform its static code analysis based on the rule set defined in the aforementioned file.

Execute the following command to install the two loaders described above:

npm install --save-dev babel-loader jshint-loader

What’s with the other loaders and plugins?

Though not related to ES 2015 and Angular, the webpack.config.js file leverages other extensibility points for handling Bootstrap CSS and fonts. Since Webpack treats virtually every asset type as a module, loaders are used to parse and to inject the CSS and fonts into the DOM. Install the prerequisites as follows:

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

When Webpack parses the modules, it evaluates the import and require statements in app.module.js:


'use strict';
// vendor module imports
require('npm/bootstrap/dist/css/bootstrap.css');
import angular from 'npm/angular';
import formly from 'npm/angular-formly';
import formlyBootstrap from 'npm/angular-formly-templates-bootstrap';
import ngAria from 'npm/angular-aria';
import uiRouter from 'npm/angular-ui-router';
// custom module imports
import {default as AppConfig} from './app.config';
import {default as PersonModule} from './person/person.module';
angular
.module('app', [
// vendor modules
ngAria,
uiRouter,
formly,
formlyBootstrap,
// custom modules
PersonModule.name
])
.config(AppConfig.disableDebugInfo);

view raw

app.module.js

hosted with ❤ by GitHub

What may initially seem like an abomination is, in actuality, a kosher amalgamation of ES 2015-style and CommonJS-style module imports. Remember that Webpack will translate, and for lack of a better term, “browserify” this arguably dubious code during the build process. But what should one expect to happen when requiring a CSS module? The contents of the Bootstrap CSS file are parsed, minified, and included in the bundled JavaScript. A style tag will be injected into the DOM with this optimized CSS.

Additionally, there’s a need to clean the generated dist directory on each run of the build. A clean-webpack-plugin npm package is used for this purpose. Also present is a plugin for safely annotating Angular module dependencies. This particular plugin will act on any occurrences of the /* @ngInject */ comment. Install the requisite packages as follows:

npm install --save-dev clean-webpack-plugin ng-annotate-webpack-plugin

How to Run Webpack

CLI output from production build
Figure B: CLI output from production build

The basic command for kicking off the build process is webpack. Couple with that a -d or -p option for development or for production, respectively. The development mode build yields a JavaScript source map file, which is an artifact the production-grade build lacks.

There are a couple other useful CLI options which will increase the verbosity of the CLI’s output: --display-modules and --progress. As one might guess, --progress will display the compilation progress as a percentage. This is useful for complex, long-running builds. The --display-modules option will expand what would otherwise be a simple module count into a detailed listing of those modules included in the bundle.

As you’re beginning to see, the CLI commands can be quite lengthy. Wouldn’t it be nice if we could abstract away the intricacies of running Webpack each time? Open your package.json file, and locate the scripts section. Within there, add the following 2 scripts:

  1. "dev-build": "webpack -d --display-modules --progress"
  2. "dist-build": "webpack -p --display-modules --progress"

Let’s assume I’m preparing a production build of the application. In that case, use this terse command:

npm run dist-build

The CLI output can be found in Figure B.

8 Comments »

  1. Hey There. I found your blog using msn. This is a very well written article.

    I’ll be sure to bookmark it and return to read more of your useful info.
    Thanks for the post. I will certainly return.

    Like

  2. Thank you for bringing this to my attention! I have added the missing api-check module. I’ve also added an .npmrc file to lock down the dependency versions. For future reference, running “npm list –depth=0” in the project’s root folder will tell you if an unmet peer dependency was found. In this case, api-check is a peer dependency for Angular Formly.

    As for the Babel problems you mention, I’m working on upgrading many of the dependencies used here, including Babel. This tutorial is using Babel 5, which is now unsupported. I’d recommend upgrading to version 6, if you haven’t already done so.

    I’ll post another comment here once the sample app’s dependencies are updated.

    Liked by 1 person

    • Try the following steps:
      1. Install the appropriate version of ngCookies. If you’re using Angular 1.5.0, then you’d execute the following command:
      npm i -S angular-cookies@1.5.0

      2. Import the ngCookies module inside of app.module.js as follows:
      import ngCookies from 'npm/angular-cookies';

      3. Add ngCookies to the “app” module’s dependencies array in app.module.js.

      4. You can then use ngCookies in person.controller.js, for example, as follows:

      'use strict';

      const SERVICE = new WeakMap();

      export default class PersonController {
      /* @ngInject */
      constructor(PersonService, $cookies){
      SERVICE.set(this, PersonService);

      const someSessionObj = { 'innerObj': 'somesessioncookievalue' };
      $cookies.dotobject = someSessionObj;
      console.log($cookies.dotobject);

      let objPerson = SERVICE.get(this).getPerson();
      this.FullName = `${objPerson.FirstName} ${objPerson.LastName}`;
      }
      }

      Like

Leave a comment