ASP.NET MVC Bundling and RequireJS

Keywords: ASP.NET JQuery Javascript Programming

Experts are quick to come around and help solve the puzzlement ~About the choice of ASP.NET MVC Bundling and RequireJS, I am confused recently. I hope there is a way to combine the advantages of both. As a. NET programmer, don't you have any confusion about this?

Because I feel that each has its own advantages and disadvantages, the disadvantage of RequireJS is that when developing, you can not introduce compressed js or css, otherwise you can not debug and modify, and Bundling's debug mode is not compressed by default, as soon as you release into production, it will automatically compress, debugging is very convenient. The advantage of RequireJS is that it can load asynchronously on demand, and modular js code. Bundling is a simple and rude combination of all into one file to load. You can't see that modular references can't load on demand. So how do you choose to be a. NET programmer in the development process? Can we combine the advantages of the two?

If you tell me you don't know that RequireJS is an amazing winter? Please move to: http://requirejs.org/docs/api.html

Mode 1 Bunding+RequireJS Mixed Use

Let's first look at a foreigner's approach, which he basically did:

Bundling section

App_Start/BundleConfig.cs:

bundles.Add(new ScriptBundle("~/bundles/test").Include(
                   "~/Scripts/jquery-{version}.js",
                   "~/Scripts/q.js",
                   "~/Scripts/globalize.js"));

RequireJS configuration section

In ASP.NET MVC projects, we usually add js references to the master page of _Layout

    <script src="~/Scripts/require.js"></script>
    @if (!HttpContext.Current.IsDebuggingEnabled)
    {
        <script>
            requirejs.config({
                bundles: {
                    '@Scripts.Url("~/bundles/test").ToString()': [
                        'jquery',
                        'globalize',
                        'q']
                }
            });
        </script>
    }

Comments: Very elegant way to achieve, say good modularity? And it does not provide a complete application solution.

The original address of the foreigner: ASP.NET MVC Bundling and Minification with RequireJS

Mode 2 RequireJS.NET

But then I found a plug-in. RequireJS.NET

What is RequireJS.NET?

RequireJS.NET allows every C# programmer to build JavaScript code without having advanced js programming skills to understand and use.

Advantages of using RequireJS in ASP.NET MVC:

  • Make JavaScript code more reusable
  • Reliable object and dependency management
  • Suitable for large and complex applications
  • Asynchronous loading of JavaScript files

Comments: Installing this installation and that, and more rigid, I can write code to achieve its functions, and more flexible, how to change it.

For the use of RequireJS.NET, please refer to: Getting started with RequireJS for ASP.NET MVC

My Way to Realize

Next, I will launch my implementation. My approach is to abandon the Bundling feature of ASP.NET MVC, because it is too foolish and rude, but you can integrate RequireJS and R.js in ASP.NET MVC project very friendly. Although RequireJS seems very convenient to use in single-page application scenarios, it is also applicable in application scenarios as long as you are willing to accept it in this way.

Use technology: using RequireJS and R.js

The catalogue structure is as follows:

Since there is template page _Layout.cshtml in ASP.NET MVC project, I can put some common calls directly into the template page. Here I encapsulate them by Html extension method.

Call of css:

     <link rel="stylesheet" href="@Html.StylesPath("main.css")" />

Call of js:

    <script src="@Url.Content("~/themes/default/content/js/require.js")"></script>
    <script>   @Html.ViewSpecificRequireJS()</script>
        @RenderSection("scripts", required: false)

RequireJsHelpers:

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

namespace Secom.Emx.WebApp
{
    public static class RequireJsHelpers
    {
        private static MvcHtmlString RequireJs(this HtmlHelper helper, string config, string module)
        {
            var require = new StringBuilder();
            string jsLocation = "/themes/default/content/release-js/";
#if DEBUG
            jsLocation = "/themes/default/content/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 + "\",\"domReady!\"] ); ");
                require.AppendLine("});");
            }

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

        public static MvcHtmlString ViewSpecificRequireJS(this HtmlHelper helper)
        {
            var areas = helper.ViewContext.RouteData.DataTokens["area"];
            var action = helper.ViewContext.RouteData.Values["action"];
            var controller = helper.ViewContext.RouteData.Values["controller"];

            string url = areas == null? string.Format("views/{0}/{1}", controller, action): string.Format("views/areas/{2}/{0}/{1}", controller, action, areas);

            return helper.RequireJs("config.js", url);
        }
        public static string StylesPath(this HtmlHelper helper, string pathWithoutStyles)
        {
#if (DEBUG)
            var stylesPath = "~/themes/default/content/css/";
#else
            var stylesPath =  "~/themes/default/content/release-css/";
#endif
            return VirtualPathUtility.ToAbsolute(stylesPath + pathWithoutStyles);
        }
    }
}

Let's look at our js main file config.js

requirejs.config({
    baseUrl: '/themes/default/content/js',
    paths: {
        "jquery": "jquery.min",
        "jqueryValidate": "lib/jquery.validate.min",
        "jqueryValidateUnobtrusive": "lib/jquery.validate.unobtrusive.min",
        "bootstrap": "lib/bootstrap.min",
        "moment": "lib/moment.min",
        "domReady": "lib/domReady",
    },
    shim: {
        'bootstrap': {
            deps: ['jquery'],
            exports: "jQuery.fn.popover"
        },
        "jqueryValidate": ["jquery"],
        "jqueryValidateUnobtrusive": ["jquery", "jqueryValidate"]
    }
});

In the development environment, our css files can't be compressed and merged, or can't be debugged, and the production environment must be compressed and merged, so I want to develop when not merged, as soon as released to production will automatically merge.

So there are two ways, one is to write a batch script separately, run it every time it is released to production, the other is to configure it directly in the project generation event, if debug mode, it will not be compressed and merged, if release mode, it will be compressed and merged.

if $(ConfigurationName) == Release node "$(ProjectDir)themes\default\content\build\r.js" -o "$(ProjectDir)themes\default\content\release-js\build-js.js"
if $(ConfigurationName) == Release node "$(ProjectDir)themes\default\content\build\r.js" -o "$(ProjectDir)themes\default\content\release-css\build-css.js"

Automatic construction

Batch automatic merge compression script build.bat:

@echo off
echo start build js
node.exe r.js -o build-js.js
echo end build js
echo start build css
node.exe r.js -o build-css.js
echo end build css
echo. & pause

Because my JS file corresponds to the view view view interface in the controller one by one, then I need a dynamic JS build script. Here I use a powerful T4 template to implement, and create a new text template build-js.tt. If your VS does not have the intelligent prompt of T4, you need to install a VS plug-in, open VS-Tool-Extension and Update:

The T4 template code is as follows:

<#@ 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-js',
    modules: [
    {
        name: "config",
        include: [
            // These JS files will be on EVERY page in the main.js file
            // So they should be the files we will almost always need everywhere
            "domReady",
            "jquery",
            "jqueryValidate",
            "jqueryValidateUnobtrusive",
            "bootstrap",
            "moment"
            ]
    },
    <# 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("/themes/default/content/js","/themes/default/content/release-js")
        }
        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("\\", "/");
} #>

The build scripts produced through T4 templates are as follows:

({
    appDir: '../js',
    baseUrl: './',
    mainConfigFile: '../js/config.js',
    dir: '../release-js',
    modules: [
    {
        name: "config",
        include: [
            // These JS files will be on EVERY page in the main.js file
            // So they should be the files we will almost always need everywhere
            "domReady",
            "jquery",
            "jqueryValidate",
            "jqueryValidateUnobtrusive",
            "bootstrap",
            "moment"
            ]
    },
    {
       name: 'views/areas/admin/default/index'
    },
    {
       name: 'views/home/index'
    },
    {
       name: 'views/home/login'
    },
    ],
    onBuildRead: function (moduleName, path, contents) {
        if (moduleName = "config") {
            return contents.replace("/themes/default/content/js","/themes/default/content/release-js")
        }
        return contents;
    },
})

Comments: Flexibility is good, how to organize it, and it can support daily build and continuous integration.

Sometimes, I always feel like a genius in a moment, O() Oha ~, please let me narcissism first, because programmers always have some ideas of their own, if you think my article is helpful or inspiring to you, please recommend it! If you have your own ideas, please come up with them. Let's discuss them together.

Postscript: I would like to add version number to the suffix of url path of each js and css to achieve caching, such as? v=1.0, but then I think that the browser itself has client cache, so I did not add it first, if necessary, you can fill it up at any time.

Posted by pornost4r on Wed, 09 Jan 2019 01:36:10 -0800