如何在不重新编译的情况下使用ASP.NET捆绑和缩小?

时间:2013-07-03 00:17:30

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

约束:我没有使用MVC,只是在我的网络应用中使用常规的'.aspx文件。不使用母版页 - 每个页面都是不同的野兽,所以这个解决方案不适合我。

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

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

例如,寻找类似的东西:

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

当禁用Bundling时,通常会输出内容:

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

有什么想法?

无论如何要自己动手,但如果已有解决方案请分享,谢谢!

3 个答案:

答案 0 :(得分:4)

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

  • ScriptBundle
  • 脚本
  • StyleBundle
  • 链接

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

在渲染ScriptBundleStyleBundle期间,我会检查一个内部设置(我在System.Web.Optimization API中用来设置EnableOptimizations的设置),它告诉页面要么使用捆绑,要么只是写出正常的script / link标记。如果启用了Bundling,它会从我的自定义捆绑库中调用此函数(对于Scripts,类似于Styles的代码。Bundler在下面的代码中是我的自定义捆绑库的类 - 以防Microsoft更改System.Web。优化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; }
}

答案 1 :(得分:1)

ASP.NET中的默认Bundling / Minification无法做到这一点。捆绑的整个要点是创建一个单一文件,以减少加载静态文件(如.JS和.CSS文件)的浏览器请求数。

滚动自己是你唯一的选择。但请注意,每个<script>行都会产生浏览器请求。从most browsers can only handle 6 requests concurrently开始,您可以等待加载这些静态文件。

而且仅供参考,每次使用内置捆绑更新.JS文件时,都不必重新编译DLL。您只需重置运行该应用程序的应用程序池即可。如果您使用外部会话持久性模型运行,则用户将不会注意到何时发生这种情况。

答案 2 :(得分:0)

你的问题在于你并没有真正想到这个问题。如果你是的话,你会意识到你要求的东西是行不通的。

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

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

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