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 IListTransforms { 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 Listbundles = 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