Introduction
As many of you know, RequireJS is a library for implementing the module pattern in JavaScript; it is an essential component when working with large client side apps, like Single Page Applications. RequireJS facilitates the job of managing script dependencies, but when used in an ASP.NET based app, it introduces a new set of challenges. This tip will demonstrate how to integrate RequireJS effectively and improve performance at the same time.
Background
I have a SPA template based on ASP.NET MVC and KnockoutJS (github) project, which is nothing more than a modified version of the original ASP.NET Web Application template, but converted into a single page app, it uses KnockoutJS to manage the client side Views/Models and routing; this is a simple project demonstrating how to replicate the Angular approach with the performance power of Knockout.
We'll focus on how I integrated RequireJS in this project.
Using RequireJS
The first step is simple, remove all script
tags from the page and place just two:
/Views/Shared/_Layout.cshtml :
<script type="text/javascript" src="/Scripts/app/require.config.js" </script>
<script type="text/javascript" src="/Scripts/lib/require.js" </script>
The order here is important, we load up the configuration settings first then the actual require.js library, which will automatically detect those settings and apply them; what's in the configuration file? The mapping of our modules; all the libraries that were being requested manually, will now be managed by RequireJS as modules.
/Scripts/app/require.config.js :
var require = {
paths: {
"bootstrap": "/Scripts/lib/bootstrap.min",
"crossroads": "/Scripts/lib/crossroads.min",
"hasher": "/Scripts/lib/hasher.min",
"jquery": "/Scripts/lib/jquery-1.10.2",
"knockout": "/Scripts/lib/knockout-3.2.0",
"knockout-projections": "/Scripts/lib/knockout-projections",
"signals": "/Scripts/lib/signals.min",
"text": "/Scripts/lib/text",
"router": "/Scripts/app/router"
},
shim: {
"bootstrap": { deps: ["jquery"] }
}
};
The 'paths' setting defines all the required modules, each loaded from an individual source. We provide names (IDs) for each one which is how they are referenced and requested by our app main scripts.
The 'shim' option allows to configure dependencies manually in special cases like bootstrap, in this example we are specifying that it depends on jquery and RequireJS needs to provide it before loading bootstrap (more on this later).
Now we define the 'entry point' to our application, the main script for starting up the app:
/Scripts/app/home/startup.js
define('startup', ['jquery', 'knockout', 'router', 'bootstrap'], function ($, ko, router) {
.....
console.info("app started");
});
I omitted the 'component configuration & init'
code for clarity here, the main purpose of this block is to show how to create a module; the magic happens in the first line, we are using the define()
method which RequireJS interprets to establish a module for us; we give it name, then an array of string
s with the IDs of any dependencies that this new module needs to work with, as you can see here we are asking for jquery, knockout, router and bootstrap; these modules were mapped in the configuration block, so RequireJS knows how to locate them. The last parameter is the function with our code to be invoked, and as you see we are accepting three parameters ($, ko, router), RequireJS will inject into these the dependencies we requested so we can use them in our code.
Bootstrap is requested but not injected, and that's because we won't be interacting with the bootstrap object directly in this code. (it just needs to be made available)
So now the last step is to initialize our app by calling our statup.js:
/Views/Home/Index.cshtml :
<script type="text/javascript">
requirejs(["startup"]);
</script>
If everything works as expected, the site will fire up and the landing page displays. RequireJS runs our entry point script (startup.js) and begins loading dependent modules Asynchronously which is what we want, but in this case it presents an issue since our startup module uses most of those dependencies from the beginning, the net requests window shows the following when first loading:
After the initial load, most of those will be cached in the browser for subsequent requests, so the performance hit is found at the start; this might not be such a big problem here since we are only using a few modules, but modern applications may have dozens of dependencies so the logical next step is to try to optimize and minimize those requests. How can we do this in ASP.NET MVC?
Using the Code
If you are in the Mean stack, a solution is provided by using RequireJS Optimization framework, and there are ways to use this approach with ASP.NET, but you'll have to use Node.js and custom build processes.
So the obvious answer that most of you already thought of is, let's use ASP.NET Bundling and Minification! We can put all those scripts into a bundle and allow ASP.NET to serve us a single optimize file, like so:
/App_Start/BundleConfig.cs:
bundles.Add(new ScriptBundle("~/bundles/libs").Include(
"~/Scripts/lib/crossroads.js",
"~/Scripts/lib/hasher.js",
"~/Scripts/lib/jquery.js",
"~/Scripts/lib/knockout.js",
"~/Scripts/lib/knockout-projections.js",
"~/Scripts/lib/signals.js",
"~/Scripts/lib/text.js"
));
BundleTable.EnableOptimizations = true;
There is a problem though, we cannot just put a Scripts.Render("~/bundles/libs")
into our page, because that would create a separate <Script>
tag to load the bundle, RequireJS will show an error because it insists in being sole manager of script dependencies (which makes sense since they are loaded asynchronously).
So the solution is to change RequireJS's configuration to ask for these using the 'bundles' option:
/Scripts/app/require.config.js:
var require = {
bundles: {
'/bundles/libs?v=2015': [
"signals",
"crossroads",
"hasher",
"jquery",
"knockout",
"knockout-projections",
"text"
]
},
paths: {
"bootstrap": "/Scripts/lib/bootstrap.min",
},
shim: {
bootstrap: { deps: ["jquery"] }
}
};
In this new configuration, the important change is we are telling RequireJS to look for all those modules in a single location: '/bundles/libs?v=2015'
, which is the hard-coded path pointing to our ASP.NET Bundle ('?v=2015'
optional, force refreshing) it will serve the minified bundle as defined with all the module scripts combined. After compiling and running again, the results are substantial:
* There are a few other script bundles defined not shown in the configuration scripts. The same principle applies, the main objective is to minimize round trips to the server for individual resources.
* Notice also that bootstrap.min.js dependency still is being retrieved individually, and that is because bootstrap does not implement the module pattern (as of this writing), it is only available globally, so we need to let RequireJS decide when to load it.
That worked great, except...
Points of Interest
...There is a catch, there is always a catch; in this case you need to make sure your source libraries and scripts are 'named modules', that is they are 'defined' with the name of the module to be requested later on.
I found this the hard way after configuring as shown above I had all sorts of errors, after some debugging I noticed some of the dependencies were not being loaded (Knockout for instance) even though the code was in the bundle, what happened?
This is how some of the libraries were defining their modules (from their original sources):
if (typeof define === 'function' && define['amd']) {
define(['exports', 'require'], factory);
}
if (typeof define === 'function' && define.amd) {
define(['signals'], factory);
}
This was the problem, those modules were unnamed, which when bundled together prevented RequireJS from finding them among the rest. This is not an issue when the modules are loaded individually, since you usually 'map' the module to the file... (as shown in the initial configuration block) but if we put all those scripts into a single bundled file, then RequireJS will not be able to find any anonymous (unnamed) modules.
The quick solution, go into each source library and verify and fix by providing a default name like this:
if (typeof define === 'function' && define['amd']) {
define("knockout", ['exports', 'require'], factory);
}
if (typeof define === 'function' && define.amd) {
define("hasher", ['signals'], factory);
}
The only other drawback to this method is the fact that you'll have to define your scripts in two places, your ASP.NET bundle and the RequireJS configuration; other than that, this solution is simple enough to implement without any external requirements.
History
- Revision 1: Created 1/5/2015
- Updated 1/6/2015
- Revision 2: Created 1/7/2015
- Updated 1/9/2015