Как использовать объединение и минификацию ASP.NET без перекомпиляции?

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

Большинство примеров, которые я читал для объединения и минификации, требуют либо специальной разметки MVC, либо требуют, чтобы вы заранее идентифицировали связанные скрипты / таблицы стилей, а затем ссылались на эти пакеты. Я хочу избежать повторной компиляции библиотек DLL каждый раз, когда я добавляю или изменяю ссылку .js на странице .aspx.

Я немного озадачен чтением документов 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>

И когда объединение отключено, обычно выводит содержимое следующим образом:

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

Есть предположения?

В любом случае собираюсь свернуть свой собственный, но если для этого уже есть решение, поделитесь, спасибо!


person nothingisnecessary    schedule 03.07.2013    source источник
comment
Аналогичная проблема, но с другим (не веб-контролем) решением: stackoverflow.com/questions/13124218/   -  person nothingisnecessary    schedule 08.07.2013


Ответы (3)


Я накатил собственное решение, и оно отлично работает! Я создал 4 класса, которые могу использовать в качестве настраиваемых серверных элементов управления:

  • ScriptBundle
  • Сценарий
  • Стиль: Бандл
  • Ссылка на сайт

Эти функции вызывают мою настраиваемую библиотеку комплектации, которая сама является оболочкой для API System.Web.Optimization.

Во время рендеринга ScriptBundle и StyleBundle я затем проверяю внутреннюю настройку (ту же, что я использую для установки EnableOptimizations в System.Web.Optimization API), которая сообщает странице либо использовать объединение, либо просто записывать обычные теги script / link . Если объединение включено, он вызывает эту функцию из моей пользовательской библиотеки объединения (для скриптов аналогичный код для стилей tho. Bundler в приведенном ниже коде является классом для моей пользовательской библиотеки объединения - на всякий случай, если Microsoft изменит API-интерфейс System.Web.Optimization, который я хотел промежуточный слой, чтобы мне не пришлось так сильно менять свой код):

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

Затем я использую эту функцию, чтобы выдать URL-адрес пакета с помощью System.Web.Optimization:

    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>

Уловка для меня заключалась в том, чтобы выяснить, что мне нужно преобразовать теги script и link, чтобы они работали как элементы списка в элементах управления ScriptBundle и StyleBundle, но после этого он отлично работает И это позволило мне использовать оператор тильды для простых ссылок относительно корня приложения. (с использованием Page.ResolveClientUrl(), который полезен для создания содержимого модуля).

Спасибо этому SO-ответу за то, что помог мне понять, как создать настраиваемый элемент управления коллекцией: Как создать настраиваемый элемент управления ASP.NET со свойством коллекции?

ОБНОВЛЕНИЕ: в интересах полного раскрытия информации я получил разрешение поделиться кодом для 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; }
}
person nothingisnecessary    schedule 17.07.2013
comment
Похоже, вы вставляете свой связанный скрипт на страницу, а не создаете оптимизированную внешнюю ссылку. Это правда? Если это так, вы теряете половину цели, которая состоит в том, чтобы иметь возможность кэшировать скрипт для повторного использования ... По сути, вы отменяете любую выгоду, которую получаете, потому что да, вы минимизируете свой скрипт, но теперь вам нужно загружать все это целиком каждый раз, когда вы загружаете страницу, которая ссылается на пакет, так что на самом деле вы, вероятно, получаете чистый отрицательный результат. - person Erik Funkenbusch; 17.07.2013
comment
Это хуже, чем вообще не создавать пакетов, потому что, как сказал @MystereMan, ваши скрипты отправляются как часть полезной нагрузки при каждом запросе. Объединение обеспечивает кэширование через ответы HTTP 304, поэтому не нужно загружать все данные каждый раз. Производительность вашей страницы теперь ниже, а затраты на пропускную способность выше, чем вы изначально. - person Tim P.; 17.07.2013
comment
Я не вставляю в ответ связанный скрипт. Я использую System.Web.Optimization.Scripts.Render() для вставки оптимизированной внешней ссылки (с v=hash). Это приложение COTS, и теперь нам не нужно указывать клиентам, чтобы пользователи вручную обновляли кеш после обновлений. Я определенно открыт для обратной связи, но Fiddler показывает, что (1) связанный сценарий кэшируется и (2) кеш обновляется при изменении содержимого .js. Попробуйте, если у вас есть время ... Я понимаю, что это может быть не лучшей практикой при запуске с нуля, но это помогло мне преобразовать устаревшее веб-приложение для использования объединения. - person nothingisnecessary; 17.07.2013
comment
@nothingisneeded - Похоже, у вас здесь потенциальная проблема. Что произойдет, если две страницы используют одно и то же имя пакета, но имеют разные определения сценария, и два пользователя используют эти страницы одновременно? Вам нужно сделать имена пакетов уникальными для каждой страницы, но тогда это означает, что вы больше не кэшируете общие сценарии, кроме как для каждой страницы. jQuery будет загружен для каждой страницы, например - person Erik Funkenbusch; 17.07.2013
comment
Имя файла .aspx включено в src путь сценария (через HttpContext.Current.Request.FilePath), который охватывает случай, когда две страницы имеют одинаковое имя пакета. (Если на одной странице есть два пакета, у них должны быть разные имена). Если несколько страниц используют один файл .js, я все равно создаю «общие» пакеты заранее (в Application_start), но рассматривал возможность переопределения поведения на основе имени файла, чтобы упростить использование «общих» специальных пакетов. (Для общих пакетов, которые были созданы заранее, я помещаю это в .aspx: <%=Tools.Bundler.GetScriptBundleHTML("~/bundles/validation.js") %>) - person nothingisnecessary; 17.07.2013

Это невозможно с объединением / минификацией по умолчанию в ASP.NET. Вся суть объединения заключается в создании одного файла для уменьшения количества запросов браузера на загрузку статических файлов, таких как файлы .JS и .CSS.

Сворачивать самостоятельно - ваш единственный выход. Однако учтите, что каждая строка <script> приведет к запросу браузера. Поскольку большинство браузеров могут одновременно обрабатывать только 6 запросов, у вас может быть время ожидания только для загрузки этих статических файлы.

И, к вашему сведению, вам не нужно перекомпилировать библиотеки DLL каждый раз, когда вы обновляете файлы .JS с помощью встроенного пакета. Вы можете просто сбросить пул приложений, в которых работает приложение. Если вы работаете с моделью сохранения внешнего сеанса, ваши пользователи не заметят, когда это произойдет.

person Tim P.    schedule 15.07.2013
comment
Это возможно, и я это сделал. Смотрите мой ответ выше. Я понимаю, что вы имеете в виду, говоря о том, что перекомпиляция не требуется, и я должен был быть более конкретным - причина, по которой мне пришлось сделать это раньше, заключалась в том, что я решил создавать `` основные '' пакеты (со сценариями, используемыми на каждой странице) во время application_start внутри Global.asax.cs, но теперь я понимаю, что это не было строго необходимым - спасибо. - person nothingisnecessary; 17.07.2013
comment
Да, одним из преимуществ является преодоление ограничений браузера для одновременных запросов, но другим важным преимуществом является то, что System.Web.Optimization создает хэш на основе содержимого файла, и браузеры используют его, чтобы узнать, изменились ли файлы. Если браузер видит другой URL-адрес запроса, он повторно запрашивает ресурс. В противном случае браузер обычно (в зависимости от настроек) проверяет кеш (и TTL: какой сервер использует для установки времени жизни кеша), и, если кеш не найден или истек срок TTL, повторно запрашивает ресурс с сервера. Если содержимое сервера изменилось, сервер отвечает новым содержимым, в противном случае отправляет HTTP 304. - person nothingisnecessary; 17.07.2013

Ваша проблема здесь в том, что вы на самом деле не обдумываете эту проблему. Если бы это было так, вы бы поняли, что то, о чем вы просите, не может сработать.

Почему? Поскольку тег скрипта предназначен для создания ссылки на внешнюю ссылку на другой URL-адрес. Таким образом, все, что вы помещаете в заголовок текущего файла, не повлияет на ваш другой URL-адрес, который фактически содержит ваши пакеты. Таким образом, нет возможности динамически изменять ваши пакеты на самой странице, потому что пакеты по определению должны быть определены во внешнем ресурсе.

Теперь нет ничего, что говорило бы, что эти пакеты должны быть скомпилированы в DLL в вашем собственном решении, но они не могут быть встроены в страницу, которая в настоящее время отображается.

Возможно, вы захотите изучить некоторые инструменты минификации на основе javascript, поскольку они обычно не компилируются.

person Erik Funkenbusch    schedule 15.07.2013
comment
Полностью не согласен. Смотрите мой ответ выше. - person nothingisnecessary; 17.07.2013
comment
Хм, много скептиков, но верят они мне или нет - мое решение работает очень хорошо и требует минимальных изменений кода в файлах .aspx. Кроме того, поскольку это корпоративное веб-приложение с большим объемом данных, минификация была наименьшей из моих проблем. Я должен был сказать это раньше, но моя проблема №1 заключалась в том, чтобы позволить браузерам пользователей автоматически обновлять кеш при изменении содержимого файлов .js. (Другая цель, о которой я раньше не упоминал, заключалась в том, чтобы сделать преобразование существующих тегов <script> как можно проще, поскольку это веб-приложение содержит сотни файлов .aspx, и мне нужно обучать других разработчиков тому, как это делать.) - person nothingisnecessary; 17.07.2013
comment
@nothingisneeded - другими словами, вы не хотели достигать каких-либо целей веб-оптимизации ... и все же настаивали на том, что на самом деле вы можете делать то, чего на самом деле не делаете. Я рад, что ваше решение работает для вас, но это не то, о чем вы просили. - person Erik Funkenbusch; 17.07.2013
comment
Не уверен, что понимаю. Я получаю (1) минимизацию (меньше данных, отправленных изначально), (2) автоматическое обновление кеша (из-за хеширования содержимого файла) и (3) улучшенную производительность во время обновлений кеша (из-за объединения нескольких файлов сценариев для преодоления ограничения браузеров в одновременные запросы к одному домену). Для меня минификация является низким приоритетом, поскольку многие страницы перегружены данными и обычно используют AJAX для запроса данных по частям (размер файлов .js крошечный, пропорционален размеру запрошенных данных XML / Json). Наивысшим приоритетом было автоматическое обновление кеша при изменении файлов .js (например, после обновления). - person nothingisnecessary; 17.07.2013