This is the follow up to a post I wrote recently called From Require.js to Webpack - Party 1 (the why) which was published in my personal blog.
In that post I talked about 3 main reasons for moving from rquire.js to webpack: Common JS support, NPM support and a healthy loader/plugin ecosystem. Here I'll instead talk about some of the technical challenges that we faced from moving from our old build system to another one. Despite the clear benefits in developer experience (DX) the setup was fairly difficult and I'd like to cover some of the challanges we faced to make the transition a bit easier.
I'm going to break this up into small chunks for easier consumption.
The first thing you do when you're converting from require.js to webpack is you take your whole require.js configuration file and convert it to a webpack.config.js file.
In practice for us this meant addressing two main areas:
- The enormous
pathslist in our require.js config - All of the
shimconfig
I'll address both, but in case you want to hear the spoiler the solution to both is generally npm.
Initially the conversion process is really straight forward.
Start with a config like this:
requirejs.config({
paths: {
"backbone": "lib/backbone-1.1.0",
"jquery": "lib/jquery-1.10.2",
"underscore": "lib/lodash.underscore-2.3.0",
"jqueryUI": "lib/jquery-ui.min"
}
});And it translates very straightforwardly into a webpack config of the following:
module.exports = {
resolve: {
alias: {
"backbone": "lib/backbone-1.1.0",
"jquery": "lib/jquery-1.10.2",
"underscore": "lib/lodash.underscore-2.3.0",
"jqueryUI": "lib/jquery-ui.min"
}
}This is an easy way to get started, but quickly you'll realize that there's even something better: pulling down dependencies directly from NPM.
More info:
- http://requirejs.org/docs/api.html#config-paths
- http://webpack.github.io/docs/configuration.html#resolve-alias
The next thing, which took some special care, was fixing up our shim config. This was slightly harder than it looked, because it's easy to forget what the shim's are actually doing. Let's revist that for a moment.
Per the require.js docs shims let you:
Configure the dependencies, exports, and custom initialization for older, traditional "browser globals" scripts that do not use define() to declare the dependencies and set a module value.
Basically they take modules that are not AMD-compatible and make them AMD comptible by wrapping them in a little bit of code which will pull in the appropriate dependencies. Let'e examine exactly what's happening with part of our old require.js shim config.
{
shim: {
"underscore": {
exports: "_"
},
"backbone": {
deps: ["jquery", "underscore"],
exports: "Backbone"
}
}
}Here we are applying shims for both underscore and backbone*. For underscore the shim will wrap the library and then return the value of the _ variable for any scripts using it as a dependency. The backbone case is slightly more complicated:
- It wraps the library and exports the value of the
Backbonevariable. - It makes sure that when evaluated backbone has access to both jquery and underscore
Let's see how we would get the same setup using webpack loaders:
{
module: {
loaders: [
{ test: /underscore/, loader: 'exports?_' }
{ test: /backbone/, loader: 'exports?Backbone!imports?underscore,jquery' }
]
}
}Not too hard, but a few thing to note:
- The
testis a regular expression which matches against the full file path, so be careful to be specific! - In order to use these loaders you need to install them
npm install exports-loader imports-loader
- Yes, both of these libraries provide AMD versions now, but when we started using them with require.js they did not.
More Info:
- http://requirejs.org/docs/api.html#config-shim
- http://webpack.github.io/docs/shimming-modules.html
- http://webpack.github.io/docs/using-loaders.html
- https://github.com/webpack/imports-loader
- https://github.com/webpack/exports-loader
This was by far the most challenging piece, mostly because my misunderstanding of how different the splitting/bundling technique is between webpack and require.js.
One of the last things that got us was our CDN. Everything worked fine in local development mode, but when we got to our live testing environments we noticed that it was trying to pull additional bundles off of the webserver by default and not the CDN where our JS was being pulled down from. require.js handles this automatically by parsing the URL from the <script> on the page. Webpack is less magic. If you have a non-standard place where you want it to pull down additional dynamic bundles you need to tell it:
__webpack_public_path__ = document.body.getAttribute('data-js-path') + '/apps/';
More info can be found here: https://github.com/webpack/docs/wiki/configuration#outputpublicpath
If we'd used webpack to handle our total build process we could let webpack apply the hash automatically, but since we're still only using it for our JS build, we need to handle this programmatically. Probably one of my least favorite things about the setup. Not terribly hard, just hard to remember and definitely threw us for a pretty big loop.
At this point we were pretty excited about everything, now that it was basically all working, but we noticed that the file sizes were a bit higher, maybe even a lot higher than our old require.js builds.
webpack --display-reasons is the most helpful things ever.
Run webpack with that flag and you'll see:
- Every module that was added to your bundle
- What file refered it
- The module format of the parent file
Here is some sample output from a recent build:
~/dev/sample-app (master) $ webpack --display-reasons
Hash: 7b470fa455efe1fa9722
Version: webpack 1.9.12
Time: 3667ms
Asset Size Chunks Chunk Names
app.js 1.74 MB 0 [emitted] app
1.1.js 35.1 kB 1 [emitted]
2.2.js 970 kB 2 [emitted]
outlook.js 856 kB 3 [emitted] outlook
[0] ./entry/toaster.js 2.13 kB {3} [built]
[16] ./router.js 2.13 kB {0} [built]
cjs require router [0] ./app.es6 17:14-31
[17] ./view/global.js 2.55 kB {0} [built]
cjs require view/global [16] ./router.js 5:21-43
[18] ./lib/neff.js 602 bytes {0} [built]
cjs require neff [0] ./app.es6 33:12-27
cjs require neff [17] ./view/global.js 7:11-26
cjs require neff [119] ./view/header.es6 27:12-27
amd require neff [131] ./widgets/sessionTimer.js 25:0-152:2
amd require neff [134] ./widgets/ajaxError.js 25:0-185:2
[23] ./constants.js 769 bytes {0} [built]
cjs require constants [16] ./router.js 7:16-36
[24] ./routes/transfer.js 3.12 kB {0} [built]
cjs require routes/transfer [16] ./router.js 8:20-46
[25] ./routes/helper.js 3.68 kB {0} [built]
cjs require routes/helper [24] ./routes/transfer.js 4:21-45
[26] ./widgets/theoverpanel.js 10 kB {0} [built]
cjs require widgets/theoverpanel [25] ./routes/helper.js 7:20-51
cjs require widgets/theoverpanel [69] ./view/transfer/layouts.es6 100:27-58
amd require widgets/theoverpanel [102] ./widgets/addressDropdown.js 1:0-130:2
amd require widgets/theoverpanel [131] ./widgets/sessionTimer.js 25:0-152:2
amd require widgets/theoverpanel [134] ./widgets/ajaxError.js 25:0-185:2
This is a goldmine of information. Which modules took up the most space? How did that get included in this bundle? All answered with this information!
By default all of the files in NPM are excluded from the output. If you want to add that simply add the --display-modules flag to your query and now you can see exactly every file that requires jquery. It's pretty awesome and it helped us find a little issue with moment.js.
One piece of advice for people using moment.js.
{
plugins: [
new webpack.IgnorePlugin(/^\.\/locale$/, [/moment$/]), // saves ~100k from build
]
}With the all of this our file size was very similar to the size of our require.js bundle and the build time went from over 50s to under 10s.
These can cause your builds to appear huge. They are great and can really help with debugging, but make sure you know what you're doing with them and make sure you're not counting them. Most browsers only download these if the developer console is open, so they don't affect your customers.
Webpack and require.js have a lot of similar concepts, but sometimes they don't use the same words to explain them. I'll do my best in this table to provide equivalent definitions.
| Require.js Version | Webpack Version |
|---|---|
| plugin | loader |
require.config() or config.js |
webpack.config.js |
paths config |
alias config |
shim config |
expose/export loaders |
| r.js | webpack |
define([]) |
module.exports |
require(['abc']) |
require('abc') |
require([viewName]) |
require.ensure(viewName)* |
- Dynamic requires are slightly more complicated in webpack, but I like that they're much more explicit. See above for more info.