【问题标题】:How do I use ASP.NET bundling and minification without recompiling?如何在不重新编译的情况下使用 ASP.NET 捆绑和缩小?
【发布时间】:2013-06-30 12:59:48
【问题描述】:

约束:我没有使用 MVC,只是在我的 Web 应用程序中使用常规的 .aspx 文件。也不使用母版页 - 每个页面都是不同的野兽,因此该解决方案不适合我。

我读过的大多数捆绑和缩小示例都需要一些特殊的 MVC 标记,或者要求您预先识别捆绑的脚本/样式表,然后参考这些捆绑包。我想避免每次在 .aspx 页面中添加或修改 .js 引用时重新编译 DLL。

阅读 Msft 文档让我有点难过。有没有一种方法(如 ASP.NET 控件)可以包装一系列 script 标签(或 CSS 的 link 标签)动态创建和使用捆绑包?我不想重新发明轮子,但认真考虑创建自己的用户控件/自定义控件来处理这个问题。还有其他选择吗?

例如,寻找这样的东西:

<asp:AdHocScriptBundle id="mypage_bundle" runat="server">
    <script type="text/javascript" src="~/scripts/mypage1.js"></script>
    <script type="text/javascript" src="~/scripts/mypage2.js"></script>
    <script type="text/javascript" src="~/scripts/mypage3.js"></script>
</asp:AdHocScriptBundle>

启用捆绑后,会自动将 asp:AdHocScriptBundle 的内容替换为类似于以下的单个 script 标记:

<script type="text/javascript" src="/webappname/bundles/mypage_bundle.js?v=dh120398dh1298dh192d8hd32d"></script>

当捆绑被禁用时,正常输出内容如下:

<script type="text/javascript" src="/webappname/scripts/mypage1.js"></script>
<script type="text/javascript" src="/webappname/scripts/mypage2.js"></script>
<script type="text/javascript" src="/webappname/scripts/mypage3.js"></script>

有什么想法吗?

无论如何我都会自己推出,但如果已经有解决方案,请分享,谢谢!

【问题讨论】:

标签: javascript asp.net bundling-and-minification


【解决方案1】:

我推出了自己的解决方案,效果很好!我创建了 4 个可以用作自定义服务器控件的类:

  • ScriptBundle
  • 脚本
  • 样式包
  • 链接

这些调用函数围绕着我的自定义捆绑库,它本身就是 System.Web.Optimization API 的包装器。

在渲染ScriptBundleStyleBundle 期间,然后我检查一个内部设置(与我在 System.Web.Optimization API 中设置EnableOptimizations 的设置相同),它告诉页面要么使用捆绑,要么只需写出正常的script / link 标签。如果启用了捆绑,它会从我的自定义捆绑库中调用此函数(对于脚本,样式的类似代码。Bundler 在下面的代码中是我的自定义捆绑库的类 - 以防万一微软更改 System.Web.Optimization API我想要一个中间层,这样我就不必更改我的代码):

    public static void AddScriptBundle(string virtualTargetPath, params string[] virtualSourcePaths)
    {
        var scriptBundle = new System.Web.Optimization.ScriptBundle(virtualTargetPath);
        scriptBundle.Include(virtualSourcePaths);
        System.Web.Optimization.BundleTable.Bundles.Add(scriptBundle);
    }

为确保仅在 Bundle 不存在时才创建它,我首先使用此方法检查 Bundle(在使用上述方法之前):

    public static bool BundleExists(string virtualTargetPath)
    {
        return System.Web.Optimization.BundleTable.Bundles.GetBundleFor(virtualTargetPath) != null;
    }

然后我使用这个函数通过 System.Web.Optimization 吐出捆绑包的 URL:

    public static System.Web.IHtmlString GetScriptBundleHTML(string virtualTargetPath)
    {
        return System.Web.Optimization.Scripts.Render(virtualTargetPath);
    }

在我的 .aspx 文件中,我这样做:

<%@ Register TagPrefix="cc1" Namespace="AdHocBundler" Assembly="AdHocBundler" %>

...

<cc1:ScriptBundle name="MyBundle" runat="Server">
    <cc1:script src='~/js/script1.js'/>
    <cc1:script src='~/js/utils/script2.js'/>
</cc1:ScriptBundle>

对我来说,诀窍是弄清楚我必须将 scriptlink 标记转换为 ScriptBundleStyleBundle 控件中的列表项,但之后它工作得很好,它让我使用波浪号运算符可轻松引用相对于应用根目录(使用Page.ResolveClientUrl(),这有助于创建模块内容)。

感谢这个 SO 答案帮助我弄清楚如何创建自定义集合控件:How do you build an ASP.NET custom control with a collection property?

更新:为了全面披露,我获得了分享 ScriptBundle 代码的许可(StyleBundle 几乎相同,所以没有包含它):

[DefaultProperty("Name")]
[ParseChildren(true, DefaultProperty = "Scripts")]
public class ScriptBundle : Control
{
    public ScriptBundle()
    {
        this.Enabled = true;
        this.Scripts = new List<Script>();
    }

    [PersistenceMode(PersistenceMode.Attribute)]
    public String Name { get; set; }

    [PersistenceMode(PersistenceMode.Attribute)]
    [DefaultValue(true)]
    public Boolean Enabled { get; set; }

    [PersistenceMode(PersistenceMode.InnerDefaultProperty)]
    public List<Script> Scripts { get; set; }

    protected override void Render(HtmlTextWriter writer)
    {
        if (String.IsNullOrEmpty(this.Name))
        {
            // Name is used to generate the bundle; tell dev if he forgot it
            throw new Exception("ScriptBundle Name is not defined.");
        }

        writer.BeginRender();

        if (this.Enabled && Bundler.EnableOptimizations)
        {
            if (this.Scripts.Count > 0)
            {
                string bundleName = String.Format("~/bundles{0}/{1}.js",
                    HttpContext.Current.Request.FilePath,
                    this.Name).ToLower();

                // create a bundle if not exists
                if (!Bundler.BundleExists(bundleName))
                {
                    string[] scriptPaths = new string[this.Scripts.Count];
                    int len = scriptPaths.Length;
                    for (int i = 0; i < len; i++)
                    {
                        if (!string.IsNullOrEmpty(this.Scripts[i].Src))
                        {
                            // no need for resolve client URL here - bundler already does it for us, so paths like "~/scripts" will already be expanded
                            scriptPaths[i] = this.Scripts[i].Src;
                        }
                    }
                    Bundler.AddScriptBundle(bundleName, scriptPaths);
                }

                // spit out a reference to bundle
                writer.Write(Bundler.GetScriptBundleHTML(bundleName));
            }
        }
        else
        {
            // do not use bundling. generate normal script tags for each Script
            foreach (Script s in this.Scripts)
            {
                if (!string.IsNullOrEmpty(s.Src))
                {
                    // render <script type='<type>' src='<src'>/> ... and resolve URL to expand tilde, which lets us use paths relative to app root
                    // calling writer.Write() directly since it has less overhead than using RenderBeginTag(), etc., assumption is no special/weird chars in the cc1:script attrs
                    writer.Write(String.Format(Script.TAG_FORMAT_DEFAULT,
                        s.Type,
                        Page.ResolveClientUrl(s.Src)));
                }
            }
        }
        writer.EndRender();
    }
}

public class Script
{
    public const String ATTR_TYPE_DEFAULT = "text/javascript";
    public const String TAG_FORMAT_DEFAULT = "<script type=\"{0}\" src=\"{1}\"></script>";

    public Script()
    {
        this.Type = ATTR_TYPE_DEFAULT;
        this.Src = null;
    }

    public String Type { get; set; }
    public String Src { get; set; }
    public String Language { get; set; }
}

【讨论】:

  • 听起来您是在将捆绑的脚本注入到页面中,而不是创建优化的外部链接。真的吗?如果是这种情况,您就达不到目的的一半,即能够缓存脚本以供重复使用。本质上,您正在取消您获得的任何好处,因为是的,您正在缩小您的脚本,但是现在每次加载引用捆绑包的页面时都必须下载整个内容,因此实际上您可能处于负面状态。
  • 这比根本不做任何捆绑更糟糕,因为正如@MystereMan 所说,您的脚本将作为每个请求的有效负载的一部分发送。捆绑通过 HTTP 304 响应提供缓存,因此不必每次都下载整个内容。您的页面性能现在变慢了,并且您的带宽成本比您最初开始的地方要高。
  • 我没有将捆绑的脚本注入到响应中。我使用System.Web.Optimization.Scripts.Render() 注入优化的外部链接(使用v=hash)。这是一个 COTS 应用程序,现在我们不必告诉客户让用户在升级后手动刷新缓存。我绝对愿意接受反馈,但 Fiddler 表明 (1) 捆绑脚本被缓存,(2) 缓存在 .js 内容更改时更新。如果你有时间试试。我知道这可能不是从头开始的最佳做法,但它帮助我将旧版网络应用程序转换为使用捆绑。
  • @nothingisnecessary - 看来你这里有一个潜在的问题。如果两个页面使用相同的包名称,但具有不同的脚本定义,并且两个用户同时使用这些页面,会发生什么情况?您需要使每个页面的包名称唯一,但这意味着您不再缓存通用脚本,除非基于每个页面。例如,将为每个页面下载 jQuery
  • .aspx 文件名包含在脚本src 路径中(通过HttpContext.Current.Request.FilePath),它涵盖了两个页面具有相同捆绑包名称的情况。 (如果一个页面有两个包,它们必须有不同的名称)。如果多个页面共享一个 .js 文件,我仍然会提前创建“共享”包(在 Application_start 中),但正在考虑允许覆盖基于文件名的行为,以使“共享”临时包更容易。 (对于预先创建的共享包,我把它放在 .aspx 中:&lt;%=Tools.Bundler.GetScriptBundleHTML("~/bundles/validation.js") %&gt;
【解决方案2】:

这对于 ASP.NET 中的默认捆绑/缩小是不可能的。捆绑的全部意义在于创建一个单一的文件以减少浏览器请求加载静态文件(如 .JS 和 .CSS 文件)的次数。

自己动手是您唯一的选择。但是,请注意每个&lt;script&gt; 行都会产生一个浏览器请求。由于most browsers can only handle 6 requests concurrently,您可以有等待时间来加载这些静态文件。

仅供参考,您不必每次使用内置捆绑更新您的 .JS 文件时都重新编译 DLL。您可以简单地重置应用程序正在运行的应用程序池。如果您使用外部会话持久性模型运行,您的用户不会注意到这种情况发生。

【讨论】:

  • 有可能,我做到了。见我上面的回答。我确实明白你所说的不需要重新编译的意思,我应该更具体一点——我之前必须这样做的原因是因为我选择在 application_start 期间创建“核心”包(每个页面上都使用脚本) Global.asax.cs,但我现在意识到这并不是绝对必要的 - 谢谢。
  • 是的,一个好处是克服了浏览器对并发请求的限制,但另一个主要好处是 System.Web.Optimization 根据文件内容创建哈希,浏览器使用它来了解文件是否已更改。如果浏览器看到不同的请求 URL,它将重新请求资源。否则浏览器通常会(取决于设置)检查缓存(和 TTL:服务器用来设置缓存的生存时间),如果没有找到缓存,或者 TTL 过期,则从服务器重新请求资源。如果服务器内容已更改,则服务器以新内容响应,否则发送 HTTP 304。
【解决方案3】:

你的问题是你没有真正考虑过这个问题。如果你是,你会意识到你的要求是行不通的。

为什么?因为脚本标签 ahs 会生成对 不同 url 的外部链接引用。因此,您在当前文件的标题中放置的任何内容都不会影响实际包含您的包的其他 URL。因此,无法在页面本身中动态更改捆绑包,因为根据定义,捆绑包必须在外部资源中定义。

现在,没有任何内容表明这些包必须在您自己的解决方案中编译成 DLL,但它们不能嵌入到当前正在呈现的页面中。

您可能想研究一些基于 javascript 的缩小工具,因为它们通常不会被编译。

【讨论】:

  • 我完全不同意。见我上面的回答。
  • 嗯,很多反对者,但不管他们是否相信我——我的解决方案运行良好,并且只需要对 .aspx 文件进行最少的代码更改。另外——因为这是一个数据量大的企业 Web 应用程序,所以我最不关心的就是缩小。我之前应该说过,但我的第一个问题是让用户的浏览器在 .js 文件的内容更改时自动更新缓存。 (我之前没有提到的另一个目标是尽可能简单地转换现有的&lt;script&gt; 标签,因为这个网络应用程序有数百个 .aspx 文件,我必须培训其他开发人员如何做到这一点。)
  • @nothingisnecessary - 换句话说,您不想实现任何网络优化目标......但坚持认为您实际上可以做一些您实际上并没有做的事情。我很高兴您的解决方案适合您,但这不是您想要的。
  • 不确定我是否关注。我得到(1)缩小(最初发送的数据更少),(2)自动缓存更新(由于文件内容的散列),以及(3)在缓存更新期间提高了性能(由于捆绑多个脚本文件以克服浏览器的限制对单个域的并发请求)。缩小对我来说是低优先级,因为许多页面都是数据密集型的,并且通常使用 AJAX 来请求块中的数据(.js 文件的大小与请求的 XML/Json 数据的大小成比例很小)。最高优先级是让缓存在 .js 文件更改时自动更新(例如升级后)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-02-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-28
相关资源
最近更新 更多