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 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 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 bundles = new List();
 
    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 allFiles = new List();
        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,不信就试试。


分享文章:Bundle小镇中由EasyUI引发的“血案”
文章链接:http://ybzwz.com/article/jjsjhg.html