Webpackin’ your ES 2015 / Angular 1.x SPA
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 […]
A highly caffeinated dev dabbling in the modern web
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 […]
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.
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.
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; |
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" | |
] | |
} |
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
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); |
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
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:
"dev-build": "webpack -d --display-modules --progress"
"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.
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.
LikeLike
Like a boss.
LikeLike
Hey friend, good tutorial!
but there are some mistakes on it:
babel can’t run with jshint so you have to shrink it in two loaders :(, Also it’s being missing the api-check dependency (which took me around one hour to figure out).
LikeLike
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.
LikeLiked by 1 person
FYI, the sample project on GitHub has been updated to use Angular 1.5 and Babel 6.
LikeLiked by 1 person
Thanks!
I have a couple of question… if I want to use for example the angular cookies… in the app.js I need to import the files but how can I use this $cookies instance in my other modules?
LikeLike
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}`;
}
}
LikeLike