ASP.NET и сжатие клиентского кода

Источник: habrahabr
ZetaTetra

Со времён MSIE4 и блокнота, Я люблю комментировать написанный код. Но одно дело, когда проект создаётся для себя, или при компиляции комментарии не попадут конечному пользователю. И совсем другое дело, когда написанные комментарии могут попасть конечному клиенту. Во первых, они ему не нужны, а во вторых, в комментариях может содержаться некий текст, который может подвергнуть опасности этот или другой проект. И в третьих, содержимые в клиентском коде (HTML, JS, CSS) комментарии, даже в сжатом виде, создают паразитный трафик.
Изучая такой код иногда можно натолкнуться на интересные вещи. Вот, к примеру, один из кусков комментариев на одном всеми известном сайте:… //ubepua07/default/main/SonyStyle/WORKAREA/Common/SonyStyleStorefrontAssetStore/include/CMSpot …А внизу всего этого хозяйства красуется вот такой текст:
… (Developers should log their changes, including line number for reference here) …Вот пример комментариев от компании Яндекс:
"Любишь заглядывать в консоль? А может и js умеешь писать?…"

Я хочу рассказать о нескольких ситуациях, с которыми мне приходится сталкиваться достаточно часто:

  1. Клиентский код в виде ресурсов в сборке
  2. Клиентский код в качестве файлов WebForms или MVC решения
  3. Клиентский код, находящийся на сайте для статичных ресурсов (http://static.sitename.com)

В тексте статьи Я использую самописный инструмент: SourceCruncher. Есть намного более эффективные средства для сжатия кода вёрстки HTML, CSS и JS скриптов с поддержкой затенения и более эффективного процесса сжатия. (Я написал свой вариант для более детального понимания процесса парсинга браузерами кода вёрстки и скриптов.)

Клиентский код в виде ресурсов в сборке.

Я начал искать варианты решения задачи используя подручные средства. И для сжатия ресурсов внутри сборки решение нашлось достаточно быстро. На идею про сжатие скрипта в сборке меня натолкнул скрипт, который я использовал до появления в 10ой студии опции, с видоизменением файлов Web.config для разных конфиураций выкладывания решения (Идея была не моя и автора идеи Я забыл). В Visual Studio можно написать команду выполняемую до и после сборки проекта.

(Project Properties → Build Events → {Pre-build event command line/Post-build event command line})
Для того, чтобы работал этот метод, необходимо прописать команду копирования наших клиентских файлов во временный файл. За это отвечает команда копирования:
copy "$(ProjectDir)Script.js" "$(ProjectDir)Script.orig.js" /V /YЗатем, необходимо применить сжатие для файла скрипта. В данном проекте, программа для сжатия находится в корневой папке:"$(SolutionDir)..\..\SourceCruncher.exe" /IO:"$(ProjectDir)Script.js" /YПосле того, как сборка собралась, необходимо вернуть наш клиентский скрипт в оригинальный вид. Для этого, добавляем в событие "Post-build event command line:" код перемещения сжатого варианта файла на не сжатый вариант:move /Y "$(ProjectDir)Script.orig.js" "$(ProjectDir)Script.js"Важно!
Не забудьте включить событие post-build только после успешной сборки:Run the post-build event: On successful build.Иначе, при ошибки компиляции файл Script.js останется сжатым.
В данном примере есть одно ограничение. Если необходимо отладить клиентский код, то необходимо будет вручную убрать код копирования и сжимания js файла. Или захардкодить для отладочного примера ссылку на оригинальный файл скрипта, а не на сжатый файл ресурса из сборки. Это связано с тем, что Build Events выполняется во всех конфигурациях.

Клиентский код в качестве файлов WebForms или MVC решения

Из готовых решений, Я нашёл только один вариант, сжимать клиентский код в рантайме через регулярное выражение. С одной стороны, решение очень простое, но Я лучше потрачу ресурсы процессора на более интересную задачу, нежели сжатие одного и того-же файла при каждом запросе.
Ещё можно написать сервис и мониторить изменение файлов на жёстких дисках серверов и сжимать их при изменении. Но такое решение неудобно по 2 причинам:

  1. Физического доступа на сервер может не быть.
  2. Действие происходит не явно.

Пришлось искать решение в режиме "До выкладывания". Сначала Я решил делать это через события "Build Events" как и в случае с кодом ресурсов сборки. Но они происходят каждый раз после сборки проекта, а не перед выкладыванием. Во первых, затормаживая сам процесс сборки (особенно если учесть что клиентского кода может быть очень много), а во вторых, усложняет процесс поиска ошибок в клиентском коде.
Т.к., в большинстве случаев, я выкладываю проект через команду "Publish Web" (т.е. имеется физический доступ к серверу), в студии, то Я решил копать в эту сторону и попытаться сжимать файлы сразу после выкладывания проекта на рабочий сервер.
Вот тут я столкнулся с первой проблемой: Для события публикации в студии нет стандартного UI.
Немного погуглив, необходимое событие нашлось в EnvDTE для студии, которое используется для написания Add-In"ов.

Итак, написав небольшой Add-In Я добавил в него необходимые команды и радовался жизни. Для примера, команда на сжатие файла Style.css выглядела следующим образом:"$(SolutionDir)\Solution Items\SourceCruncher.exe" /IO:"\\server\Path\styles\Style.css" /YВ начале, проблемы не было, т.к. каждый проект выкладывался только в одну папку и путь к папке можно было захардкодить. Но если необходима бесперебойная работа сервера, то отключать пользователей сообщением App_Offile.html, Я считаю не вполне этично. Так что сначала начал выкладывать проекты в 2 папки. По принципу, рабочая версия/предыдущая версия. Сначала Я решил эту проблему таким образом:

  1. Подлключаюсь к IIS через ADSI
  2. Беру путь папки в которой у меня работает решение
  3. Удаляю всё содержимое папки {ProjectName}_PrevVersion
  4. Копирую всё решение из этой папки в папку {ProjectName}_PrevVersion
  5. Переключаю решение с рабочей папки на папку {ProjectName}_PrevVersion
  6. Выкладываю решение в рабочую папку
  7. Переключаю папки с {ProjectName}_PrevVersion на рабочую папку.

Но такой подход занимает много времени, несмотря на то, что всё это решается автоматом. А если ещё весь проект лежит на DFS, то сервера будут заниматься лишней работой перетасовывания файлов.
Пришлось копать дальше… В документации на разработку Ad-In"ов для студии нет ни одного упоминания о пути куда выкладывалось решение. В результате, самым прямолинейным путём осталось копать память. К моему облегчению, код публикации написан на управляемом коде, так что использование рефлексии помогло найти место переменной для VS 2005 и 2008 быстро. Для VS 2010 место переменной, где хранится путь к папке, куда выкладывалось решение пришлось поискать подольше, т.к. разработчики добавили профили выкладывания (Publish Profile).
В результате, получился Add-In позволяющий выполнять произвольные команды перед и после выкладывания проекта.

Установка Add-In'а

Прописанные комманды сохраняются в .sln файле решения, так что все разработчики решения у которых будет стоять этот Add-In смогут выложить решение с применением необходимого сжатия.
Проблемы:
При использовании TFS 2010 есть проблема (ссылка больше не доступна). Соответственно, при использовании VS9 и VS10, при открытии решения любым разработчиком, sln файл будет взят разработчиком на изменение автоматически.
Если Вы пользуетесь VSS или Tortoise, то таких проблем нет.
Есть ещё одна проблема связанная с TFS, которую Я пока до конца не изучил: Если .sln файл взят на изменение другим разработчиком, то плагин не может прочитать команды для сжатия из sln файла.

Пример на WebForms

Приведу пример ситуации, при котором генерируемый код вёрстки получается сильно замусоренный лишним текстом (Для примера Я использовал типизированный Repeater, написанный Andrey Shchekin в 2007 году):<user:TypedRepeater ID="repFilters" DataItemTypeName="Company.Dal.Catalog.FilterToc" OnItemDataBound="repFilters_ItemDataBound" runat="server"> <ItemTemplate> <tr> <td><%# Container.DataItem.FilterName %></td> <td> <%-- Строковый тип фильтра--%> <user:TypedRepeater ID="repFiltersString" DataItemTypeName="Company.Dal.Catalog.FilterString" runat="server"> <ItemTemplate> <asp:TextBox Text="<%# Container.DataItem.FilterValue %>" runat="server" /> </ItemTemplate> </user:TypedRepeater> <%-- Битовый тип фильтра --%> <user:TypedRepeater ID="repFiltersBit" DataItemTypeName="Company.Dal.Catalog.FilterBit" runat="server"> <ItemTemplate> <asp:CheckBox Checked="<%# Container.DataItem.FilterValue %>" Text="<%# Container.DataItem.FilterName %>" runat="server" /> </ItemTemplate> </user:TypedRepeater> … </td> </tr> </ItemTemplate> </user:TypedRepeater>Если представить что типов фильтров может быть, скажем, 10, так же как и среднее кол-во фильтров на страницу, то в файле отправляемом клиенту будет большое кол-во паразитного траффика.

Клиентский код, находящийся на сайте для статичных ресурсов

Есть несколько решений, которые используют одни и те-же стили. Допустим, основной сайт и интранет сайт одной из задач которого является написание статей для основного сайта в WYSIWYG редакторе. Соответственно, базовые стили обоих сайтов должны быть идентичными. Для этого используются файлы стилей и изображений доступным обоим сайтам (это статья не о версионности, так что проблему соответствия всех сайтов одним глобальным стилям Я опускаю. Так же как и проблему хранения версий). Для примера приведу несколько вариантов:

  1. CSS/JS файлы находятся внутри основного решения в виде ссылок на актуальные файлы на общем сервере.
    В таком случае можно применять к ним сжатие как и к файлам основного решения описанного в пп.2 статьи.
  2. CSS/JS файлы находятся в основном проекте и при выкладывании основного решения перемещаются на общий сервер.
  3. Если верстальщик не пользуется VS, а использует свой инструмент для редактирования общих файлов, то можно использовать программу DropTarget в совокупности с программой для сжатия клиентского кода.

Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=32292