天天看點

Bundle 小鎮中由 EasyUI 引發的“血案”

由于預設的 ASP.NET MVC 模闆使用了 Bundle 技術,大家開始接受并喜歡上這種技術。Bundle 技術通過 Micorosoft.AspNet.Web.Optimization 包實作,如果在 ASP.NET WebForm 項目中引入這個包及其依賴包,在 ASP.NET WebForm 項目中使用 Bundle 技術也非常容易。

關于在 WebForm 中使用 Bundle 技術的簡短說明

通過 NuGet 很容易在 WebForm 項目中引入

Microsoft.AspNet.Web.Optimization

 包及其依賴包。不過在 MVC 項目的 Razor 頁面中可以使用類似下面的語句引入資源
@Scripts.Render("...")      
而在 

*.aspx

 頁面中則需要通過 

<%= %>

 來引入了:
<%@ Import Namespace="System.Web.Optimization" %>
// ...
<%= Scripts.Render("...") %>      
備注 有些資料中是使用的 

<%: %>

,我實在沒有發現它和 

<%= %>

 有啥差別,但至少我在《ASP.NET Reference》的《Code Render Blocks》一節找到了 

<%= %>

,卻暫時沒在官方文檔裡找到 

<%: %>

然後,我在一個使用了 EasyUI 的項目中使用了 Bundle 技術。才開始一切正常,至到第一個 Release 版本測試的那一天,“血案”發生了——

由于一個腳本錯誤,EasyUI 沒有生效。最終原因是 Bunlde 在 Release 版中将 EasyUI 的腳本壓縮了——當然,定位到這個原因還是經曆了一翻周折,這就不細說了。

[方案一] 禁用代碼壓縮

這個解決方案理論上隻需要在配置裡加一句話就行:

BundleTable.EnableOptimizations = false;      

但問題在于,這樣一來,為了一個 EasyUI,就放棄了所有腳本的壓縮,而僅僅隻是合并,效果折半,隻能當作萬不得已的備選。

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

先看看原本的 Bundle 配置(已簡化)

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")