Building Single Page Application – Bundle Orderer

27 בדצמבר 2013

In my previous post I discussed a way to arrange scripts in a single page application. Short story: each application layer is associated with exactly one bundle.

Assuming a vertical dependency between layers, we can just include the bundles in the right order and any time we add a new file to a layer that file is automatically added in the right position with respect to other files in other layers.

For example, below is a common bundle configuration in my single page applications

bundles.Add(new ScriptBundle("~/bundles/lib")
    .IncludeDirectory("~/Scripts/Lib""*.js"true));
bundles.Add(new ScriptBundle("~/bundles/server")
    .IncludeDirectory("~/Scripts/Server""*.js"true));
bundles.Add(new ScriptBundle("~/bundles/bl")
    .IncludeDirectory("~/Scripts/BL""*.js"true));
bundles.Add(new ScriptBundle("~/bundles/views")
    .IncludeDirectory("~/Scripts/Views""*.js"true));
bundles.Add(new ScriptBundle("~/bundles/app")
    .IncludeDirectory("~/Scripts/App""*.js"true));

Please note that each bundle is configured to include a specific directory. This means that when you add a new file to the directory the file is automatically added to the bundle. The source HTML looks as below:

<script src="/Scripts/Lib/jquery.js"></script>
<script src="/Scripts/Lib/jquery.validate.js"></script>
<script src="/Scripts/Lib/jquery.validate.unobtrusive.js"></script>
<script src="/Scripts/Lib/bootstrap.js"></script>
<script src="/Scripts/Lib/modernizr.js"></script>
<script src="/Scripts/Lib/respond.js"></script>

<script src="/Scripts/Server/Authentication.js"></script>
<script src="/Scripts/Server/Contacts.js"></script>
<script src="/Scripts/Server/Users.js"></script>

<script src="/Scripts/BL/Contact.js"></script>
<script src="/Scripts/BL/User.js"></script>

<script src="/Scripts/Views/MainView.js"></script>
<script src="/Scripts/Views/NotificationView.js"></script>
<script src="/Scripts/Views/SiteMenuView.js"></script>

While the order between bundles is set by the us it is not clear how the order inside the bundle is set. Short answer: alphabetical. However, there are some exceptions. For example, look at lib bundle. Bootstrap, modernizr and respond are indeed ordered by alphabetical order. But what about jquery? Why is it first? This is by design. Microsoft assumes (quite reasonably) that jquery holds no dependency and many libraries are dependent upon it and therefore is should be first.

How can we control the order of the bundle? Read below …

Each bundle can be associated with an object named Bundle Orderer. This object must implement an interface named IBundleOrderer (the interface looks a bit different under ASP.NET MVC 4)

public interface IBundleOrderer
{
    IEnumerable<BundleFile> OrderFiles(BundleContext context,IEnumerable<BundleFile> files);
}

As you can see the interface is quite simple. You get a list of files to be ordered and return them in your preferred order.

But, how should we implement it? I guess we can think about some strategics.

Cassette has a nice strategic to order files. It looks at the head of each file looking for Visual Studio references like the one below

/// <reference path="Users.js" />

It assumes that a script reference means a dependency. Then it scans the tree of dependencies and output the correct flat list of ordered java script files.

We can implement this behavior with a bit of coding

public IEnumerable<BundleFile> OrderFiles(BundleContext ctx, IEnumerable<BundleFile> files)
{
    Dictionary<stringNode> nodes = new Dictionary<stringNode>();

    //
    //  Build a node object for each file and collect dependency information for it
    //
    foreach (BundleFile bundleFile in files)
    {
        string virtualPath = bundleFile.VirtualFile.VirtualPath;
        string filePath = ctx.HttpContext.Server.MapPath(virtualPath);
        FileInfo file = new FileInfo(filePath);

        //
        //  Create a node for the current file (the node may be already created)
        //
        string fileId = GetFileID(file.FullName);
        Node node = GetCreateNode(nodes, fileId);
        node.BundleFile = bundleFile;

        //
        //  Extract dependencies from script's header
        //
        string[] dependencies = ParseDepenedencies(ctx, bundleFile, file);
        foreach (string dependencyId in dependencies)
        {
            Node dependencyNode = GetCreateNode(nodes, dependencyId);
            dependencyNode.Children.Add(node);
        }

        if (dependencies.Length > 0)
        {
            //
            //  Since this node has dependency it is no longer considered a root
            //
            node.IsRoot = false;
        }
    }

    //
    //  Set the level field for each node
    //  Start with root nodes and increase the level for each encountered child node
    //
    foreach (Node rootNode in (from n in nodes.Values where n.IsRoot select n))
    {
        WalkBFS(rootNode, 0);
    }

    //
    //  Arrange all nodes into groups of levels
    //  The key is a level. The value is a list of all nodes that belong to 
    //  this level
    //
    Dictionary<intList<Node>> nodesByLevel = new Dictionary<intList<Node>>();
    foreach (Node node in nodes.Values)
    {
        List<Node> levelNodes;
        if (!nodesByLevel.TryGetValue(node.Level, out levelNodes))
        {
            levelNodes = new List<Node>();
            nodesByLevel.Add(node.Level, levelNodes);
        }

        levelNodes.Add(node);
    }

    //
    //  Merge all nodes into a flat list. First are level 0 nodes and then level 1 and so on
    //
    List<Node> flat = new List<Node>();
    int level = 0;
    while (nodesByLevel.ContainsKey(level))
    {
        foreach (Node node in nodesByLevel[level])
        {
            //
            //  During our scan we might encounter files that are not part of 
            //  the current bundle. We should ignore them and return only files
            //  that originally we were requested to order
            //
            if (node.BundleFile != null)
            {
                flat.Add(node);
            }
        }

        ++level;
    }

    return flat.Select(n=>n.BundleFile).ToArray();
}

Our custom bundle orderer need to be set on a bundle object. I've created an extension method to help with that

public static void IncludeDirectory(this BundleCollection bundles, 
                                    string bundleVirtualPath, 
                                    params string[] directories)
{
    ScriptBundle bundle = new ScriptBundle(bundleVirtualPath);
    foreach (string dir in directories)
    {
        DoIncludeDirectory(bundle, dir);
    }

    bundle.Orderer = defaultOrderer;
    bundles.Add(bundle);
}

Updated bundling configuration using our custom bundle orderer looks as below

bundles.IncludeDirectory("~/bundles/lib","~/Scripts/Lib");
bundles.IncludeDirectory("~/bundles/server""~/Scripts/Server");
bundles.IncludeDirectory("~/bundles/bl""~/Scripts/BL");
bundles.IncludeDirectory("~/bundles/views""~/Scripts/Views");
bundles.IncludeDirectory("~/bundles/app""~/Scripts/App");

Let's analyze bl bundle. This layer contains only two files with no script references inside them. The generated HTML for it is

<script src="/Scripts/BL/Contact.js"></script>
<script src="/Scripts/BL/User.js"></script>

Now, lets add a script reference from Contact.js to User.js

/// <reference path="User.js" />

The script reference causes our custom bundle orderer to understand that User.js is a dependency of Contact.js and therefore it should be included first. Updated HTML is now correct

<script src="/Scripts/BL/User.js"></script>
<script src="/Scripts/BL/Contact.js"></script>

Full sample can be downloaded from BundlesInsideSPA

If your are interested only at the custom bundle orderer you can download it from GitHub

Enjoy, …

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*