webpack and legacy applications
By Per Fröjd
- webpack
- legacy
- c#
- mvc
Using Webpack in older applications
A lot of my current time is invested in one of our older systems, in particular one which is running ASP.NET MVC with a lot of server-rendered Razor views. Together with this, we use a handful of Javascript libraries, meshed together with jQuery.
Over the years, the client code has grown, and every now and then we find the need of adding a new library, it could be a tooltip-plugin or some other sort of widget. Unfortunately, this also has an effect on the load and response times of the application, and even though we knew the problem would occurr, we haven’t had the opportunity to look into fixing the issue.
The current architecture
The current architecture is based on a pattern known as the Revealing Module Pattern
and looks something like this:
;(window.page || (window.page = {})).Controller.Action = (function () {
var myInnerVar = 'Foo'
function myInnerFunction() {
console.log(myInnerVar)
}
return {
init: function () {
myInnerFunction()
},
}
})()
What happpens here is that we produce our own inner scope for each Controller and Action we have present in the MVC application. So for example, the action Index in the Home Controller would be accessed and executed through calling page.Home.Index.init();
Doing it like this gives us greater control in making sure that the JS that executes, is only the file hooked up to this particular namespace. Together with the code above, we have a main function that checks the current controller and action and then executes the init function of that namespace.
Now, as the application grows, the amount of files grow (naturally), but the only pre-processing that’s done is a minified bundle of CSS and Javascript, which isn’t always enough.
Introducing webpack
We’ve previously worked with webpack in React projects so we’re quite familiar with how it works. However, it’s always been rather simple because we’ve implemented the frontend from scratch, which means we can at that point dictate how our structure should look like. Coming into an older application, the structure is already there, and there isn’t much we can do about it (aside from attempting to fix it, or stick to it).
We set out with a small glimmer of hope that we could at least introduce webpack to our Javascript, mostly so that we (in the future) could consider implementing additionals like Flow, TypeScript or any other modern workflow.
Introducing dynamic imports
Dynamic imports is a way to load dependencies on demand from within the Javscript ecosystem, and it looks something like this.
import('lodash').then(function (_) {
_.each([1, 2, 3, 4), function(item) {
// 1 etc.
})
});
Now, the interesting part about this is that this can be done asynchronously, once a user clicks a button, or in any other way interacts with the code.
This became very interesting to us since our goal was to reduce the heavy load of loading our bundle on our first page load. With this in mind, I figured this could be a very interesting way of loading our files on a per-page basis. Imagine if we could load the file that we had previously described using the Revealing Module Pattern to only load once that page was accessed.
Dynamic Imports + Webpack = <3
So dynamic imports isn’t a Javascript standard (yet) and cannot be natively used in browsers without introducing some sort of transpilation step. Except that webpack has already taken it upon themselves to accept this type of syntax, without using transpilation. This means that we can easily introduce this type of syntax without relying on anything else but webpack.
Lets look at the previous code sample again:
import(/* webpackChunkName: "lodash" */ 'lodash').then(function (_) {
_.each([1, 2, 3, 4), function(item) {
// 1 etc.
})
})
I’ve now edited the code to contain what’s known as a “magic comment”, which more or less is a comment that won’t affect the output of the code, but will help webpack to understand what we want to do with this module.
In the above case, it means that webpack will move this module into it’s own chunk, and separate it from the rest of the files. Without going too indepth here, you (typically) let webpack bundle multiple files into a single file (especially for SPA applications). In this case, this is the opposite of what we want, because we want your browser to only load whats absolutely necessary.
How do we use it?
Our end result looks something like the following:
const $ = require('jquery');
(function () {
var controller = $("meta[name=controller]").attr('content');
var action = $("meta[name=action]").attr('content');
var area = $("meta[name=area]").attr('content');
import(
/* webpackMode: "lazy", webpackChunkName: "module" */
`./${controller}/${action}`
).then(function (module) {
module.init();
}
})();
So what actually happens with the above code. Through our meta tags (this can of course also be parsed through the path) we can figure out which controller and action we are currently viewing. With this information (assuming we are at the Index action of the Home-controller), we will attempt to load the chunk for the file at the following path: ./Home/Index
.
Now, because of webpack magic (the best kind), webpack has already figured this out at compilation time and noticed that we have a file in that particular path, and has turned it into a chunk.
What happens when I load this page turns out to be pretty straight forward.
1. We load the razor view, which contains the bundle.js with the above code.
2. The above code executes, and loads the chunk for the Home/Index.js file.
3. The file is loaded (through the network) and isn't a part of the original page load.
What this means, in the end, is that we on each page load can choose exactly what we load. “Well, duh” - I hear you say, you’ve always been able to do that, simply put in the Home/Index file in a <script>
-tag. Yes, but what about the dependencies on that page, maybe I need to use lodash in the file.
Admittedly, yes, this could all be hardcoded, but I feel like the real win is what this looks like in my Layout.cshtml
file:
...
<body>
<!-- content -->
<script src="~/Scripts/dist/bundle.js"></script>
</body>
...
No manual dependencies added, even jQuery is added into the bundle.