【问题标题】:Declarative dynamic bundling/minification for MVC?MVC 的声明性动态捆绑/缩小?
【发布时间】:2013-12-03 09:17:52
【问题描述】:

是否有库存或可插入方式(如 NuGet 包)让我在我使用它们的 MVC 视图和部分中声明 .js.css 和理想情况下的 .less 文件,并让它们自动运行在生产中捆绑和缩小? (又名“Autobunding”)

我已经尝试了内置的 MVC 4 捆绑。我不喜欢在 BundleConfig.cs 中定义的捆绑包远离页面作者期望找到它们的位置。这对于非 C# 团队成员是行不通的。

作为我正在寻找的示例,这是我自己使用SquishIt 拼凑而成的。

ExtendedViewPage.cs

/// <summary>
/// Caches a bundle of .js and/or .css specific to this ViewPage, at a path similar to:
/// shared_signinpartial_F3BD3CCE1DFCEA70F5524C57164EB48E.js
/// </summary>
public abstract class ExtendedViewPage<TModel> : WebViewPage<TModel> {
    // This is where I keep my assets, and since I don't actually store any in my root,
    // I emit all my bundles here. I also use the the web deployment engine,
    // and remove extra files on publish, so I never personally have to clean them up,
    // and I also don't have to hand-identify generated bundles from original code.
    // However, to keep from needing to give the app write permissions
    // on a static content folder, or collocate bundles with original assets,
    // or conform to a specific asset path, this should surely be configurable
    private const string ASSET_PATH = "~/assets/";

    /// <summary>
    /// Emits here the bundled resources declared with "AddResources" on all child controls
    /// </summary>
    public MvcHtmlString ResourceLinks {
        get {
            return MvcHtmlString.Create(
                string.Join("", CssResourceLinks) + string.Join("", JsResourceLinks));
        }
    }

    // This allows all resources to be specified in a single command,
    // which permits .css and .js resources to be declared in an
    // interwoven manner, in any order the site author prefers
    // For me, this makes it clearer, to group my related .css and .js links,
    // and to place my often control-specific CSS near last in the list
    /// <summary>
    /// Queues compressible resources to be emitted with the ResourceLinks directive
    /// </summary>
    /// <param name="resourceFiles">Project paths to JavaScript and/or CSS files</param>
    public void AddResources(params string[] resourceFiles) {
        var css = FilterFileExtension(resourceFiles, ".css");
        AddCssResources(css);
        var js = FilterFileExtension(resourceFiles, ".js");
        AddJsResources(js);
    }

    /// <summary>
    /// Bundles JavaScript files to be emitted with the ResourceLinks directive
    /// </summary>
    /// <param name="resourceFiles">Zero or more project paths to JavaScript files</param>
    public void AddJsResources(params string[] resourceFiles) {
        if (resourceFiles.Any()) {
            JavaScriptBundle jsBundle = Bundle.JavaScript();
            foreach (string jsFile in resourceFiles) {
                jsBundle.Add(jsFile);
            }
            // Pages render from the inside-out, which is required for us to expose
            // our resources declared in children to the parent where they are emitted
            // however, it also means our resources naturally collect here in an order
            // that is probably not what the site author intends.
            // We reverse the order with insert
            JsResourceLinks.Insert(0, jsBundle.MvcRender(ASSET_PATH + ViewIdentifier + "_#.js"));
        }
    }

    /// <summary>
    /// Bundles CSS files to be emitted with the ResourceLinks directive
    /// </summary>
    /// <param name="resourceFiles">Zero or more project paths to CSS files</param>
    public void AddCssResources(params string[] resourceFiles) {
        // Create a separate reference for each CSS path, since CSS files typically include path-relative images.
        foreach (
            var cssFolder in resourceFiles.
                GroupBy(r => r.Substring(0, r.LastIndexOf('/')).ToLowerInvariant()).
                // Note the CssResourceLinks.Insert command below reverses not only desirably
                // the order of view emission, but also undesirably reverses the order of resources within this one view.
                // for this page we'll 'pre-reverse' them. There's probably a clearer way to address this.
                Reverse()) {
            CSSBundle cssBundle = Bundle.Css();
            foreach (string cssFile in cssFolder) {
                cssBundle.Add(cssFile);
            }
            // See JsResourceLinks.Insert comment above
            CssResourceLinks.Insert(0, cssBundle.MvcRender(cssFolder.Key + "/" + ViewIdentifier + "_#.css"));
        }
    }

    #region private implementation
    private string _viewIdentifier = null;
    // ViewIdentifier returns a site-unique name for the current control, such as "shared_signinpartial"
    // Some security wonks may take issue with exposing folder structure here
    // It may be appropriate to obfuscate it with a checksum
    private string ViewIdentifier {
        get {
            if (_viewIdentifier == null) {
                _viewIdentifier =
                    // VirtualPath uniquely identifies the currently rendering View or Partial,
                    // such as "~/Views/Shared/SignInPartial.cshtml"
                    Path.GetFileNameWithoutExtension(VirtualPath).
                    // This "Substring" truncates the ~/Views/ or ~/Areas/ in my build, in others
                    // but it is probably inappropriate to make this assumption.
                    // It is certainly possible to have views in the root.
                    // Substring(8).
                    // It's assumed all of these bundles will be output to a single folder,
                    // to keep filesystem write-access minimal, so we flatten them here.
                    Replace("/", "_").
                    // The following assumes a typical MS filesystem, preserve-but-ignore case.
                    // The .NET string recommendations suggest instead using ToUpperInvariant
                    // for such an operation, but this was just a personal preference.
                    // My IIS rules typically drop the case on all content served.
                    // It may be altogether inappropriate to alter,
                    // although appending the MD5 hash ensure it does no harm on other platforms,
                    // while still collapsing the cases where multiply-cased aliases are used
                    ToLowerInvariant();
            }
            return _viewIdentifier;
        }
    }

    private List<MvcHtmlString> CssResourceLinks {
        get { return getContextHtmlStringList("SquishItCssResourceLinks"); }
    }

    private List<MvcHtmlString> JsResourceLinks {
        get { return getContextHtmlStringList("SquishItJsResourceLinks"); }
    }

    // Note that at the resource render, if no bundles of a specific type (.css or .js)
    // have been provided, this performs the unnecessary operation of instanciating a new List<MvcHtmlString>
    // and adding it to the HttpContext.Items. This get/set could benefit from some clarification.
    private List<MvcHtmlString> getContextHtmlStringList(string itemName) {
        IDictionary contextItems = Context.ApplicationInstance.Context.Items;
        List<MvcHtmlString> resourceLinks;
        if (contextItems.Contains(itemName)) {
            resourceLinks = contextItems[itemName] as List<MvcHtmlString>;
        }
        else {
            resourceLinks = new List<MvcHtmlString>();
            contextItems.Add(itemName, resourceLinks);
        }
        return resourceLinks;
    }

    private string[] FilterFileExtension(string[] filenames, string mustEndWith) {
        IEnumerable<string> filtered =
            filenames.Where(r => r.EndsWith(mustEndWith, StringComparison.OrdinalIgnoreCase));
        return filtered.ToArray();
    }
    #endregion private implementation
}

PageWithHeaderLayout.cshtml(示例用法)

@{
    AddResources(
        Links.Assets.Common.Script.GoogleAnalytics_js,
        Links.Assets.Common.Style.ProprietaryTheme.jquery_ui_1_8_23_custom_css,
        Links.Assets.Common.Style.SiteStandards_css,
        Links.Assets.Common.CdnMirror.jquery._1_7_2.jquery_js,
        Links.Assets.Common.CdnMirror.jQuery_Validate._2_0_0pre.jquery_validate_120826_js,
        Links.Assets.Common.CdnMirror.jqueryui._1_8_23.jquery_ui_min_js,
        Links.Assets.Common.JqueryPlugins.templates.jquery_tmpl_min_js,
        Links.Assets.Common.JqueryPlugins.jquery_ajaxmanager_js,
        Links.Assets.Common.JqueryPlugins.hashchange.jquery_ba_hashchange_min_js
        );
}

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>@ViewBag.Title</title>
    <meta name="description" content="@ViewBag.Description" />
    <meta name="keywords" content="@ViewBag.Keywords" />
    <link rel="shortcut icon" href="@Url.Content("~/favicon.ico")" type="image/x-icon" />
    <!-- all bundles from all page components are emitted here -->
    @ResourceLinks
</head>
<body>
    @Html.Partial(MVC.Common.Views.ContextNavigationTree)
    <div id="pageContent">
        @RenderBody()
    </div>
</body>
</html>

不幸的是我写了它,所以它有很多限制。脚本不会重复,它采用简单的方法来描述捆绑包,我最近添加了一个丑陋的 hack 以允许 .less 支持等。

是否有任何现有的解决方案可以做到这一点?

【问题讨论】:

    标签: asp.net-mvc bundling-and-minification squishit


    【解决方案1】:

    这是一条评论,但空间不足。

    这很简洁,但看起来您最终每个(完整的、渲染的)页面都有一个捆绑包,对于首次访问该网站的访问者来说,这几乎是最糟糕的情况。如果您有多个页面使用相同的母版页并且不添加任何其他内容,那么您最终会在每个页面上下载相同的大文件,但名称不同。不要将名称基于页面名称,而是尝试连接所有文件名(按顺序)并计算 MD5 散列以用作您的包名称 - 这是一个相当好的唯一性检查,并且应该真正减少您的带宽使用.您可以在 SquishIt 中看到一个示例 here 说明我们如何执行此操作 - 请记住,您计算的值将是代码中此时作为“键”输入的内容。我会考虑的另一件事是为每个物理视图文件而不是为整个页面定义捆绑包,以最大限度地提高可重用性。

    我知道这听起来很重要,但我确实喜欢您的总体方向。我只是不确定确切的目的地是什么。如果您需要任何帮助,我会尝试密切关注此标签,并且很容易在其他地方找到。

    就“自动捆绑”而言,我认为没有任何东西可以满足您的需求 - 很大程度上是因为它需要如此细致入微的方法。你可以看看RequestReduce——它在没有干预的情况下为你做了很多优化,但我不相信它结合了资产。

    【讨论】:

    • 实际上捆绑包是按组件生成的。如果页面上没有唯一的 javascript 或 css(大多数公共内容都会出现这种情况),则不会生成捆绑包。不过,我知道我的代码有缺点,这就是为什么我要寻找其他人编写的代码:)。
    • 也就是说,如果什么都不存在,那么我愿意花一点时间将这个想法推向社区。​​span>
    • 大声笑,我什至没有意识到是你亚历克斯。 :) 感谢您几个月前的帮助。当你出于好奇问我为什么要对某些东西进行空检查时,我把这段代码发给了你。
    • 另外,请注意,捆绑 ID 确实使用了您所建议的 MD5 哈希,因为它来自 SquishIt。 :)
    • 哦,简直不敢相信我忘记了 :) 我认为虚拟路径会将路径返回到页面而不是控件。有什么理由将资源全部添加到一起而不是在单独的脚本和样式存储桶中?将使预处理更容易。当您希望脚本稍后在页面中呈现时,单独公开可能会有所帮助。实际上,我通常最终会为需要放在头脑中的脚本维护三分之一。在 squishit.MVC 中包含这种具有等效控件的东西真的很酷。需要一种方法来控制渲染策略(在内存与文件等中)。
    【解决方案2】:

    请查看Enfold project。这可能是您正在寻找的解决方案。

    假设你在 web 项目中有以下视图:

    ~/Views/Home/About.cshtml
    ~/Views/Home/Contact.cshtml
    ~/Views/Home/Index.cshtml

    您可以这样组织您的 Javascript 文件:

    ~/Scripts/Views/default.js
    ~/Scripts/Views/Home/default.js
    ~/Scripts/Views/Home/about.js
    ~/Scripts/Views/Home/contact.js
    ~/Scripts/Views/Home/index.js

    通过这样的设置,将创建以下捆绑包:

    ~/bundles/home/about

    ~/scripts/views/default.js
    ~/scripts/views/home/default.js
    ~/scripts/views/home/about.js

    ~/bundles/home/contact

    ~/scripts/views/default.js
    ~/scripts/views/home/default.js
    ~/scripts/views/home/contact.js

    ~/bundles/home/index

    ~/scripts/views/default.js
    ~/scripts/views/home/default.js
    ~/scripts/views/home/index.js

    【讨论】:

    • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会失效。
    • 已修复,有帮助吗?
    猜你喜欢
    • 1970-01-01
    • 2014-11-26
    • 2012-05-23
    • 2018-05-11
    • 1970-01-01
    • 2013-02-21
    • 2014-05-22
    • 1970-01-01
    • 2019-03-05
    相关资源
    最近更新 更多