高手速來圍觀幫忙解惑~關於ASP.NET MVC Bundling and RequireJS的取舍問題,最近比較困惑,我希望有一種方式可以結合兩者的優點。作為.NET程序員,難道你沒有過這方面的困惑嗎?
因為我感覺各自都有優缺點,RequireJS的缺點在於,在開發的時候,你不能引入壓縮后的js或者css,否則無法調試和修改,而Bundling的話debug模式默認情況下是不壓縮,你一發布到生產成release就自動壓縮,調試起來非常方便。RequireJS的優點在於可以異步按需加載,還有就是模塊化js代碼,而Bundling 則是簡單粗暴的全部合並成一個文件進行加載,你看不出模塊化引用也實現不了按需加載, 那么在開發過程中作為.NET程序員是如何取舍的呢?能不能結合二者的優點來使用呢?
目標:在ASP.NET MVC項目中實現js和css的模塊化,並支持壓縮合並。
如果你跟我說你還不知道RequireJS是個神馬冬冬?請移步至:http://requirejs.org/docs/api.html
項目目錄結構沿用上一篇ASP.NET MVC 重寫RazorViewEngine實現多主題切換
方式一 Bunding+RequireJS混用
先來看看一個老外的做法,他大體上是這樣做的:
Bundling部分
App_Start/BundleConfig.cs:
bundles.Add(new ScriptBundle("~/bundles/test").Include( "~/Scripts/jquery-{version}.js", "~/Scripts/q.js", "~/Scripts/globalize.js"));
RequireJS配置部分
在ASP.NET MVC項目中,我們一般是在_Layout母版頁中添加js引用
<script src="~/Scripts/require.js"></script> @if (!HttpContext.Current.IsDebuggingEnabled) { <script> requirejs.config({ bundles: { '@Scripts.Url("~/bundles/test").ToString()': [ 'jquery', 'globalize', 'q'] } }); </script> }
個人點評:很不優雅的實現方式,說好的模塊化呢?而且並沒有提供完整的應用程序解決方案。
老外原文地址:ASP.NET MVC Bundling and Minification with RequireJS
方式二 RequireJS.NET
但是隨后我就發現了一個插件RequireJS.NET
什么是RequireJS.NET?
RequireJS.NET讓每一個C#程序員可以來構建JavaScript代碼,不需要具備高級的js編程技能就可以來理解和使用。
在ASP.NET MVC中使用RequireJS的優勢:
- 讓JavaScript代碼更加可復用
- 可靠的對象和依賴關系管理
- 適用於大型復雜的應用
- 異步加載JavaScript文件
個人點評:安裝這個安裝那個,而且比較死板,我完全可以自己寫代碼實現它的功能,而且更加靈活,想怎么改怎么改。
RequireJS.NET的使用請參考:Getting started with RequireJS for ASP.NET MVC
我的實現方式
接下來,我將隆重推出我的實現方式。我的做法是:拋棄ASP.NET MVC自帶的Bundling功能,因為它太傻瓜、太粗暴了,但是可以將RequireJS and R.js 很友好的集成在ASP.NET MVC項目中來。雖然RequireJS看上去在單頁應用的場景下用起來非常方便,但是在應用程序場景下也是同樣適用的,只要你願意接受它的這種方式。
使用技術: using RequireJS and R.js
目錄結構如下:
由於在ASP.NET MVC項目中,有模板頁_Layout.cshtml,那么我可以把一些公用調用的東西直接放到模板頁中,這里我通過Html的擴展方法進行了封裝
css的調用:
<link rel="stylesheet" href="@Html.StylesPath("main.css")" />
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); } } }
再來看下我們的js主文件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"] } });
在開發環境,我們的css文件肯定不能壓縮合並,不然無法調試了,而生產環境肯定是需要壓縮和合並的,那么我想要開發的時候不合並,一發布到生產就自動合並
那么有兩種方式,一種呢是單獨寫一個批處理腳本,每次發布到生產的時候就運行一下,一種呢是直接在項目的生成事件中進行配置,如果是debug模式就不壓縮合並,如果是release模式則壓縮合並
if $(ConfigurationName) == Release node "$(ProjectDir)themes\default\content\build\r.js" -o "$(ProjectDir)themes\default\content\build\build-js.js" if $(ConfigurationName) == Release node "$(ProjectDir)themes\default\content\build\r.js" -o "$(ProjectDir)themes\default\content\build\build-css.js"
自動構建
批處理自動合並壓縮腳本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
因為我的js文件是和控制器中的view視圖界面一一對應的,那么我需要一個動態的js構建腳本,這里我使用強大的T4模板來實現,新建一個文本模板build-js.tt,如果你的VS沒有T4的智能提示,你需要安裝一個VS插件,打開VS——工具——擴展和更新:
T4模板代碼如下:
<#@ 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("\\", "/"); } #>
通過T4模板生產的構建腳本如下:
({ 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; }, })
個人點評:靈活性很好,想怎么整怎么整,而且可以很好的支持每日構建和持續集成。
有時候,總是會在某一剎那間就有新奇的想法,這就是程序員,如果你覺得我的文章對你有幫助或者啟發,請推薦一下吧!如果有自己的想法也請提出來,大家一起探討!
后記:我本來還想給每js和css的url路徑后綴添加版本號來實現客戶端緩存的自動更新,如?v=1.0,但是后面想了下瀏覽器本身就自帶客戶端緩存,所以就先沒有添加,如果真有需要,可以隨時補上。
調整后的項目源碼:https://git.coding.net/zouyujie/Smp.git