Статьи

ASP.NET и Компактный код

22 февраля 2013 г.

Введение

Со времён MSIE4 и блокнота, Я люблю комментировать написанный код. Но одно дело, когда проект создаётся для себя, или при компиляции комментарии не попадут конечному пользователю. И совсем другое дело, когда написанные комментарии могут попасть конечному клиенту. Во первых, они ему не нужны, а во вторых, в комментариях может содержаться некий текст, который может подвергнуть опасности этот или другой проект. И в третьих, содержимые в клиентском коде комментарии расходую никому не нужный трафик.

Изучая такой код иногда можно натолкнуться на интересные вещи. Вот, к примеру, один из кусков комментариев на одном всеми известном сайте:


…
//ubepua07/default/main/SonyStyle/WORKAREA/Common/SonyStyleStorefrontAssetStore/include/CMSpot
…

А внизу всего этого хозяйства красуется вот такой текст:

…
(Developers should log their changes, including line number for reference here)
…

А вот пример комментариев от компании Яндекс:

"Любишь заглядывать в консоль? А может и js умеешь писать?…"

Я рассмотрю несколько наиболее часто встречающихся сценариев:

  1. Клиентский код в виде ресурсов в сборке
  2. Клиентский код в качестве файлов ASP.NET или MVC решения
  3. Клиентский код, находящийся на сайте для статичных ресурсов

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

Клиентский код в качестве файлов ASP.NET или 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
В начале, проблемы не было, т.к. каждый проект выкладывался только в одну папку и путь к папке можно было захардкодить. Но через некоторое время Я решил выкладывать проекты в 2 папки. По принципу, рабочая версия/предыдущая версия. Сначала Я решил эту проблему таким образом:
  1. Подлключаюсь к IIS через ADSI
  2. Беру путь папки в которой у меня работает решение
  3. Удаляю всё содержимое папки {ProjectName}_PrevVersion
  4. Копирую всё решение из этой папки в папку {ProjectName}_PrevVersion
  5. Переключаю решение с рабочей папки на папку {ProjectName}_PrevVersion
  6. Выкладываю решение в рабочую папку
  7. Переключаю папки с {ProjectName}_PrevVersion на рабочую папку.

Но такой подход занимает много времени, несмотря на то, что всё это решается через Active Directory. А если ещё весь проект лежит на ферме, то сервера будут заниматься лишней работой перетасовывания файлов.

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

UI.

(Пример для Visual Studio 2008, но от VS 2010 никаких различий нет.)

Элемент меню AddIn'а

Запуск окна с настройкой

К сожалению, вариант установки есть только один — ручками. Инсталлера, увы, нет.:

  1. Скачиваем весь архив и распаковываем его в желаемую папку (Или в известную папку с Add-in’ами, если у Вас такая уже есть).
  2. Затем, открываем студию: Tools → Options → Add-in/Macros Security и в окне Add-in File Paths прописываете путь который Вы указали на шаге один. (Или переносите распакованные файлы в папку указанную в этом списке).
  3. Перезапускаем студию, включаем Add-In через "Add-in Manager" (Tools→Add-in Manager...→Flatbed.EnvDTE)
  4. И в меню Tools появиться команда "Flatbed.EnvDTE Properties" (По Add-in, ищет плагины в директории из которой он запущен).
  5. После этого, загружаем проект
  6. Если разработчиков проекта несколько человек, то добавляем в проект консольное приложение для сжатия кода (В данном случае, в качестве примера, используется моя утилита SourceCruncher). Или указываем сетевой путь до консольного приложения, которое будет доступно всем разработчикам.
  7. Открываем окно настройки плагина "PluginDTE.CmdLine" (Tools→Flatbed.EnvDTE Properties→PluginDTE.CmdLine)
  8. И прописываем консольную команду на сжимание интересующих файлов. Пример:
    "\\server\Path\SourceCruncher.exe" /IO:"${PublishLocation}js\Utils.js" /Y
  9. После того, как команды прописаны, нажимаем Ctrl+S для сохранения команд. (Команды сохраняются в .sln файле решения, так что все разработчики проекта у которых будет стоять этот Add-In увидят все внесённые изменения).
    Тут есть один минус, если проект лежит в TFS’е, то каждый раз при открытии проекта файл .sln будет браться на редактирование (Check out).

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

Проблемы:

При использовании TFS 2010 есть проблема. Соответственно, при использовании VS9 и VS10, при открытии решения любым разработчиком, sln файл будет взят разработчиком на изменение автоматически. Если Вы пользуетесь VSS или Tortoise, то таких проблем нет.

Есть ещё одна проблема связанная с TFS, которую Я пока до конца не изучил: Если .sln файл взят на изменение другим разработчиком, то плагин не может прочитать команды для сжатия из sln файла.

Пример на ASP.NET’е

Приведу пример ситуации, при котором генерируемый код вёрстки получается сильно замусоренный лишним текстом (Для примера Я использовал аналог типизированного Repeater’а, который разработчики ASP.NET’а добавят в следующей версии, т.к. на таком коде лучше понять суть, нежели на обычном репитере):

<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:HyperLink NavigateUrl="<%# PageGetFilterUrl(Container.DataItem) %>"
							Text="<%# Container.DataItem.FilterValue %>" runat="server"/>
					</ItemTemplate>
				</user:TypedRepeater>
				<%-- Битовый тип фильтра --%>
				<user:TypedRepeater ID="repFiltersBit" DataItemTypeName="Company.Dal.Catalog.FilterBit" runat="server">
					<ItemTemplate>
						<asp:HyperLink NavigateUrl="<%#  PageGetFilterUrl(Container.DataItem) %>"
							Text="<%# Container.DataItem.FilterValue %>" runat="server"/>
					</ItemTemplate>
				</user:TypedRepeater>
			</td>
		</tr>
	</ItemTemplate>
</user:TypedRepeater>

Немного комментариев: Есть 2 датасета. Первый это типы фильтров, а 2ой значения фильтров. Код в DataBind’е выглядит так:

repFilters.DataSource=this.GetFilters(…);
repFilters.DataBind();

А код в repFilters_ItemDataBound следующий:

…
FilterToc filter=e.Item.DataItem;
Repeater repeater;
switch(filter.FilterType)
{
case FilterType.StringFilter:
	repeater=(Repeater)e.Item.FindControl("repFiltersString");
	break;
case FilterType.BitFilter:
	repeater=(Repeater)e.Item.FindControl("repFiltersBit");
	break;
…
default:
	…
}
repeater.DataSource=this.GetFilterValues(filter.FilterID);
repeater.DataBind();
…

Если представить что типов фильтров может быть, скажем, 10, так же как и среднее кол-во фильтров на страницу, то в файле отправляемом клиенту будет большое кол-во лишних пробелов и пустых строк.

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

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

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

Ссылки на проекты упомянутые в статье