【问题标题】:Runtime dynamic bundling and minifying in MVC 4MVC 4 中的运行时动态捆绑和缩小
【发布时间】:2012-05-23 18:34:17
【问题描述】:

我想知道是否有人可以帮助我使用 MVC 4 附带的新优化命名空间进行捆绑和缩小。 我有一个多租户应用程序,我想根据每个用户的设置来决定应该加载哪些 js 文件。一种方法是预先创建所有捆绑包并根据用户的设置更改 resolvebundleurl 的虚拟路径,但这感觉不是正确的方法。 此外,我在基于用户设置的 cshtml 视图中有动态 css,我希望在运行时将其缩小。

有什么建议吗?我还在其他问题中看到了很多关于查看 Requestreduce 的反应,但它们都来自同一个用户。

处理这两种情况的最佳方法是什么?

提前致谢!

【问题讨论】:

  • 没人?当我在开发过程中更改我的 Javascript 或 css 时。缩小(捆绑)文件无需重建即可更新,因此必须在运行时完成....
  • 应更改问题标题以强调动态捆绑包(或每个用户)。

标签: asp.net asp.net-mvc-4 optimization bundle asp.net-optimization


【解决方案1】:

您可以采用的一种方法是在应用程序启动时动态构建捆绑包。因此,如果您的脚本位于~/scripts,您可以这样做:

Bundle bundle = new Bundle("~/scripts/js", new JsMinify());

if (includeJquery == true) {     
  bundle.IncludeDirectory("~/scripts", "jquery-*");
  bundle.IncludeDirectory("~/scripts", "jquery-ui*");
} 

if (includeAwesomenes == true) {
  bundle.IncludeDirectory("~/scripts", "awesomeness.js");
}

BundleTable.Bundles.Add(bundle);

那么你的标记可以是这样的

@Scripts.Render("~/Scripts/Libs/js")

注意:我正在使用位于 here 的 system.web.optimization(现为 Microsoft.AspNet.Web.Optimization)的最新 nuget 包。 Scott Hanselman 对此有很好的post

【讨论】:

  • 您的帖子让我升级到 Visual Studio 2012 RC,我正在转换我的项目。现在有了 bundleconfig 文件,思考变得容易多了。完成后我会发布我的最终解决方案。
【解决方案2】:

我们很早就考虑过支持动态捆绑包,但这种方法的根本问题是多服务器场景(即云)无法正常工作。如果未预先定义所有捆绑包,则任何发送到与提供页面请求的服务器不同的服务器的捆绑包请求都将获得 404 响应(因为捆绑包定义仅存在于处理页面请求的服务器上)。因此,我建议预先创建所有捆绑包,这是主线方案。捆绑包的动态配置也可以工作,但这不是完全支持的方案。

【讨论】:

  • 我不确定我是否完全理解为什么会出现问题,因为当我更改服务器上的 js 文件并重新加载页面时,更改位于新的缩小/捆绑文件中。正如所指出的,我现在已经预先创建了所有捆绑包......
  • 即使您预先在服务器上创建了所有捆绑包,但在您将它们注册到 BundleTable.Bundles 集合中之前,它们实际上并不存在于服务器上。因此,在多服务器场景中,如果对捆绑包的请求转到尚未注册捆绑包的其他服务器,您将收到 404。
  • @HaoKung:这与任何 Http 处理程序(例如 asp.net mvc 的控制器)没有什么不同:当然,您需要能够根据 http 请求识别正确的包。但就像其他请求一样,处理程序在某些情况下没有理由不能被参数化,甚至在必要时依赖于会话状态。
  • 某人如何包含来自另一台服务器的捆绑包(考虑到捆绑包已在另一台服务器上注册)<script src="@Url.Content("http://localhost/CardGame/bundles/jquery")" type="text/javascript"></script> 之类的东西对我不起作用。响应 500。
【解决方案3】:

我写了一个辅助函数来动态缩小我的 css & js

    public static IHtmlString RenderStyles(this HtmlHelper helper, params string[] additionalPaths)
    {
        var page = helper.ViewDataContainer as WebPageExecutingBase;
        if (page != null && page.VirtualPath.StartsWith("~/"))
        {
            var virtualPath = "~/bundles" + page.VirtualPath.Substring(1);
            if (BundleTable.Bundles.GetBundleFor(virtualPath) == null)
            {
                var defaultPath = page.VirtualPath + ".css";
                BundleTable.Bundles.Add(new StyleBundle(virtualPath).Include(defaultPath).Include(additionalPaths));
            }
            return MvcHtmlString.Create(@"<link href=""" + HttpUtility.HtmlAttributeEncode(BundleTable.Bundles.ResolveBundleUrl(virtualPath)) + @""" rel=""stylesheet""/>");
        }
        return MvcHtmlString.Empty;
    }

    public static IHtmlString RenderScripts(this HtmlHelper helper, params string[] additionalPaths)
    {
        var page = helper.ViewDataContainer as WebPageExecutingBase;
        if (page != null && page.VirtualPath.StartsWith("~/"))
        {
            var virtualPath = "~/bundles" + page.VirtualPath.Substring(1);
            if (BundleTable.Bundles.GetBundleFor(virtualPath) == null)
            {
                var defaultPath = page.VirtualPath + ".js";
                BundleTable.Bundles.Add(new ScriptBundle(virtualPath).Include(defaultPath).Include(additionalPaths));
            }
            return MvcHtmlString.Create(@"<script src=""" + HttpUtility.HtmlAttributeEncode(BundleTable.Bundles.ResolveBundleUrl(virtualPath)) + @"""></script>");
        }
        return MvcHtmlString.Empty;
    }

用法

~/views/Home/Test1.cshtml

~/Views/Home/Test1.cshtml.css

~/Views/Home/Test1.cshtml.js

在Test1.cshtml中

@model object
@{
   // init
}@{

}@section MainContent {
  {<div>@{
     if ("work" != "fun")
     {
        {<hr/>}
     }
  }</div>}
}@{

}@section Scripts {@{
  {@Html.RenderScripts()}
}@{

}@section Styles {@{
  {@Html.RenderStyles()}
}}

但是 ofcoz,我把我的大部分 sripts、样式放在 ~/Scripts/.js、~/Content/.css

并在 Appp_Start 中注册它们

【讨论】:

  • 我喜欢这个解决方案,因为它允许您从 cshtml 文件动态创建捆绑包。它不假定您的 cshtml 文件是静态的,并且您提前知道所需的包。
【解决方案4】:

更新:不确定是否重要,但我使用的是 MVC 5.2.3 和 Visual Studio 2015,问题有点老了。

但是,我制作了可以在 _viewStart.cshtml 中使用的动态捆绑。我所做的是创建了一个帮助类,将捆绑包存储在捆绑包字典中。然后在应用程序启动时,我从字典中提取它们并注册它们。我制作了一个静态布尔“bundlesInitialzed”,这样捆绑包只会添加到字典中一次。

示例助手:

public static class KBApplicationCore: .....
{
    private static Dictionary<string, Bundle> _bundleDictionary = new Dictionary<string, Bundle>();
    public static bool BundlesFinalized { get { return _BundlesFinalized; } }
    /// <summary>
    /// Add a bundle to the bundle dictionary
    /// </summary>
    /// <param name="bundle"></param>
    /// <returns></returns>
    public static bool RegisterBundle(Bundle bundle)
    {
        if (bundle == null)
            throw new ArgumentNullException("bundle");
        if (_BundlesFinalized)
            throw new InvalidOperationException("The bundles have been finalized and frozen, you can only finalize the bundles once as an app pool recycle is needed to change the bundles afterwards!");
        if (_bundleDictionary.ContainsKey(bundle.Path))
            return false;
        _bundleDictionary.Add(bundle.Path, bundle);
        return true;
    }
    /// <summary>
    /// Finalize the bundles, which commits them to the BundleTable.Bundles collection, respects the web.config's debug setting for optimizations
    /// </summary>
    public static void FinalizeBundles()
    {
        FinalizeBundles(null);
    }
    /// <summary>
    /// Finalize the bundles, which commits them to the BundleTable.Bundles collection
    /// </summary>
    /// <param name="forceMinimize">Null = Respect web.config debug setting, True force minification regardless of web.config, False force no minification regardless of web.config</param>
    public static void FinalizeBundles(bool? forceMinimize)
    {
        var bundles = BundleTable.Bundles;
        foreach (var bundle in _bundleDictionary.Values)
        {
            bundles.Add(bundle);
        }
        if (forceMinimize != null)
            BundleTable.EnableOptimizations = forceMinimize.Value;
        _BundlesFinalized = true;
    }        
}

示例_ViewStart.cshtml

@{

    var bundles = BundleTable.Bundles;
    var baseUrl = string.Concat("~/App_Plugins/", KBApplicationCore.PackageManifest.FolderName, "/");
    //Maybe there is a better way to do this, the goal is to make the bundle configurable without having to recompile the code
    if (!KBApplicationCore.BundlesFinalized)
    {
        //Note, you need to reset the application pool in order for any changes here to be reloaded as the BundlesFinalized property is a static field that will only reset to false when the app restarts.
        Bundle mainScripts = new ScriptBundle("~/bundles/scripts/main.js");
        mainScripts.Include(new string[] {
            baseUrl + "Assets/lib/jquery/jquery.js",
            baseUrl + "Assets/lib/jquery/plugins/jqcloud/jqcloud.js",
            baseUrl + "Assets/lib/bootstrap/js/bootstrap.js",            
            baseUrl + "Assets/lib/bootstrap/plugins/treeview/bootstrap-treeview.js",   
            baseUrl + "Assets/lib/angular/angular.js",
            baseUrl + "Assets/lib/ckEditor/ckEditor.js"      
        });
        KBApplicationCore.RegisterBundle(mainScripts);

        Bundle appScripts = new ScriptBundle("~/bundles/scripts/app.js");
        appScripts.Include(new string[] {
            baseUrl + "Assets/app/app.js",
            baseUrl + "Assets/app/services/*.js",
            baseUrl + "Assets/app/directives/*.js",
            baseUrl + "Assets/app/controllers/*.js"
        });
        KBApplicationCore.RegisterBundle(appScripts);

        Bundle mainStyles = new StyleBundle("~/bundles/styles/main.css");
        mainStyles.Include(new string[] {
           baseUrl + "Assets/lib/bootstrap/build/less/bootstrap.less",
           baseUrl + "Assets/lib/bootstrap/plugins/treeview/bootstrap-treeview.css",   
           baseUrl + "Assets/lib/ckeditor/contents.css",
           baseUrl + "Assets/lib/font-awesome/less/font-awesome.less",
           baseUrl + "Assets/styles/tlckb.less"
        });
        mainStyles.Transforms.Add(new BundleTransformer.Core.Transformers.CssTransformer());
        mainStyles.Transforms.Add(new CssMinify());
        mainStyles.Orderer = new BundleTransformer.Core.Orderers.NullOrderer();
        KBApplicationCore.RegisterBundle(mainStyles);


        KBApplicationCore.FinalizeBundles(true); //true = Force Optimizations, false = Force non Optmizations, null = respect web.config which is the same as calling the parameterless constructor.
    }
}

注意:这应该更新为使用线程锁定来防止 2 个请求在第一个退出之前进入捆绑代码。

这种工作方式是在应用程序池重置后对站点的第一个请求运行视图启动。它调用助手上的 RegisterBundle,并按照调用 RegisterBundles 的顺序将 ScriptBundle 或 StyleBundle 传递给字典。

当 FinalizeBundles 被调用时,您可以指定 True 将强制优化而不管 web.config 调试设置,或者将其保留为 null 或使用不带该参数的构造函数以使其尊重 web.config 设置。传递 false 将强制它不使用优化,即使 debug 为 true。 FinalizeBundles 在 bundles 表中注册 bundles 并将 _BundlesFinalized 设置为 true。

一旦完成,再次调用 RegisterBundle 的尝试将引发异常,此时它被冻结。

此设置允许您添加新捆绑包以查看启动并重置应用程序池以使其生效。我写这篇文章的最初目的是因为我正在制作其他人会使用的东西,所以我希望他们能够完全改变前端 UI,而无需重建源代码来更改包。

【讨论】:

  • 嗨 Ryios,下面行 KBApplicationCore.PackageManifest.FolderName 的目的是什么。这行代码是否会返回基本文件夹路径?
  • 啊,误读了你的评论。那是我在这里发布时忘记从项目中编辑出来的代码。 KBApplicationCore.PackageManifest.FolderName 是 PackageManifest 所在文件夹的名称。所以是的,它用作资产的基本路径。我设计的应用程序有一个主题系统,其中每个主题文件夹中都有一个 Package.Manifest 文件。 Theme 文件夹用作所有资产的基本路径。如果您更改主题,它会有所不同,并且拥有所有不同的资产。
猜你喜欢
  • 2013-02-21
  • 1970-01-01
  • 2014-11-26
  • 1970-01-01
  • 2018-05-11
  • 1970-01-01
  • 2012-10-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多