因为默认的 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,不信就试试。