因爲默認的 ASP.NET MVC 模板使用了 Bundle 技術,你們開始接受並喜歡上這種技術。Bundle 技術經過 Micorosoft.AspNet.Web.Optimization 包實現,若是在 ASP.NET WebForm 項目中引入這個包及其依賴包,在 ASP.NET WebForm 項目中使用 Bundle 技術也很是容易。jquery
關於在 WebForm 中使用 Bundle 技術的簡短說明
c#經過 NuGet 很容易在 WebForm 項目中引入
Microsoft.AspNet.Web.Optimization
包及其依賴包。不過在 MVC 項目的 Razor 頁面中可使用相似下面的語句引入資源app@Scripts.Render("...")而在
*.aspx
頁面中則須要經過<%= %>
來引入了:ide<%@ Import Namespace="System.Web.Optimization" %> // ... <%= Scripts.Render("...") %>備註 有些資料中是使用的
<%: %>
,我實在沒有發現它和<%= %>
有啥區別,但至少我在《ASP.NET Reference》的《Code Render Blocks》一節找到了<%= %>
,卻暫時沒在官方文檔裏找到<%: %>
源碼分析
而後,我在一個使用了 EasyUI 的項目中使用了 Bundle 技術。纔開始一切正常,至到第一個 Release 版本測試的那一天,「血案」發生了——測試
因爲一個腳本錯誤,EasyUI 沒有生效。最終緣由是 Bunlde 在 Release 版中將 EasyUI 的腳本壓縮了——固然,定位到這個緣由仍是經歷了一翻周折,這就不細說了。ui
這個解決方案理論上只須要在配置里加一句話就行:
this
BundleTable.EnableOptimizations = false;
但問題在於,這樣一來,爲了一個 EasyUI,就放棄了全部腳本的壓縮,而僅僅只是合併,效果折半,只能看成萬不得已的備選。spa
先看看本來的 Bundle 配置(已簡化)
code
public static void Register(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/libs") .Include("~/scripts/jquery-{version}.js") .Include("~/scripts/jquery.eaysui-{versoin}.js") .Include("~/scripts/locale/easyui-lang-zh_CN.js") .IncludeDirectory("~/scripts/app", "*.js", true) ); }
這段配置先引入了 jquery,再引入了 easyui,最後引入了一些爲當前項目寫的公共腳本。爲了實現解決方案二,必需要改爲分三個 Bundle 引入,同時還得想辦法阻止壓縮其中一個 Bundle。
要分段,簡單
public static void Register(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/jquery") .Include("~/scripts/jquery-{version}.js") ); bundles.Add(new ScriptBundle("~/easyui") .Include("~/scripts/jquery.eaysui-{versoin}.js") .Include("~/scripts/locale/easyui-lang-zh_CN.js") ); bundles.Add(new ScriptBundle("~/libs") .IncludeDirectory("~/scripts/app", "*.js", true) ); }
但爲了阻止壓縮,查了文檔,也搜索了很多資料都沒找到解決辦法,因此只好看源碼分析了,請出 JetBrains dotPeek。分析代碼以後得出結論,只須要去掉默認的 Transform 就行
// bundles.Add(new ScriptBundle("~/easyui") // .Include("~/scripts/jquery.eaysui-{versoin}.js") // .Include("~/scripts/locale/easyui-lang-zh_CN.js") // ); Bundle easyuiBundle = new ScriptBundle("~/easyui") .Include("~/scripts/jquery.eaysui-{versoin}.js") .Include("~/scripts/locale/easyui-lang-zh_CN.js") ); easyuiBundle.Transforms.Clear(); bundles.Add(easyuiBundle);
關鍵代碼的分析說明
首先從 ScriptBunlde 入手
public class ScriptBundle: Bundle { public ScriptBundle(string virtualPath) : this(virtualPath, (string) null) {} public ScriptBundle(string virtualPath, string cdnPath) : base(virtualPath, cdnPath, (IBundleTransform) new JsMinify() ) { this.ConcatenationToken = ";" + Environment.NewLine; } }
能夠看出,ScriptBunlde 的構建最終是經過其基類 Bunlde 中帶 IBunldeTransform 參數的那一個來構造的。再看 Bunlde 的關鍵代碼
public class Bunlde public IList<IBundleTransform> Transforms { get { return this._transforms; } } public Bundle( string virtualPath, string cdnPath, params IBundleTransform[] transforms ) { // ... foreach(IBundleTransform bundleTransform in transforms) { this._transforms.Add(bundleTransform); } } }
容易理解,ScriptBunlde 構建的時候往 Transforms 中添加了一默認的 Transform——JsMinify,從名字就能夠看出來,這是用來壓縮腳本的。而 IBundleTransform 只有一個接口方法
public interface IBundleTransform { void Process(BundleContext context, BundleResponse response); }
看樣子它是在處理 BundleResponse。而 BundleResponse 中定義有文本類型的 Content 和 ContentType 屬性,以及一個 IEnumerable<BundleFile> Files。
爲何是 Files 而不是 File 呢,我猜 Content 中包含的是一個 Bundle 中全部文件的內容,而不是某一個文件的內容。要驗證也很容易,本身實現個 IBundleTransform 試下就好了
Bundle b = new ScriptBundle("~/test") .Include(...) .Include(...); b.Transforms.Clear();b.Transforms.Add(new MyTransform()) // MyTransform 能夠自由發揮,我其實啥都沒寫,只是在 Process 裏打了個斷點,檢查了 response 的屬性值而已
實驗證實在 BundleResponse 傳入 Transforms 以前,其 Content 就已經有全部引入文件的內容了。
方案二解決了方案一不能解決的問題,但同時也帶來了新問題。原來只須要一句話就能引入全部腳本
@Scripts.Render("~/libs")
而如今須要 3 句話
@Scripts.Render("~/jquery") @Scripts.Render("~/easyui") @Scripts.Render("~/libs")
鑑於方案二帶來的新問題,試想,若是有一個東西,能把 3 個 Bundle 對象組合起來,變成一個 Bundle 對象,豈不是就解決了?
因而,我發明了 Bundle 的 Bundle,不妨就叫 BundleBundle 吧。
public class BundleBundle : Bundle{ readonly List<Bundle> bundles = new List<Bundle>(); public BundleBundle(string virtualPath) : base(virtualPath) { } public BundleBundle Include(Bundle bundle) { bundles.Add(bundle); return this; } // 在引入 Bundle 對象時申明清空 Transforms,這幾乎就是爲 EasyUI 準備的 public BundleBundle Include(Bundle bundle, bool isClearTransform) { if (isClearTransform) { bundle.Transforms.Clear(); } bundles.Add(bundle); return this; } public override BundleResponse GenerateBundleResponse(BundleContext context) { List<BundleFile> allFiles = new List<BundleFile>(); StringBuilder content = new StringBuilder(); string contentType = null; foreach (Bundle b in bundles) { var r = b.GenerateBundleResponse(context); content.Append(r.Content); // 考慮到 BundleBundle 可能用於 CSS,因此這裏進行一次判斷, // 只在 ScriptBundle 後面加分號(兼容 ASI 風格腳本) // 這裏可能會出如今已有分號的代碼後面加分號的狀況, // 考慮到只會浪費 1 個字節,忍了 if (b is ScriptBundle) { content.Append(';'); } content.AppendLine(); allFiles.AddRange(r.Files); if (contentType == null) { contentType = r.ContentType; } } var response = new BundleResponse(content.ToString(), allFiles); response.ContentType = contentType; return response; } }
使用 BundleBundle 也簡單,就像這樣
bundles.Add(new BundleBundle("~/libs") .Include(new ScriptBundle("~/bundle/jquery") .Include("~/scripts/jquery-{version}.js") ) .Include( new ScriptBundle("~/bundle/easyui") .Include("~/scripts/jquery.easyui-{version}.js") .Include("~/scripts/locale/easyui-lang-zh_CN.js") ) .Include(new ScriptBundle("~/bundle/app") .IncludeDirectory("~/scripts/app", "*.js", true) ) );
而後
@Scripts.Render("~/libs")
注意,每一個子 Bundle 都有名字,但這些名字不能直接給 @Scripts.Render() 使用,由於它們並無直接加入 BundleTable.Bundles 中。但名字是必須的,並且不能是 null,不信就試試。