First, the links:
Where we are
Today's place: Colorado
Module Bundling 101
Implement a module bundler (we’ll use Webpack 2 here) to turn our beautifully architected and readable code into a slim, optimized package for our users to consume.
Modules are an amazing tool for architecting applications, but the way the web works today, loading a bunch of files in a web app is detrimental to the performance of the app. A module bundler will allow us to maintain our great developer experience while still providing the most optimized user experience possible by building, or bundling, our app for production.
Before we get started, I’ll admit that this is by far the most involved step. Webpack is designed to be modular and leverage the work of others where possible – this makes it powerful, but can also make the setup process seem confusing. If you’ve worked with the Dojo Build system before, you know build/bundle systems are inherently complex beasts. If you get stuck, ask for help!
First off, we’ll be installing some npm modules locally with our project, so we need a package.json. Let's scaffold out a package.json with a simple command in the project root:
> npm init
Next, install Webpack:
> npm install webpack --save-dev
Webpack processes our assets with the help of loaders. These are simply npm packages that handle specific responsibilities within the webpack bundling process. For now, we will install loaders to handle HTML and CSS files (Webpack can handle JS on its own for now):
> npm install html-loader style-loader css-loader --save-dev
Next, we’ll create a configuration file for Webpack. If you’ve used Grunt or Gulp before, this is very similar to a Gruntfile.js or Gulpfile.js. The default name for a Webpack config file is webpack.config.js. Create this file in the project root and copy the contents of this file into yours. You can keep everything the same except for the entry property – this will need to point to the entry point of your app.
The next step is going to be a little more specific to the structure of your app. We need to modify our index.html and entry file to be more JS-centric (i.e. don't invoke your JS from index.html). In the case of the Crowdsource Reporter, here was the situation:
First, here’s my commit diff for reference. And a photo.
Here’s the scenario in the existing app: index.html contained a require() which imported bootstrapper.js (the entry file in this app). It then instantiated a new instance of that module. Finally, it called the startup() method on bootstrapper.js.
Instead, we need index.html to simply require our bundle.js (the output from Webpack). We then need our entry file to invoke itself. In this case, I named the Dojo module (Bootstrapper) and invoked the constructor and startup methods within bootstrapper.js.
While your setup may be slightly different, the basic idea is that you need to kick off your app from within the JS file you define as your entry point – not from index.html.
At this point, we might be done. However, you’re likely using the Dojo Loader’s dojo/text! plugin, which Webpack isn’t aware of. This is why we installed Webpack’s html-loader earlier – it does almost exactly the same thing as the Dojo Loader plugin. One beautiful thing about Webpack loaders is that when we configure them properly in webpack.config.js, we don’t need to reference them in our JS, so it’s a simple matter of finding and replacing “dojo/text!” with “” to get what we need.
To avoid overcomplicating things, I'll address the externals section of the Webpack config by saying that it tells Webpack to ignore all modules beginning with dojo, dojox, dijit, and esri in the bundling process, and assume that they will be available for loading at runtime. If you want to know more about this - read the following tangential section...
<-- Begin Tangent -->
Okay – in some ways, this is the crucible of this entire exercise. Getting Webpack to play nicely with the Dojo Loader. The problem this section of the configuration file addresses is that the Dojo Loader does not implement the AMD standard like most module loaders (including Webpack), so lots of issues arise when attempting to run Dojo and JSAPI modules through the Webpack (or any other module loader’s) bundling process.
A few shoutouts to those smart people, in case you don't dive into that blog post...
You stuck with me through all that? Here's a photo.
The beautiful mile-high city of Denver, Colorado. As seen from my roof.
<!-- End Tangent -->
To get our app back to functional, we could simply run webpack from the command line. Instead though, let’s add it as a script to our package.json so we don’t have to remember any command line arguments later. For now, we’ll simply run webpack with no arguments by adding the following property to the “scripts” section of package.json:
The moment of truth! In your terminal, try running:
> npm run build
If you’re fist pumping at your awesome app right now, you had more luck than I did! You deserve a photo.
Waterfalls are awesome. Waterfalls in the Uncompahgre National Forest are the awesomest.
I ran into a few issues here. You may or may not have run into them as well, but I’m going to list them out (along with how I resolved them) just in case.
First, I missed a few modules when manually converting package names to relative paths. The webpack build nicely alerted me, so I just had to go fix them.
The second issue took much longer to figure out. Webpack successfully gave me a bundle, so that was positive. But when I opened my app in the browser, I saw this in the console:
TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
This error is complaining about things related to strict mode. I wasn’t even aware my code was being run in strict mode, so to Google I went. I found out that the ES6 Module spec requires code to be in strict mode – who knew? Given the fact that this is in spec, Webpack naturally adds use strict to any ES6 modules it finds (i.e. all our code).
I guessed that shouldn’t be a big problem though – strict mode is a good idea anyway, right? Well, apparently, Dojo doesn’t think so. The error I was seeing can be traced to the numerous places in the app where Dojo modules do inheritance the Dojo way, using this.inherited(arguments). It’s a known fact that Dojo’s inheritance model is straight-up incompatible with strict mode.
I have an admission of guilt: I cheated to fix this problem. I followed a hunch that the original developers had added this.inherited(arguments) more as a matter of practice than actual necessity. Therefore, I simply commented out all occurrences of that statement, and the app worked. I admit, this is a total cop out! But it is also a possible solution – if your app doesn’t need this type of inheritance, simply remove it.
Luckily, I did stumble across an alternative workaround using Babel. If you want/need to do this, jump to the Legacy Support section below to add Babel to your Webpack config, and include the additional preset es2015-without-strict.
That was it for me – after resolving those issues, my app was back to a fully functioning state. Step back and take some joy from what you have so far. Even if you stop here, you have:
- Implemented some ES6 in your codebase (arrow functions, let/const, template strings, object shorthand)
- A foundational webpack setup, with most of your code being bundled into a single file.
I’d recommend taking account of your app here. Are there other ES6 features you could take advantage of?
- Peruse the remaining Lebab transforms – would any of them improve your codebase?
Once you’re happy with things, let’s move on. With this foundation, we can start unlocking some of the bonus features of modern web development!
Next up is part 4 of this series, Module Bundling 201 - Optimization.