在MVC 4中运行时动态打包和压缩

19
我想知道是否有人可以帮助我使用 MVC 4 中提供的新优化命名空间进行捆绑和压缩。 我有一个多租户应用程序,我想根据每个用户的设置决定要加载哪些 js 文件。一种方法是预先创建所有捆绑包,并根据用户设置更改 resolvebundleurl 的虚拟路径,但这并不是正确的方式。 同时我在 cshtml 视图中有基于用户设置的动态 css,希望在运行时对其进行压缩。
有什么建议吗?我也看到其他问题中有很多关于检查 Requestreduce 的反应,但它们都来自同一个用户。
处理这两种情况的最佳方法是什么?
谢谢!

没有人吗?当我在开发过程中更改我的Javascript或CSS时,缩小(捆绑)的文件会在不需要重新构建的情况下进行更新,因此必须在运行时完成... - Cyril Mestrom
2
问题标题应更改以强调动态捆绑包(或每个用户)。 - yzorg
4个回答

12

你可以采取的一种方法是,在应用程序启动时动态构建捆绑包。因此,如果你的脚本位于~/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")

注意:我正在使用最新的系统.web.optimization NuGet包(现在是Microsoft.AspNet.Web.Optimization),它位于此处。Scott Hanselman在他的博客文章中有很好的介绍。


你的帖子让我开始使用Visual Studio 2012 RC,我正在转换我的项目。现在有了bundleconfig文件,事情变得更加容易了。完成后,我会发布我的最终解决方案。 - Cyril Mestrom

9
我写了一个辅助函数来动态压缩我的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()}
}}

当然,我把大部分的脚本和样式表放在~/Scripts/.js、~/Content/.css中,并在App_Start中进行注册。

2
我喜欢这个解决方案,因为它允许您从cshtml文件动态创建捆绑包。它不假设您的cshtml文件是静态的,并且您预先知道要使用哪些捆绑包。 - Kess

5

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


我不确定我完全理解为什么这是个问题,因为当我在服务器上更改一个js文件并重新加载页面时,更改会出现在新的minified/bundled文件中。正如指出的那样,我现在已经提前创建了所有的bundles... - Cyril Mestrom
1
即使您提前在服务器上创建了所有的捆绑包,它们在 BundleTable.Bundles 集合中注册之前并不存在于服务器上。因此,在多服务器场景下,如果请求捆绑包的请求发送到尚未注册该捆绑包的另一台服务器,您将会收到 404 错误。 - Hao Kung
@HaoKung:这与任何 Http 处理程序(例如 asp.net mvc 的控制器)没有区别:当然,您需要能够根据 http 请求识别正确的 bundle。但是就像其他请求一样,在某些情况下,处理程序无法参数化,甚至可能依赖于会话状态。 - Eamon Nerbonne
有人想从另一个服务器包含一个捆绑包(考虑该捆绑包已在其他服务器上注册),该怎么办?像 <script src="@Url.Content("http://localhost/CardGame/bundles/jquery")" type="text/javascript"></script> 这样的方式对我来说不起作用。响应500。 - Kevkong

0

更新:不确定是否重要,但我正在使用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个请求进入捆绑代码。

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

当调用FinalizeBundles时,您可以指定True,这将强制进行优化,而不管web.config debug设置如何,或者将其保留为空或使用没有该参数的构造函数,以使其遵守web.config设置。传递false将强制它不使用优化,即使debug为true。FinalizeBundles在bundles表中注册bundles并将_BundlesFinalized设置为true。

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

此设置允许您添加新的bundles到view start并重置应用程序池以使其生效。我编写此内容的最初目标是因为我正在制作其他人将使用的东西,因此我希望他们能够完全更改前端UI,而无需重新构建源代码以更改bundles。


嗨Ryios,下面这行代码KBApplicationCore.PackageManifest.FolderName的目的是什么?这行代码会返回基础文件夹路径吗? - Ashish Shukla
啊,我误读了你的评论。那是我发布这里时忘记编辑我的项目中的代码。KBApplicationCore.PackageManifest.FolderName是PackageManifest所在文件夹的名称。因此,它被用作资产的基本路径。我设计的应用程序具有主题系统,在每个主题文件夹中都有一个Package.Manifest文件。主题文件夹用作所有资产的基本路径。如果您更改主题,它将是不同的,并且具有所有不同的资产。 - Ryan Mann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接