Making RequireJS play nice with ASP.NET MVC
Working with web-projects are getting more and more Javascript heavy, and good developers as we are, we cry out for structure! There are several ways of achieving structure when working with Javascript, and in this blogpost we'll look into RequireJS.
This is not an in-depth RequireJS tutorial, but we'll touch on the basics. If you feel it's over your head, then take a look at RequireJS.org to get started. Keep reading though, as you might not need to :)
The problem with RequireJS and ASP.NET MVC###
RequireJS.org is a great little piece of Javascript magic that will help you modularize your Javascript and keep track of dependencies and also, with a little extra work, uglify and minify everything. The problem is that most tutorials and articles on RequireJS makes the assumption that we are building a Single Page Application, and the application will have a single point of Javascript-entry - normally main.js or app.js
Although this might be the case in many projects, it's far from mainstream or the only way to make an MVC web-app. When working with multiple Javascript-heavy pages in MVC we end up repeating ourselves alot when working with RequireJS, and the build-process is a whole other story. In this post we'll try to leverage Visual Studio 2012, a bit of ASP.NET MVC magic and some Resharper (optional) sugar in the end to make the whole process a bit easier and RAD-friendly!
Step 1 - Setting up###
Starting out with an empty ASP.NET MVC4 solution, let's start by adding a /Views/Shared/_Layout.chtml
with some basic markup, and a /Views/_Viewstart.cshtml
to get us set up. I'm going to add Twitter Bootstrap to make it look like someone else than me designed it.
_Layout.cshtml
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<link href="~/Content/bootstrap.css" rel="stylesheet" />
</head>
<body>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<ul class="nav">
</ul>
</div>
</div>
</div>
<div class="container">
@RenderBody()
</div>
</body>
</html>
Next up, let's pull down Require.js from Nuget (install-package RequireJS
)
I'm also going to add a simple HomeController
with a single Index
action, with the corresponding view.
The RequireJS basics###
RequireJS works by loading the Require.js file in a regular script-tag but with a minor difference; you also add reference to the starting point of your application in a data-main
attribute, like this:
<script src="~/Scripts/Require.js" data-main="/public/js/app" />
Notice how we're omitting the .js-extension from the data-main
attributevalue? Well, that's because RequireJS treats every Javascript file as a single module, expecting every .js-file to contain only one module. The above example simply tells RequireJS to load the file /public/js/app.js when it's ready.
The app.js file could look like this:
require([], function(){
console.log('RequireJS loaded me!');
});
The require
keyword tells RequireJS that this piece of code is dependent on other modules that needs to be loaded before it's executed. In this example I omitted all dependencies, but let's add one just to get familiar with the syntax
I'll add the folder /public/js/common
and create a file called capitalizer.js
in it:
/public/js/common/capitalizer.js
define(function(){
return function(str){
return str.toUpperCase();
}
});
Notice we're using define
instead of require
? That's because we're telling RequireJS that this is a module that will not only possibly depend on others, but it's dependable by other modules created both with the define
and the require
function. Other than your entry-point, you'll most likely use the depend
keyword to define modules.
All this module does is return a function that capitalizes whatever string we send in. Going back to our app.js
we can now pull in that depenency like this:
/public/js/app.js
require(['/public/js/common/capitalizer.js'], function(capitalizer){
console.log(capitalizer('RequireJS loaded me!'));
});
This should result in the text being outputted in uppercase in the console. Whoop!
Although this works fine, it doesn't really look that neat. Luckily RequireJS can help us with that with some configuration. Before all the code in app.js, we'll add add a call to requirejs.config
and pass in some properties - keeping it simple for now
/public/js/app.js
requirejs.config({
baseUrl: '/public/js'
});
require(['common/capitalizer'], function(capitalizer){
console.log(capitalizer('RequireJS loaded me!'));
});
The baseUrl
property tells RequireJS where all the Javascript-modules will recide, and we can now use a much cleaner syntax for our dependency - common/capitalizer
. Again without the extension, as RequireJS will now assume there is a folder named common with a Javascript-file called capitalizer.js
in it, containing one module.
Refreshing the page confirms this, by still outputting the text uppercase
Moving on - RequireJS, MVC and Conventions###
Some of the code in the next sections have been inspired or copied from http://tech.pro/tutorial/1156/using-requirejs-in-an-aspnet-mvc-application - which was the basis for me writing this blogpost :)
Now that we are RequireJS experts, it's time to move on. Since I'm not working in a single page application, I'd like to have one app.js
file for each separate View, that will be responsible for loading the required dependencies for that view. So for Home/Index.cshtml
I want a Home/index.js
that recides in /public/js/views/home/
. Not only that, but I want the RequireJS config to be the same for all my viewspecific .js-files also. Doing this manually for Index.cshtml
would mean that we need to pull the requirejs.config
out into a separate file. Let's just call that config.js
and put it in our /public/js
folder:
/public/js/config.js
requirejs.config({
baseUrl: '/public/js'
});
Then let's take the remainder of that file, and place that in our newly created /public/js/views/home
folder:
/public/js/views/home/index.js
require(['common/capitalizer'], function(capitalizer){
console.log(capitalizer('RequireJS loaded me!'));
});
Now we need to do some changes in the way we load RequireJS and our index.js
in _Layout.cshtml
(we wont keep this in _Layout, but just stick with me...):
_Layout.cshtml
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<link href="~/Content/bootstrap.css" rel="stylesheet" />
</head>
<body>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<ul class="nav">
</ul>
</div>
</div>
</div>
<div class="container">
@RenderBody()
</div>
<script src="~/Scripts/Require.js" />
<script>
require(['/public/js/config.js'], function(){
require(['views/home/index']);
});
</script>
</body>
</html>
What we did, was remove the data-main
attribute and instead load up the app as inline Javascript. First we require the config.js
, and once that's loaded, we require the index.js
file. Since we haven't loaded the config with baseUrl yet, we're required to specify the full path with extension to the config.js
, but once inside the callback, the configuration has been run and we can work with the modular syntax of RequireJS. We also don't need a callback on the index.js
require-call as the code within that file will simply be run.
Looking at the modulename views/home/index
tells me that we should be able to do this with a HtmlHelper. I wont explain this code in detail, as it is snipped pretty much from the blogpost mentioned above:
RequireJSHelpers.cs
public static class RequireJsHelpers
{
public static MvcHtmlString RequireJs(this HtmlHelper helper, string config, string module)
{
var require = new StringBuilder();
string jsLocation = "/public/js/";
if (File.Exists(helper.ViewContext.HttpContext.Server.MapPath(Path.Combine(jsLocation, module + ".js"))))
{
require.AppendLine("require( [ \"" + jsLocation + config + "\" ], function() {");
require.AppendLine(" require( [ \"" + module + "\"] );");
require.AppendLine("});");
}
return new MvcHtmlString(require.ToString());
}
public static MvcHtmlString ViewSpecificRequireJS(this HtmlHelper helper)
{
var action = helper.ViewContext.RouteData.Values["action"];
var controller = helper.ViewContext.RouteData.Values["controller"];
return helper.RequireJs("config.js", string.Format("views/{0}/{1}", controller, action));
}
}
The only difference is that I've added a method I've called ViewSpecificRequireJS
which investigates the RouteData, and creates a path for the current view. This means we can now remove our added Javascript code from _Layout.cshtml
, and replace it with a call to @Html.ViewSpecificRequireJS()
. I also added a File.Exists call, that simply outputs an empty string if the Javascript-file does not exist for the given view.
With this in place, all we need to do to load our View-specific Javascript is to add a corresponding .js when we add a new View. Let's try that by creating a UserController
with a Login
Action. In addition to the controller, we'll create the /public/js/views/user/login.js
and just keep the contents like simple:
/public/js/views/user/login.js
define(function(){
console.log('Navigated to Login!');
});
Navigating to /user/login
should display the text "Navigated to Login!" in the console.
Uglifying and minifying###
If you don't care for minifying, you can just skip this section as you should now have a working solution with some nice automatic conventionbased boilerplating.
When we installed RequireJS from NuGet, we got two files - Require.js
and r.js
. The latter is a big-ass Javascript file that runs with <NodeJS or a whole lot of different stuff. We're going to stick with NodeJS, as that's pretty straight forward. I must admit I struggled a bit with the whole r.js stuff, but the solution I'm going to show you will work as a charm. Perhaps it's not the pretties of solutions, but it's hard to beat "It works!"
You might be asking 'Why not just use the bundling and minification provided in ASP.NET MVC?'. Well, you could try, but it will blow up in your face and your website will literally melt down. Everything with RequireJS happens asynchronously, and the minification in MVC makes RequireJS all confused, and it just gives up. That's why we have to use r.js
.
I found r.js
a bit confusing to use at first, but once I came to terms with it, it's not all that bad. You can use it in two ways - processing a single file, or processing multiple files. Again, the majority of examples focuses on the first, but we're interested in the latter. For that we need a configuration-file that r.js
can read, in order to know what and where to minify. The way it works when doing this is that it makes a replica of the scripts-folder you specify and minifies the Javascript-files recursivly, making sure dependencies are nested correctly. We're going to create /public/_build/
and put a build.js
file there:
/public/_build/build.js
({
appDir: '../js',
baseUrl: './',
mainConfigFile: '../js/config.js',
dir: '../release',
modules: [
{
name: 'views/home/index'
}
]
})
You can read more about this file at RequireJS in the Optimization section, but I'll briefly explain the four properties:
- appDir
- This is the directory that should be replicated for minification
- baseUrl
- The location of your app relative to appDir
- mainConfigFile
- This provides the build-config with the configuration-file for RequireJS. This should be relative from build.js
- dir
- This is the destination-path that the app-path should be copied to
- Modules
- We need to define what modules we should minify and track dependencies for. Everything not listed here will only be uglified.
Let's try to run r.js with this build.js, and see what happens. Open a PowerShell console (CommandLine works just fine, but it's not as blue and cool :)), and navigate to the _build
folder we created. From here run the command node ..\..\scripts\r.js -o build.js
. This will hopefully output something like this:
Tracing dependencies for: views/home/index Uglifying file: C:/Dropbox/Projects/RequireJS/MvcApplication2/public/release/common/capitalizer.js Uglifying file: C:/Dropbox/Projects/RequireJS/MvcApplication2/public/release/config.js Uglifying file: C:/Dropbox/Projects/RequireJS/MvcApplication2/public/release/views/home/index.js Uglifying file: C:/Dropbox/Projects/RequireJS/MvcApplication2/public/release/views/user/login.js views/home/index.js ---------------- common/capitalizer.js views/home/index.js
As we see, it's looking in the index.js and resolving the dependencies for it. Opening up the index.js
should reveal a neatly minified Javascript-file:
/public/release/views/home/index.js
define("common/capitalizer",[],function(){return function(e){return e.toUpperCase()}}),define("views/home/index",["common/capitalizer"],function(e){console.log(e("I was loaded by RequireJS"))});
It even pulled in our capitalizer-module so that it will be loaded properly when views/home/index
requires it.
NodeJS
A small sitestep is neccessary here. In your team, it's probably best if every developer has to install NodeJS, so I recommend creating a folder next to your solution folder where you can stick the executable (it's all you need). This way we can have node.exe checked in to source-control, and be sure that it's always there. Let's just add a post-build event as well. In my case it looks like this: $(SolutionDir)\Tools\node.exe $(ProjectDir)\Scripts\r.js -o $(ProjectDir)\public\_build\build.js
. You might need to mend it to fit your structure.
Aw crap, 'Include In Project' every time I minify?###
That's what I thought, but I came across a little trick that solves this. It requires mending your .csproj file, and simply adding the following somewhere:
<Content Include="public\release\**" />
This will tell Visual Studio that the folder public\release and all it's subitems should always be included in the project. NB: You will not automatically see changes in the folder when new files are generated. For that you'll need to reload the project. This does not matter, however, as you don't have to care about this, as long as the next ones opening the project (a build-server for instance) gets all the content loaded.
Important: do not manually use the 'Include in project' on any items in this folder after you added the line in the project-file, as that will cause stuff to appear twice in Solution explorer.
Revisting build.js###
So now we've got conventionbased boostrapping and automatic minifiying on build, and everything looks good. Except for one thing - the modules
-setting in our build.js
. I can really see this becoming somewhat of a hassle as I add more and more views - or better yet - delete them. For this I decided to write something that I literally never use - enter T4 TextTemplates! Let's delete our build.js file, and let Visual Studio create it for us.
Again, I'd like you to just take my word for it and study the code yourself. The important thing here is that it does exactly what I want it to do:
/public/_build/build.tt
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Configuration" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".js" #>
({
appDir: '<#= relativeBaseUrl #>',
baseUrl: './',
mainConfigFile: '<#= relativeBaseUrl #>/config.js',
dir: '../release',
modules: [
<# foreach(string path in System.IO.Directory.GetFiles(this.Host.ResolvePath(relativeBaseUrl+"/views"), "*.js", System.IO.SearchOption.AllDirectories)) { #>
{
name: '<#= GetRequireJSName(path) #>'
},
<# } #>
],
onBuildRead: function (moduleName, path, contents) {
if (moduleName === "config") {
return contents.replace("/public/js","/public/release")
}
return contents;
},
})
<#+
public const string relativeBaseUrl = "../js";
public string GetRequireJSName(string path){
var relativePath = Path.GetFullPath(path).Replace(Path.GetFullPath(this.Host.ResolvePath("..\\js\\")), "");
return Path.Combine(Path.GetDirectoryName(relativePath), Path.GetFileNameWithoutExtension(relativePath)).Replace("\\", "/");
} #>
Basically what this does, is look through our /public/views/
folder recursivly, and outputting a modules-setting for each of the files found there.
With our current structure, we're left with a build.js
looking like this:
({
appDir: '../js',
baseUrl: './',
mainConfigFile: '../js/config.js',
dir: '../release',
modules: [
{
name: 'views/home/index'
},
{
name: 'views/user/login'
},
],
onBuildRead: function (moduleName, path, contents) {
if (moduleName === "config") {
return contents.replace("/public/js","/public/release")
}
return contents;
},
})
We've added one more setting, namely the onBuildRead
setting. This will trigger for each module, and in our case we're making sure that the path /public/js
is replaced with the /public/release
folder that the minified files will end up in. This will trigger only when the module it's loading is the config.js
module. Building again should now output the node-progress to the Output-panel in Visual Studio:
Tracing dependencies for: views/home/index Tracing dependencies for: views/user/login Uglifying file: C:/Dropbox/Projects/RequireJS/MvcApplication2/public/release/common/capitalizer.js Uglifying file: C:/Dropbox/Projects/RequireJS/MvcApplication2/public/release/config.js Uglifying file: C:/Dropbox/Projects/RequireJS/MvcApplication2/public/release/views/home/index.js Uglifying file: C:/Dropbox/Projects/RequireJS/MvcApplication2/public/release/views/user/login.js views/home/index.js ---------------- common/capitalizer.js views/home/index.js views/user/login.js ---------------- views/user/login.js
And as we can see, it picked up both the index and the login module.
For the story to be complete, we need a way to load either the minified or the unminified Javascript-files. We'll do this by simply adding a compiler directive to our RequireJSHelpers.cs:
public static class RequireJsHelpers
{
public static MvcHtmlString RequireJs(this HtmlHelper helper, string config, string module)
{
var require = new StringBuilder();
string jsLocation = "/public/release/";
#if DEBUG
jsLocation = "/public/js/";
#endif
if (File.Exists(helper.ViewContext.HttpContext.Server.MapPath(Path.Combine(jsLocation, module + ".js"))))
{
require.AppendLine("require( [ \"" + jsLocation + config + "\" ], function() {");
require.AppendLine(" require( [ \"" + module + "\"] );");
require.AppendLine("});");
}
return new MvcHtmlString(require.ToString());
}
public static MvcHtmlString ViewSpecificRequireJS(this HtmlHelper helper)
{
var action = helper.ViewContext.RouteData.Values["action"];
var controller = helper.ViewContext.RouteData.Values["controller"];
return helper.RequireJs("config.js", string.Format("views/{0}/{1}", controller, action));
}
}
We're simply telling it to default to /public/release/
as our Javascript path, but change it to /public/js/
whenever we're building with Debug. And that's that! Change your build-configuration to Release, build, and refresh the site. You should now end up with the minified Javascripts loaded by RequireJS. Back to Debug-configuration, and you get your full Javascript-files!
Conclusion###
RequireJS is a really powerful framework, and can do wonders for the structure in your application. For a SPA it really shines, and it can shine for a MPA as well, but in order to 'do it right' we end up with alot of repetition. Hopefully my blog-post have given you some ideas as to how you can get around just that.
I've more or less followed my own blogpost in this repository on Github, so feel free to download/fork/clone and play around. Pull-requests making it even better is more than welcome!
Hope you enjoyed this lengthy blogpost, and thanks for reading!