Background
I’ve been working on a team developing a large web application used for Fleet Management for several years. The application is designed as a bunch of mini-SPAs. This approach has proven to be an effective and scalable way to grow the application over time and manage complexity. Each of these pages [or regions] is downloaded and bootstrapped client-side (more on this later). There is an application page pipeline and each page has an opportunity to do it’s own bootstrapping within that pipeline.
In the fall of 2017 I had the opportunity to migrate our module loading-and-bundling infrastructure from RequireJS to Webpack. RequireJS worked fine for a long time, but has been losing favor in the industry and the ecosystem around webpack is growing rapidly.
There were numerous motivations for moving to Webpack, such as:
- Webpack can work directly with ES6 imports/exports so there’s no need to transpile to another format prior to bundling (with RequireJS we needed to transpile to ES5 AMD style modules before feeding handing off to the r.js optimizer).
- Opportunities to simplify our supporting infrastructure – such as reducing Gulp tasks. For example, we were able to eliminate the side-by-side file transpilation from babel for our ES6 modules and do this during the webpack compilation (Bonus: This also eliminated the need to source-control all those transpiled files. Same thing for our Sass files).
- The ability to more intelligently break our modules into bundles with long-term caching in-mind (for less-volatile bundles) thus reducing download size and time.
- We never got into bower, and [somewhat embarrassingly] had even used Nuget for some of our client libraries. So, in short, we didn’t have a good client package manager. No worries – Webpack can work with CommonJS files as well as AMD & ES6, so npm is the package manager 🙂
- With RequireJS, most projects would have two deployment modes correlating to “development” and others (QA, UAT, Prod). This often meant that things could run differently between those environment configurations. For example, we had our require optimizer configuration set up so that we’d get a bundle-per-page/endpoint. This worked well, but the developer had to remember to keep the configuration up-to-date whenever a new page/endpoint was added (or we could have augmented the process by generating the list in node – something we did later with the webpack stuff). This wouldn’t be needed during development [locally] because we were lazy-loading all the modules (and thus not using the bundles) and therefore was easy to forget. With Webpack, we have the same compilation pipeline.
Um, no, Webpack is not super-simple [in all cases]
Webpack likes to tout how simple it is and in many cases that’s true, but not all. Webpack is constantly improving and there’s lots of energy around it, so it continues to get better and better. However, there are a few things that I noticed:
- The people within or immediately around Webpack itself or its ecosystem speak a language that makes total sense to them. They live and breath module loading and bundling. I think they forget how easily terms like chunk get’s too-casually thrown around, or takes on numerous meaning, causing confusion.
- The documentation – especially at the high-level Concepts, Configuration, and *some* of the Guides – is great. However, you’ll find many of the plugins, loaders, and some of the guides sorta leave you hanging…..wanting more…. Again, google will be your friend here.
- The ultimate Webpack friendly application is a SPA with a single entry, and you use the CommonsChunkPlugin to extract your code from 3rd party and voila. Or, if you are coming from a node-heavy environment, perhaps using Browserify, then you might also fit really nicely into Webpack. But, if you aren’t a single-entry SPA (like us!), and maybe pushed your existing bundler pretty hard, you might find a few bumps in the road. Fret not though – at this point, I think there have been so many projects which have moved over to Webpack that it’s clearly been battle-tested. You’re unlikely to hit a dead-end.
To the Googles…
There’s little sense in me listing much of the typical migration stuff – you’ll find existing articles explaining this reasonably well (like this article). In fact, for many projects, the steps will be fairly simple. My plan in this post is to mention a few repetitive tasks and some atypical things you might run into.
Repetitive stuff
A couple of fairly typical migration tasks were repetitive enough to mention are:
-
- Plugins: You’ll want to remove RequireJs plugins from any import paths across all your modules. For example, you might want to import an html file as text in your module, and thus you used the text Require JS plugin and your imported module’s path was in the form of:
text![your module path here]
With Webpack, you could still maintain those import paths – but why? It’s much more flexible – and maintainable – to leverage the module section in your webpack.config. This way everything is centralized in one place and not scattered all around your files (easy to change later too!).
- Import path casing: Surprisingly there were a few casing mistakes in our import paths that RequireJS was cool with – but which Webpack was not. This manifested as a warning from Webpack that variations of the same path were being imported. So we cleaned those up and eliminated that.
- Require JS Async plugin: We used the RequireJS async plugin and this doesn’t really map well to a Webpack feature given that Webpack is a build-time tool and doesn’t have a runtime equivalent to RequireJS’s. In our case we were using the async plugin to pull down the google maps api (a common use-case). It was easy enough to sub this out with a lightweight tool like ScriptJs. This tool allows you to pass a callback, which you could easily wrap in a promise for easier consumption like this:
<span class="pl-k">const</span> <span class="pl-c1">status</span> <span class="pl-k">=</span> <span class="pl-k">new</span> <span class="pl-en">Promise</span>(<span class="pl-smi">resolve</span> <span class="pl-k">=></span> <span class="pl-en">scriptJs</span>(<span class="pl-s"><span class="pl-pds">"</span>https://maps.googleapis.com/maps/api/js?key=[your api key]<span class="pl-pds">"</span></span>, resolve));
Atypical stuff
Below are some migration tasks I ran into which weren’t entirely obvious and some were a bit time-consuming. Perhaps these fit some challenges you are running into. We’ll list a couple here and several more in a follow-up post.
Page bootstrapping
I’ll discuss how we did this in RequireJS and how we migrated this to a similar approach in Webpack.
It’s worth mentioning that newer Javascript UI libraries like React or frameworks like Angular have establish patterns or components which make this discussion below somewhat-moot (ie, you don’t need to roll your own page bootstrapping stuff). However, we’re invested in RequireJS and since this application will ultimately be maintained by a another team, we’ve struggled with justifying adding another UI library into the mix and potentially increasing complexity. Also, the approach below provided a good round one migration to Webpack with the most impact and least risk. We would not have taken this approach if we were building a new application.
Page bootstrapping with RequireJS
We’re using KnockoutJS for our client side view engine, so we created our own application page bootstrapping process. This process took advantage of AMD’s define and require calls and their asynchronous callbacks to execute a page pipeline and to ensure a consistent execution path. We nested these calls to ensure certain dependencies were in-place and timed correctly. We use ASP.net MVC (often simply as a delivery mechanism for our SPA’s and less as a server-side rendering engine), and within each page’s view there’s a call to inject this bootstrapping code below the fold, within a tag. The pipeline achieved the following (in this order):
- Points RequireJS to our configuration file (which contains stuff like paths, aliases, shims and what-not).
- Emitted a couple calls to define dynamic modules via data generated server-side. These were modules containing user information and generic payloads for page-specific data which each page’s controller could provide (in whatever shape it needed/wanted).
- Require’d several common modules, which took care of bootstrapping application-level things such as initializing various navigational elements, providing context-sensitive help, setting up notifications, raising events, and showing a splash screen.
- The specific page’s module would be require[d] to allow the page to initialize and render itself. This might include data-binding, getting additional data via ajax, or whatever.
- Some post-page processing logic, clearing any splash screens, and raising events.
Page bootstrapping with Webpack
With webpack, we don’t have AMD constructs like define and require, so we needed a different place to put our application page pipeline pieces. Webpack has a configuration item called Entry. Each item in this configuration points to a unique page in the application (in our case, there’s > 200 items in this configuration). Turns out each item in the entry object can point to an array of modules and those modules are bundled together and executed in order. Perfect!
As an example, let’s say you have standard application things you want to happen before a page runs, so you put this in a “beforePage.js” file. Then you have your page module, “myPage.js” and perhaps some additional standard application things you want to run after a page runs which you place in an “afterPage.js” file. Within each of these files you can import whatever else you need. Then, within your webpack.config, you list those 3 files within an array for that item in the entry:
const entry = {
"myPage": ["beforePage", "myPage", "afterPage"] :
};
The result is that the “myPage” entry will execute those 3 modules in-order. Bam!
Dynamic modules (slight hack)
Earlier I mentioned how we generated AMD modules server-side allowing for dynamic modules. Webpack, is a build-time tool, and doesn’t supply AMD functions like define. So, while this is pretty hacky (and shame on me, pollutes the global), the approach I took was to emit values into a single global variable [namespace] and reference those in the webpack configuration’s externals section. For example you could emit some javascript into your page like so:
<script>
window.myNamespace = window.myNamespace ||{};
window.myNamespace.myModule = [Json serialized object here];
</script>
Then you have this corresponding configuration in webpack.config:
externals: {
myModule: "window.myNamespace.myModule",
}
And at this point, any module can import the module “myModule” and get the server-generated result.
Coming in part 2…
There were other challenges overcome which we’ll continue looking into in the next post such as:
- A simple bit of Node to generate the entries configuration.
- Long-term caching: Injecting webpack-generated module IDs into page the page (to feed back to webpack)
- Reusing webpack configuration (example: with Karma) and breaking configuration into environment-specific files.
- Correcting stacktraces emitted by Jasmine (using sourcemapped-stacktrace)
- Handling SignalR referenced tied in a module.