Pages

Showing posts with label Bundling Optimization. Show all posts
Showing posts with label Bundling Optimization. Show all posts

Friday, January 23, 2015

Handling JavaScript Dependencies and ASP.Net MVC Framework

In functional or component base programming, tracking dependencies is important.

Methods handling dependency tracking can be handled client side, however, I was using ASP.Net MVC bundling and I wanted server side to handle the JavaScript dependency tracking.

ASP.Net MVC bundling allows extensible points, where I wrote a class with a method that traced a specified directory and included the dependent files into a bundle.

I was using JScript IntelliSense the “Reference” Tag at the top of the JavaScript files, which “if the “path” attribute points to another JS file, any objects or functions defined inside that file-or in a file referenced by that file-will show up in IntelliSense.”

Code below, using the reference tags, I was able to trace dependencies.

I created a class called BundleDependencies and created a reference to this class when registering bundles.

var bundleDependencies = new BundleDependencies();

and calling referencePath method, which returns an array of dependent file names.

This method accepts a relative path off the root to the directory. The files located in the specified directory will be root parent files. The root parent files will not be included in the bundle.

The root parent file is the file that contains the reference. Only reference files will be included in the bundle and not the root parent file. Referenced files that have reference to another file isn't considered a parent file. There can only be one parent file, so all files with a reference will be traversed.

JavaScript file can contain more than one reference:
/// <reference path="Components/DropBox.js" />

/// <reference path="Components/PageTitle.js" />

/// <reference path="Components/RosterList.js" />
and reference a file located in another directory. Must be relative and strictly follow ../ notation:
/// <reference path="../../Application/Model.js" />
Since Visual Studio supports Reference tags, all is need is drag JavaScript file from Solution Explorer and drop on the working JavaScript file.


Bundle Configuration Class

namespace com.hillHigh1980
{
    public class BundleConfig
    {
        public static void RegisterBundles(BundleCollection bundles)
        {
            var bundleDependencies = new BundleDependencies();

            // Script Bundles ================================
            var scriptBundle = new ScriptBundle("~/bundles/Views/Home");
            scriptBundle.Include(bundleDependencies.referencePath("Scripts/Views/Home/Components"));
            scriptBundle.IncludeDirectory("~/Scripts/Views/Home", "*.js", true);
            scriptBundle.Orderer = new NonOrderingBundleOrderer();
            bundles.Add(scriptBundle);

        }
    }
}



BundleDependencies Class

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;

namespace com.hillHigh1980.Models
{
    public class BundleDependencies
    {

        List<string> fileDependencies(string directory, string file, string scriptPath)
        {
            var regularExpression = new Regex(@"^///\s?$", RegexOptions.IgnoreCase);
            Match match;

            List<string> reference;
            string relativePath;
            List<string> directoryParts;
            int countRelativePaths;
            int countFolders;
            var references = new List();
            Func<string,bool> parents = delegate(string parent) { return parent == ".."; };

            var rows = File.ReadAllLines(file);
            foreach (var row in rows)
            {
                match = regularExpression.Match(row);

                if (match.Success)
                {
                    //Parse reference row
                    reference = new List(match.Groups[1].Value.Split('"')[1].Split('/'));
                    countRelativePaths = reference.Count(parents);
                    reference.RemoveRange(0, countRelativePaths);

                    //Parse directory
                    directoryParts = directory.Split('\\').ToList();
                    countFolders = directoryParts.Count() - 1;

                    //Create path where reference is located.
                    directoryParts.RemoveRange(countFolders - countRelativePaths + 1, countRelativePaths);
                    directoryParts.AddRange(reference);

                    //Complete path to reference file.
                    file = string.Join(@"\", directoryParts.ToArray());

                    //Create relative path, which will be added to bundle.
                    relativePath = "~/" + file.Substring(file.IndexOf(scriptPath)).Replace(@"\", "/");

                    //Creating directory path, don't need file.
                    directoryParts.RemoveAt(directoryParts.Count() - 1);
                    directory = string.Join(@"\", directoryParts.ToArray());

                    //Specified refernce may contain additional references.
                    references.AddRange(fileDependencies(directory, file, scriptPath));

                    references.Add(relativePath);
                }
                else
                {
                    //Limit to the first line. All done if not found.
                    break;
                }
            }

            return references;
        }

        //Locates all files within a specified directory.
        //All sub-directories are searched if includeSubDirectories is set true.

        public string[] referencePath(string viewPath, bool includeSubDirectories = false, string scriptPath = "Scripts")
        {
            var references = new List();
            List<string> result;
            string[] directories;

            if (includeSubDirectories)
            {
                directories = Directory.GetDirectories(HttpContext.Current.Server.MapPath(viewPath));
            }
            else
            {
                directories = new[] {HttpContext.Current.Server.MapPath(viewPath)};
            }


            foreach (var directory in directories)
            {
                foreach (var file in Directory.GetFiles(directory))
                {
                    result = fileDependencies(directory, file, scriptPath);
                    references.AddRange(result);
                }
            }

            var responses = references.ToArray();

            return responses;
        }

    }
}
}


NonOrderingBundleOrderer Class


using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Optimization;

namespace com.hillHigh1980.Models
{
    class JSFileTransformer : IBundleTransform
    {
        public void Process(BundleContext context, BundleResponse response)
        {

        }
    }

    class NonOrderingBundleOrderer : IBundleOrderer
    {
        public IEnumerable OrderFiles(BundleContext context, IEnumerable files)
        {
            var count = files.Count();
            var arry = files.ToArray();
            var j = 1;
            BundleFile hold = arry[0];
            BundleFile temp;

            for (int i = 0; i < count; i++)
            {
                if (hold.VirtualFile.Name.IndexOf("_View") == -1)
                {
                    if (j < count)
                    {
                        temp = arry[j];
                        arry[j] = hold;
                        hold = temp;
                        j += 1;
                    }
                }
                else
                {
                    arry[0] = hold;
                    break;
                }
            }

            return arry;
        }
    }
}

Monday, June 2, 2014

AngularJS and MVC 4 Bundling Optimization

When bundling AngularJS controllers, MVC 4 optimization best optimizes by renaming variables. Shrinking of variables reduces file size, however, when incorporating AngularJS and creating a controller, the $scope variable can NOT be renamed.

Not a big fan of third party scripts, yet the company that I was employed with, I had to find a work around.

This is why I enjoy .Net MVC, because I’m not stuck with a deterministic framework. MS MVC offers many tools. Below is how I got Angular to be minified.
Create a new C# class (NoRenameTransform.cs) and extended the IBundleTransform interface.

Include these namespaces:
System.Web.Optimization
Microsoft.Ajax.Utilities


using System;
using Microsoft.Ajax.Utilities;
using System.Web;
using System.Web.Optimization;

And implement the IBundleTransform interface

public class NoRenameTransform : IBundleTransform
{
    protected static Minifier compiler = new Minifier();

    public void Process(BundleContext context, BundleResponse response)
    {
        var codeSettings = new CodeSettings
        {
            NoAutoRenameList = "$scope"
        };
        response.Content = compiler.MinifyJavaScript(response.Content, codeSettings);
    }
}

The Micorsoft.Ajax.Utilities exposes the Minifier / Minifer ASP.NET object, which has a MinifyJavascript method. MinifyJavaScript method optimizes the file’s contents.

Micorsoft.Ajax.Utilities also exposes the CodeSettings object.

Create a field instantiating Minifier object.

Within the Process method, create a variable instantiating CodeSettings object. Set the NoAutoRenameList property to an array of strings of the item(s) that shouldn’t be changed. The $scope variable will not be minified.

var codeSettings = new CodeSettings
{
    NoAutoRenameList = "$scope"
};

IBundleTransform contract expects a Process method, and the Process method arguments expose BundleContext and BundleResponse.

Within the registered bundle configuration file, instantiate a new ScriptBundle class. ScriptBundle inherits from the Bundle class.

ScriptBundle exposes a Generic IList Transforms property.

Clear Transform

Add a new reference to the NoRenameTransform class.

var scriptBundleCore = new ScriptBundle"~/bundles/Modules/EventsApp");
scriptBundleCore.Include("~/Scripts/Modules/EventsApp/app.js");
scriptBundleCore.IncludeDirectory("~/Scripts/Modules/EventsApp/controllers", "*.js", true);
scriptBundleCore.Transforms.Clear();
scriptBundleCore.Transforms.Add(new NoRenameTransform());
bundles.Add(scriptBundleCore);


Bundle lifecycle:
Global.asax, Application_Start, Bundles are registered

BundlesConfig.RegisterBundles(BundleTable.Bundles);

BundlesConfig.RegisterBundles is called and ScriptBundle objects are added to the BundleTable.
ScriptBundles exposes a default transform property, which can be cleared.

scriptBundleCore.Transforms.Clear();

Add new behavior with how the bundle should be minified.

scriptBundleCore.Transforms.Add(new NoRenameTransform());
The transform Process method is called from when the razor view is rendered.

@Scripts.Render("~/bundles/Modules/EventsApp")

Complete Code

Razor View

@{
    ViewBag.Title = "Index";
}

<div ng-app class="container">
    <div ng-controller="EventController">
        {{name}}
    </div>
</div>

@section scripts {
    @Scripts.Render("~/bundles/Modules/EventsApp")
}

Config.asax

using FileUpload_MVC.App_Start;
using System;
using System.Collections.Generic;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace FileUpload_MVC
{
 
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundlesConfig.RegisterBundles(BundleTable.Bundles);

        }
    }
}

BundleConfig.cs

using FileUpload_MVC.Models;
using System.Web.Optimization;

namespace FileUpload_MVC.App_Start
{
    public class BundlesConfig
    {
        public static void RegisterBundles(BundleCollection bundles)
        {
            bundles.Clear();
            BundleTable.EnableOptimizations = true;

            var scriptBundleCore = new ScriptBundle("~/bundles/Modules/EventsApp");
            scriptBundleCore.Include("~/Scripts/Modules/EventsApp/app.js");
            scriptBundleCore.IncludeDirectory("~/Scripts/Modules/EventsApp/controllers", "*.js", true);


            scriptBundleCore.Transforms.Clear();
            scriptBundleCore.Transforms.Add(new NoRenameTransform());
            
            
            bundles.Add(scriptBundleCore);
            
        }
    }
}

NoRenameTransform.cs

using System;
using Microsoft.Ajax.Utilities;
using System.Web;
using System.Web.Optimization;

namespace FileUpload_MVC.Models
{
    public class NoRenameTransform : IBundleTransform
    {
        protected static Minifier compiler =  new Minifier();

        public void Process(BundleContext context, BundleResponse response)
        {
            context.UseServerCache = false;
            response.Cacheability = HttpCacheability.NoCache;

            var codeSettings = new CodeSettings
            {
                NoAutoRenameList = "$scope"
            };
            response.Content = compiler.MinifyJavaScript(response.Content, codeSettings);
        }
    }
}

This post is for the purpose of my notes only and sometimes a rant.
“I invented nothing new. I simply assembled the discoveries of other men behind whom were centuries of work. Had I worked fifty or ten or even five years before, I would have failed. So it is with every new thing. Progress happens when all the factors that make for it are ready and then it is inevitable. To teach that a comparatively few men are responsible for the greatest forward steps of mankind is the worst sort of nonsense.”
Henry Ford