Why RequireJS?

RequireJS is a popular JavaScript module loader.  RequireJS solves the problem of needing to invoke some JavaScript code from another file, but not knowing if that file has been loaded yet.

It does this with either a define or a require declaration, which takes an array of strings and a callback function, then passes in the module names as arguments so you can reference them in your JavaScript code.

define(["jquery", "moment"],
    function ($, moment) {
        "use strict";

        var init = function () {
            // Requires MomentJS to be loaded. 
            // "moment" is the argument passed in by RequireJS for the MomentJS module, aliased as "moment" in the define/require statement.
            var now = new moment().format("M/D/YYYY h:mm:ss A");

            // Requires jQuery to be loaded. 
            // "$" is the argument passed in by RequireJS for the "jquery" module in the define/require statement.
            $("#currentDateTime").text(now);
        };

        return {
            init: init
        };
    }
);

Here "moment" is both the name of the module to load, and the argument passed in for that module name.  It could have been "m" throughout, and it would still work.  The same is also true for "jquery at the top in the define, which tells RequireJS what module to load, and "\(" below that, which tells RequireJS to load that module into an argument named "\)".  We're using "moment" and "\(" inside the function so the code looks conventional, but the "\)" could have been "j" if you wanted it to be.

RequireJS guarantees that the external modules are loaded by the time your code runs.  It also keeps track of which modules have already been loaded on the page, so it doesn't pull in the same module twice.

So which is it – require() or define()?

The require and define statement are the main ways you use RequireJS. The API signature is the same for both, but they work differently: "define" is for modules that will be loaded and run by code that uses a "require" statement to pull that module in. So think of "define" as your module definitions, and "require" as the way to execute them.

As you can see, the RequireJS usage is very simple and clean.  The setup can be a headache, especially around paths, so I'll take you through how I've done it.

Configuring your paths in main.js

RequireJS needs to be configured.  When we said "jquery" and "moment" above, we have to tell RequireJS where those files are so we can use those aliases.  That's where main.js and required.config() come in:

require.config({
    baseUrl: "/Scripts/app",
    paths: {
        jquery: "../lib/jquery-2.1.1",
        jqueryValidate: "../lib/jquery.validate",
        jqueryValidateUnobtrusive: "../lib/jquery.validate.unobtrusive",
        bootstrap: "../lib/bootstrap",
        moment: "../lib/moment"
    },
    shim: {
        jqueryValidate: ["jquery"],
        jqueryValidateUnobtrusive: ["jquery", "jqueryValidate"]
    }
});

require(["kickoff"], function(kickoff) {
        kickoff.init();
    }
);

RequireJS uses the baseUrl setting in main.js if you pass it a script name that is not an alias in the paths section.  In other words, if you call "jquery" in your define/require, it knows from your paths setting where to go.  If you call "myScript" in define/require, it looks for it in the baseUrl path, which is /Scripts/app in this case.

Notice there are no .js extensions to the end of the file names?  This is something RequireJS does to help you.  It's a JavaScript loader; it knows those are JavaScript files.  In my view, this just adds to some of the confusion of configuring this thing.

Here's how I've got my paths configured under my Scripts folder in Visual Studio:

https://cdn.volaresoftware.com/images/posts/2014/5/image_thumb.png

You can set up different folder structures or names based on your preferences, but it probably makes sense for main.js, which is the entry point of the app, to be at the root of /Scripts, or at least above your /app and /lib script folders.  Also, you don't have to name your file main.js. that's just a RequireJS convention.

The shim section in the code above is to tell RequireJS about any dependencies your files have before they can be used.  Here, we are saying if we call "jqueryValidate" to load that module, we have to load "jquery" first.  The "jqueryValidateUnobtrusive" module has to load "jqueryValidate" and "jquery" first, before it can be used.  You could probably shorten the shim for "jqueryValidateUnobtrusive" to just "jqueryValidate", since that module requires "jquery", and RequireJS tracks the whole dependency graph for you.  It doesn't hurt to leave it in, either.

For many apps, it makes sense to call code to load the app or run code on every page from the main.js file, and we've done that here with a call to "kickoff.init()".

Calling the main.js file

The normal advice for adding RequireJS to an app is to put this line in the <head> section of _Layout.cshtml.

<script data-main="Scripts/main" src="/Scripts/lib/require.js"></script>

This is a kind of weird looking script tag.  The src attribute is normal enough for pulling in RequireJS, but the data-main is new.  That's RequireJS's special attribute for getting the path to main.js.  Notice again there is no ".js" in the data-main.  RequireJS knows it's a .js file and appends that automatically.

This script tag is usually all you need for a single page app, where all modules run inside one full page request.

Set up for multi-page apps

I don't see too many single-page apps in the wild.  More often, they are multi-page apps where each page has a LOT going on.  If you aren't building a single page app, you can still take advantage of RequireJS, but we need to change some things.

We need to change the code that loads RequireJS and main.js in _Layout.cshtml from:

<script data-main="Scripts/main" src="/Scripts/lib/require.js"></script>

to:

    <script src="/Scripts/lib/require.js"></script>
    @RenderSection("scripts", required: false)

You can put this right before the closing </body> tag if you like, but RequireJS will inject <script> tags with the async attribute in the <head> section like this:

https://cdn.volaresoftware.com/images/posts/2019/12/script_before_closing_body_tag.png

Now we've got a common layout, and a hook in each page (the Razor @RenderSection) where we can load THAT page's scripts.  In each page, you'll then need something like:

@section scripts
{
    <script>
        require(["Scripts/main"],
            function () {
                require(["currentDateTime"],
                    function (currentDateTime) {
                        currentDateTime.init();
                    }
                );
            }
        );
    </script>
}

Each page will be different, depending on what you need to load and run, but you will always load and run main.js, then when that's done, you'll load the scripts just for that page in the inner require().  Note this inner require() is pulling in the code that was in the define() at the top of this post.

Now drop this code in on every page in your app, but first…

What about jQuery and DOM loading?

What if your JavaScript loads before the DOM is loaded?  With jQuery, we normally call:

$(function () {
    ...the code to run on DOM ready...
});

So how do we know the DOM is loaded before we start touching it in RequireJS world?  We need to use the domReady plugin with:

require(["domReady!"],
    function() {
        ...the code to run on DOM ready...
    }
);

Download the plugin and put it in the /Scripts/lib folder.  Now we have a new file we want to alias, so we need to add that to our require.config paths in main.js

paths: {
    jquery: "../lib/jquery-2.1.1",
    jqueryValidate: "../lib/jquery.validate",
    jqueryValidateUnobtrusive: "../lib/jquery.validate.unobtrusive",
    bootstrap: "../lib/bootstrap",
    moment: "../lib/moment",
    domReady: "../lib/domReady",
},

There we go.  We have another piece of our run-on-every-page code snippet.

MVC Helpers

When you start repeating yourself in an MVC view, consider adding an HTML Helper.  What we want generically is:

require(["Scripts/main"],
    function () {
        require(["...the module to load...", "domReady!"],
            ...run the module...
        );
    }
);

We also don't want to have to think about relative paths.  We want the scripts to find main.js and the /Scripts/app and /Scripts/lib folder whether the page is located at the root (/) or /Deep/Within/The/Site.

Here's the C# to do that:

using System;
using System.Text;
using System.Web;
using System.Web.Mvc;

namespace RequireJSWithMVC.Extensions
{
    public static class RequireJsHelpers
    {
        public static MvcHtmlString InitPageMainModule(this HtmlHelper helper, string pageModule)
        {
            var require = new StringBuilder();
            var scriptsPath = "~/Scripts/";
            var absolutePath = VirtualPathUtility.ToAbsolute(scriptsPath);

            require.AppendLine("<script>");
            require.AppendFormat("    require([\"{0}main.js\"]," + Environment.NewLine, absolutePath);
            require.AppendLine("        function() {");
            require.AppendFormat("            require([\"{0}\", \"domReady!\"]);" + Environment.NewLine, pageModule);
            require.AppendLine("        }");
            require.AppendLine("    );");
            require.AppendLine("</script>");

            return new MvcHtmlString(require.ToString());
        }
    }
}

What we're going for is having several things loaded on every page: 1) require.js, 2) main.js, 3) the DOM, and 4) the main JavaScript module for that page and anything that module needs.  This "main module" concept works well to just run as it's loaded instead of forcing the caller to invoke it with an init() call, so let's change our "currentDateTime.js" module from above to do that and create "main-currentDateTime.js" instead:

require(["jquery", "moment"],
    function ($, moment) {
        "use strict";

        // Requires MomentJS to be loaded. 
        // "moment" is the argument passed in by RequireJS for the MomentJS module, aliased as "moment" in the require statement.
        var now = new moment().format("M/D/YYYY h:mm:ss A");

        // Requires jQuery to be loaded. 
        // "$" is the argument passed in by RequireJS for the "jquery" module in the require statement.
        $("#currentDateTime").text(now);
    }
);

We've also changed from define() to require() since this is a self-loading module, not one a caller has to invoke.  We got rid of the init() function so this code runs when the module is loaded.

Download

That should do it for adding RequireJS to your MVC mutli-page app.  All the code above is available for view/download on GitHub.