Bundle 小鎮中由 EasyUI 引起的「血案」

因爲默認的 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


[方案二] 分段引入並阻止壓縮 EasyUI 的 Bundle

先看看本來的 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")


[方案三] Bundle 的 Bundle

鑑於方案二帶來的新問題,試想,若是有一個東西,能把 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,不信就試試。

相關文章
相關標籤/搜索