Динамическое связывание и минимизация во время выполнения в MVC 4

Мне было интересно, может ли кто-нибудь помочь мне с объединением и минимизацией с использованием нового пространства имен оптимизации, поставляемого с MVC 4. У меня есть приложение Multitenant, в котором я хочу решить, какие файлы js следует загружать на основе настроек для каждого пользователя. Один из подходов состоит в том, чтобы заранее создать все пакеты и изменить виртуальный путь к resolvebundleurl в зависимости от настроек пользователя, но это не совсем правильный путь. Также у меня есть динамический css в представлении cshtml на основе пользовательских настроек, которые я хотел бы минимизировать во время выполнения.

Какие-либо предложения? Я также вижу много ответов на другие вопросы, чтобы проверить Requestreduce, но все они от одного и того же пользователя.

Как лучше всего справиться с обеими ситуациями?

Заранее спасибо!


person Cyril Mestrom    schedule 16.05.2012    source источник
comment
Никто? Когда я меняю свой Javascript или CSS во время разработки. Минимизированные (связанные) файлы обновляются без перестроения, поэтому это необходимо делать во время выполнения....   -  person Cyril Mestrom    schedule 31.05.2012
comment
Название вопроса следует изменить, чтобы подчеркнуть динамические пакеты (или для каждого пользователя).   -  person yzorg    schedule 28.11.2012


Ответы (4)


Один из подходов, который вы можете использовать, — это динамическое создание пакета при запуске приложения. Итак, если ваши скрипты расположены в ~/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")

Примечание. Я использую последний пакет nuget для system.web.optimization (теперь Microsoft.AspNet.Web.Optimization), расположенный здесь. У Скотта Хансельмана есть хорошая публикация об этом.

person JaySilk84    schedule 02.06.2012
comment
Ваш пост заставил меня перейти на Visual Studio 2012 RC, и я конвертирую свой проект, пока мы говорим. Думать стало намного проще благодаря файлу bundleconfig. Я опубликую свое окончательное решение, когда оно будет готово. - person Cyril Mestrom; 06.06.2012

я написал вспомогательную функцию для динамической минимизации моих 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

~/Вид/Главная/Test1.cshtml.css

~/Вид/Главная/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

и зарегистрировать их в Appp_Start

person Community    schedule 26.07.2014
comment
Мне нравится это решение, потому что оно позволяет динамически создавать пакеты из файла cshtml. Это не предполагает, что ваши файлы cshtml являются статическими и что вы заранее знаете, какие пакеты вам нужны. - person Kess; 15.07.2015

Мы изначально рассматривали возможность поддержки динамических пакетов, но основная проблема такого подхода заключается в том, что многосерверные сценарии (т. е. облако) не будут работать. Если все пакеты не определены заранее, любые запросы пакетов, отправленные на сервер, отличный от того, который обслуживал запрос страницы, получат ответ 404 (поскольку определение пакета будет существовать только на сервере, обрабатывающем запрос страницы). В результате я бы предложил создать все пакеты заранее, это основной сценарий. Динамическая конфигурация пакетов также может работать, но это не полностью поддерживаемый сценарий.

person Hao Kung    schedule 15.06.2012
comment
Я не уверен, что полностью понимаю, почему это проблема, потому что, когда я меняю js-файл на сервере и перезагружаю страницу, изменение происходит в новом минимизированном/связанном файле. Теперь я, как указано, создал все пакеты заранее... - person Cyril Mestrom; 25.06.2012
comment
Даже если вы заранее создадите все пакеты на сервере, они не будут существовать на сервере до тех пор, пока вы не зарегистрируете их в коллекции BundleTable.Bundles. В результате в сценарии с несколькими серверами, если запрос на пакет отправляется на другой сервер, который еще не зарегистрировал пакет, вы получите 404. - person Hao Kung; 29.06.2012
comment
@HaoKung: это ничем не отличается от любого обработчика Http, такого как контроллеры asp.net mvc: конечно, вы должны иметь возможность определить правильный пакет на основе http-запроса. Но, как и в случае с другими запросами, нет никаких причин, по которым обработчик нельзя было бы в некоторых случаях параметризовать или даже при необходимости зависеть от состояния сеанса. - person Eamon Nerbonne; 22.08.2012
comment
Как бы кто-то включил бандл с другого сервера (считай, что бандл зарегистрирован на другом сервере) что-то вроде <script src="@Url.Content("http://localhost/CardGame/bundles/jquery")" type="text/javascript"></script> у меня не сработало. Ответ 500. - person Kevkong; 23.10.2012

Обновление: не уверен, что это имеет значение, но я использую 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.
    }
}

Примечание. Это следует обновить, чтобы использовать блокировку потока, чтобы предотвратить ввод кода пакета двумя запросами до выхода первого.

Это работает так: запуск представления выполняется при первом запросе к сайту после сброса пула приложений. Он вызывает RegisterBundle в помощнике и передает ScriptBundle или StyleBundle в словарь в порядке вызова RegisterBundle.

Когда вызывается FinalizeBundles, вы можете указать True, что приведет к принудительной оптимизации независимо от настройки отладки web.config, или оставить его нулевым, или использовать конструктор без этого параметра, чтобы он учитывал настройку web.config. Передача false заставит его не использовать оптимизацию, даже если для отладки установлено значение true. FinalizeBundles Регистрирует пакеты в таблице пакетов и устанавливает для параметра _BundlesFinalized значение true.

После завершения попытка снова вызвать RegisterBundle вызовет исключение, в этот момент оно зависает.

Эта настройка позволяет добавлять новые пакеты для просмотра запуска и сбрасывать пул приложений, чтобы они вступили в силу. Первоначальная цель, которую я ставил перед собой, заключалась в том, что я делаю что-то, что будут использовать другие, поэтому я хотел, чтобы они могли полностью изменить интерфейс пользователя без необходимости перестраивать исходный код для изменения пакетов.

person Ryan Mann    schedule 27.08.2015
comment
Привет, Риос. Какова цель строки ниже KBApplicationCore.PackageManifest.FolderName. Вернет ли эта строка кода путь к базовой папке? - person Ashish Shukla; 19.01.2016
comment
А, неправильно прочитал ваш комментарий. Это был код, который я забыл отредактировать из своего проекта, когда публиковал здесь. KBApplicationCore.PackageManifest.FolderName — это имя папки, в которой находится PackageManifest. Так что да, оно используется в качестве базового пути для ресурсов. Приложение, которое я разработал, имеет систему тем, в которой есть файл Package.Manifest в каждой папке темы. Папка Theme используется в качестве базового пути для всех ресурсов. Если вы измените тему, она будет другой и будет иметь разные активы. - person Ryan Mann; 19.01.2016