Как отрефакторить 17 тысяч строк CSS

Источник изображения: Ursus Wehrli. The Art Of Clean Up

Многие из нас работают над большими проектами. Тот, о котором расскажу я, живёт вот уже 15 лет и имеет в своём составе пару десятков веб-приложений на ASP.NET WebForms, главное из которых содержит в себе около полутора тысяч aspx-страниц.

Зачем так много? Желание подстроиться под заказчиков с различными требованиями даёт о себе знать. Каждый хочет какую-нибудь свою особенную функциональность и в итоге получает её. Но речь не об этом. В довесок к большому числу страниц, у нас было много стилей CSS. Очень много.

Изначально существовал один веб-проект. У него был один CSS-файл. Файл был не слишком большим, и читаемость его не вызывала ни у кого вопросов. Время шло, люди приходили, создавали новую функциональность и дописывали селекторы в CSS-файл. Это выглядело не очень страшно, в содержимом пока ещё можно было разобраться.

Затем пришла пора сложной функциональности, когда перекрывались стили контролов, расположенных на определённых страницах, на которые навешивался определённый класс. UI нравился дизайнерам и пользователям, и класс перекочёвывал на другие страницы без всякой семантической связи с первоначальным местом. В какой-то момент файл превратился в гигантскую помойку из 17 тысяч строк.

Чтобы не казалось слишком просто, в системе придумали делать кастомные скины (отдельные стили) для отдельных заказчиков. Вот эти 17 тысяч строк для всех общие, а вот ещё чуть-чуть мы вынесем, чтобы кастомизировать цвета текста и фона. На деле вышло совсем не чуть-чуть, а очень даже много. Километры CSS, причём какая-то их часть вполне себе могла быть для всех общей.

Проблема

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

  • 12 различных гридов, выглядящих не то чтобы похоже, но и не то чтобы совсем по-разному.
  • 4 варианта тулбаров над гридами с тремя вариантами кнопок и выпадающих списков (тоже разных, но подозрительно похожих по стилю).
  • Даже информационных сообщений, которые представляют собой всего-то текст с иконкой на красном, жёлтом или синем фоне, и то было 2 варианта.

Тут надо сказать ещё об одной вещи. У нас не один веб-проект, а несколько. Контролы и стили к каждому из них развивались параллельно довольно продолжительное время, пока, наконец, кому-то в голову не пришла светлая идея объединить это всё и вынести в библиотеку общих контролов с частью общих стилей (которые можно затем перекрывать в каждом из проектов). Написание этой библиотеки не ознаменовало унификации всего и вся и удаления дубликатов. Вместо этого контролов и стилей стало ещё больше. Общий грид в общей библиотеке — вещь полезная, но удалить 12 имеющихся из трёхсот мест по всему проекту вы не сможете — ни одна нормальная команда под это не подпишется. Вообще в какой-то момент народ перестал понимать, какой из наших многочисленных контролов стоит использовать в том или ином случае. Каждый из них может реализовать какое-нибудь специфическое поведение, пришедшее в голову дизайнеру в момент дизайна конкретной функциональности.

И ещё мы наткнулись на известную проблему в браузере Internet Explorer 9, который почему-то отказывался воспринимать более чем 4095 селекторов в нашем славном файле.

И вот нам пришлось полностью поменять весь дизайн приложения. Было ясно, что просто так поменять дизайн будет себе дороже — надо предварительно отрефакторить CSS. Весь CSS. Product owners понимали ситуацию и мы получили отмашку на рефакторинг.

Начинаем

Итак, мы все договорились и получили свободу действий — начинаем разгребать завалы. Что же у нас в наличии?

  • Один гигантский CSS файл с 17 тысячами строк для основного веб-приложения.
  • Более 50 кастомных скинов, для каждого отдельный CSS-файл. В теории, в нём должны присутствовать только незначительные различия, на практике встречались вполне себе нехилые куски общего кода.
  • CSS-файл с библиотекой контролов, который использовался не только в основном, но и в других приложениях.
  • CSS-файлы дополнительных приложений. Тут всё было несложно хотя бы потому, что все они были хоть и с копипастой, но довольно небольшого размера.

Первое, с чего мы начали — это удаление неиспользуемых стилей. С кодом на C# всё просто: включил анализатор солюшена, он и показал, какие из публичных методов можно удалить. Со стилями единственный путь — это текстовый поиск по солюшену. Если вас когда-нибудь попросят вычистить таким способом 17 тысяч строк в CSS-файле, лежащем в солюшене из двухсот пятидесяти проектов, попросите себе SSD-диск. Процесс пойдёт чуть быстрее.

А вот если вы только планируете написать такой гигантский проект с нуля, то вот вам несколько советов:

Всегда используйте название селектора в коде полностью.

Никогда не разбивайте его на части, например:

public const string ProgressBar = "progressbar";
public const string ProgressBarSmall = ProgressBar + "small";

Крупный прогресс-бар вы найдёте, а мелкий удалите как неиспользуемый при рефакторинге. Потом придётся восстанавливать. Думаете, и так запомните, что используется, а что нет? В проекте из полутора тысяч страниц? И с сотнями настроек, включающих и отключающих различные фичи?

Также не умничайте с конструкциями такого вида:

public class Feedback
{
    public static string CssClass = GetType().ToString();
}

Во-первых, для наследников класса всё равно не работает (GetType() надо поменять на typeof(Feedback)); во-вторых, искать невозможно.

Просто храните текст. Поиск по тексту в солюшене с preserve case и whole word. Не усложняйте себе жизнь.

Используйте префиксы.

Про это стоит рассказать отдельно чуть ниже. Опустим пока что возможность условно делить селекторы, принадлежащие разным модулям, давая им отдельные префиксы.

Использования CSS-классов со следующими именами искать невозможно:

.hidden
.visible
.error
.text

Куда приятней с префиксами. Первые два можно условно отнести к помощникам — helpers. Наделим их префиксами: .h-hidden и .h-visible. Уже можно искать! Если классы специфические, то можно использовать название проекта. MyProject? Пусть будет .mp-. Скажем, .mp-login-page. Контролы общие? — .ct-

Некоторые третьесторонние библиотеки также используют префиксы. По префиксу становится легко отличить, чему принадлежит код. Кроме того устраняется небольшой риск пересечения имён.

Не используйте одни и те же названия селекторов для разных нужд.

Скажем, .error и .text из предыдущего примера — это совсем запущенный случай. Селекторы не всегда уникальны и могут использоваться совершенно по-разному в разных случаях.

.mp-login-page .error
{
    color:red;
}
.ct-feedback .error
{
    background-color: red;
}

Я бы заменил это на следующие селекторы:

.mp-login-page-error
{
    color:red;
}
.ct-feedback-error
{
    background-color: red;
}

Громоздко? Несомненно. Зато хорошо ищется и удаляется.

Сделайте себе список стилей.

Сделайте себе статический класс с указанием если не всех, то хотя бы часто использующихся стилей. Будет куда проще и искать затем, и переименовывать.

Действуем

Мой дядя всегда говорил, что уже на этапе строительства надо думать о том, как бы сделать так, чтобы при желании всё это можно было проще всего разломать.

Думаете, у вас проект маленький, и вам не придётся? Мы тоже не думали, и проект превратился в монстра очень большой. В общем, удаление неиспользуемых стилей — вполне себе нормальная ситуация. Неиспользуемых классов было много, частенько можно было удалить несколько экранов текста подряд. В результате в нашем файле из 17 тысяч строк осталось всего… 16 тысяч. Правда ли, что мы это всё используем? Да! Это наше всё годами нажитое. Всё нужно!

Что дальше? Выяснилось, что селекторов у нас осталось всё равно очень много, причём после чистки все 100% из них используются. Когда мы удаляли неиспользуемые стили, то обратили внимание на такие вот селекторы, в которых название класса встречалось лишь единожды

.personal-report-bottom-section
{
    margin-top: 10px;
}
.users-overview-header
{
    padding-left: 15px;
}

Получалось как-то совсем глупо. Куча похожих стилей в классах, использующихся один раз. Идея была заимствована у Bootstrap — использовать вспомогательные классы. Скажем, для margin-top: 10px можно использовать имя .h-mt10, для padding-left: 15px; –.h-pdl15. Это помогло вычистить ещё множество мест.

Затем принялись искать повторяющиеся по смыслу куски. Самым популярным было иметь гиперссылку или обычный текст с картинкой слева (<a><img /><span></span></a>):

a.ct-imagewithtext
{
    text-decoration: none;
}
.ct-imagewithtext img,
.ct-imagewithtext span
{
    vertical-align: middle;
}
.ct-imagewithtext img
{
    margin-right: 4px;
}
.ct-imagewithtext span
{
    text-decoration: underline;
}

Думаю, что похожих стилей в коде было штук 20, не меньше. Но каждый раз названия классов были новыми, стили отличались иногда незначительно, а иногда и довольно серьёзно. В процессе преобразования проект стал хорошеть визуально — небольшие, но заметные на глаз отличия на разных страницах стали постепенно стираться.

В дальнейшем мы сумели отрефакторить похожие контролы с совершенно разными стилями — удаляли редко используемые, меняя их на аналоги. Если использований было слишком много — брали наиболее удачный с точки зрения CSS контрол и «гримировали» под него все остальные. Не всё удалось удалить или загримировать, но об этом чуть позже.

Наконец, руки дошли и до библиотеки общих контролов. Как я уже писал, в общую часть была вынесена лишь часть стилей. Затем эти стили уже перекрывались в каждом из наших веб-приложений. Идея была не очень удачной — всё то, что перекрывалось, перекрывалось в каждом приложении одинаковым образом, т.е. фактически было копипастой. Впрочем, не совсем. Когда по дизайну нужно было чуточку изменить цвета, меняли только в одном месте, забывая про остальные. Мы вынесли всё это в общую часть и затем долго убивали копипасту. Приложения хорошели — тулбары, гриды и другие контролы разных цветов стали похожими друг на друга.

В какой-то момент мы поняли, что хорошо бы вынести некое подобие CSS Reset в эту библиотеку. До этого CSS Reset был представлен только в двух приложениях, причём в каждом свой. В результате решено было использовать Normalize.css, который мы и включили в самое начало. Туда же мы добавили базовые стили для нашего приложения — размер и гарнитуру шрифта (тоже везде отличались) и многие другие вещи.

В этот момент мы всё ещё занимались так или иначе удалением копипасты и унификацией стилей. Наконец, руки дошли до кастомных скинов. По сути, интерфейс они меняли не сильно, но вот почему-то содержали в себе огромное количество стилей. Скинов было что-то около 50, некие старые из них были написаны руками, более новые использовали LESS. Несмотря на это, общего шаблона не было, генерация производилась вручную, а результат затем пристыковывался к некой общей части. С них-то мы и начали. Во-первых, вынесли общий шаблон (не у всех совпадал) и повторяющуюся часть. Во-вторых, настроили генерацию CSS-файлов при билде проекта с помощью утилиты lessc. Затем приступили к старым скинам, где LESS не использовался.

Несмотря на большой объём кода, всё оказалось не сильно изменённой впоследствии копипастой. В конце у нас получилась для всех одинаковая общая часть, шаблон на 1,5 экрана и 50 less-файлов с 15 переменными для индивидуальной настройки скина.

Общий кусок вынесли в конец нашего гигантского файла. Поскольку при равенстве весов CSS-селекторов приоритет будет за тем, что находится ниже по тексту, это важно. На дальнейшей ступени рефакторинга нам сильно помог add-on к Visual Studio — Web Essentials. Штука очень полезная — если вкратце, то своеобразный решарпер для CSS. Помимо нахождения синтаксических ошибок и советов добавить недостающий вендорный префикс, Web Essentials помогает искать одинаковые классы внутри файла. И оказалось, что в нашем коде часто возникала такая вот ситуация.

Определён селектор скажем на 6255-й строке:

.topmenu-links
{
    margin-top: 15px;
    background-color: blue;
}

Затем где-нибудь на 13467-й:

.topmenu-links
{
    margin-top: 10px;
    background-color: green;
}

Я немного утрирую, но было примерно так. Причём случай носил не единичный, а массовый характер. Доходило и до четырёх перекрытий. Web Essentials нещадно ругается на такие вещи, так что все они были найдены и удалены. Как я говорил, при равенстве весов селекторов приоритет за тем, что ниже, так что удаляем селекторы сверху и объединяем. Процесс немного рисковый. При большом количестве разных стилей, навешенных на один и тот же элемент, и разбросе селекторов по файлу перемещение чревато сменой приоритета. Но тут уж ничего не поделаешь. В ходе всей работы наши QA периодически гуляли по всем страницам системы и сравнивали вид с продакшеном.

В какой-то момент мы созрели до того, чтобы разбить наш огромный CSS на части по сегментам. Получилось 120. При билде файл собирался обратно в один. А через некоторое время мы перешли на LESS.

Как оно сейчас

Посмотрим, как это всё выглядит на примере.

Немного упростим задачу и представим, что у нас есть библиотека общих контролов (CommonControls) и проект для статического контента (CDN), использующегося в основном веб-проекте.

библиотека общих контролов

В библиотеке с контролами имеем LESS-файлы, которые при билде собираются в один (common-controls.less) и затем транслируются в CSS (common-controls.css).

папка Common Controls

Рассмотрим чуть подробнее, что где хранится.

  • 01-essentials.less хранит в себе только переменные и миксины. Они используются как LESS-файлами библиотеки контролов, так и файлами остальных проектов.
  • 02-normalize.less. Про него я уже немного рассказывал. Он хранит слегка изменённый под нужды проекта код нормализации CSS.
  • 03-default-styles.less хранит общие стили оформления (например, цвет фона элемента body, гарнитуру используемого шрифта, и т.д.)
  • 04-helpers.less хранит вспомогательные классы вроде уже описанных отступов margins, paddings.
  • Далее идут собственно стили контролов.

Настраиваем Build Events для проекта CommonControls. Я выношу всё в отдельный файл, чтобы не редактировать и не мёржить файл проекта каждый раз, когда меняется содержимое скрипта.

Pre=Build Event Command Line

Код скрипта очень простой. Собираем вместе все файлы LESS в папке Stylesheets и переносим результат в папку CombinedStylesheets. Затем запускаем препроцессор и получаем готовый CSS.

set ProjectDir=%~1
copy "%ProjectDir%Stylesheets\*.less" %ProjectDir%CombinedStylesheets\common-controls.less
call "%ProjectDir%..\Tools\lessc\lessc.cmd" %ProjectDir%CombinedStylesheets\common-controls.less %ProjectDir%CombinedStylesheets\common-controls.css

Теперь посмотрим на стили проекта Cdn. В папке _cssparts лежат стили проекта, которые затем собираются в файл combined.less. В реальном проекте файлов очень много. На скриншоте всё чуточку упрощено.

папка Cdn

Последовательность файлов особого значения не имеет, кроме самого первого и самого последнего.

001-imports.less содержит в себе следующий код:

// Importing LESS template from CommonControls
@import "../../CommonControls/Stylesheets/01-essentials.less";
// Usual CSS import
@import "common-controls.css";

В первой директиве импортируется содержимое файла LESS — в данном случае, 01-essentials.less. Это равносильно тому, как если бы мы конкатенировали этот файл вместе с остальными при комбинировании. Импорт позволяет использовать все переменные и миксины, которые мы определили в библиотеке CommonControls. Вторая директива — классический импорт, в результирующем CSS генерируется как есть. Вообще импорты CSS использовать не рекомендуется, и единственная причина, почему он здесь — это IE9.

z-ie9-special.less содержит в себе один-единственный селектор, который в комбинированном файле идёт самым последним и используется на специальной странице, чтобы понять, применяется он или нет. Если общее число селекторов превысило 4095, то стиль не применится. Значит надо разбивать файл на части. Фактически нам пришлось не комбинировать результирующий CSS библиотеки контролов и собственно CSS для веб-проекта.

При билде происходят следующие вещи:

@REM Copy common controls stylesheet
COPY %ProjectDir%..\CommonControls\CombinedStylesheets\common-controls.css "%ProjectDir%Skins\common-controls.css" /Y
@REM Combine CDN LESS files and run preprocessor
copy "%ProjectDir%Skins\_cssparts\*.less" %ProjectDir%Skins\combined.less
call "%ProjectDir%..\Tools\lessc\lessc.cmd" %ProjectDir%Skins\combined.less %ProjectDir%Skins\combined.css

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

Посмотрим теперь на генерацию кастомных скинов.

папка Skins

В папке _custom-parts лежит шаблон для генерации custom-template.less. Предположим, что нам пока что достаточно кастомизировать цвета заголовков H1 и H2 (в реальности, конечно, намного больше вещей). custom-template.less будет выглядеть так:

h1
{
    color: @h1Color;
}
h2
{
    color: @h2Color;
}

Default-values.less будет содержать в себе значения переменных по умолчанию (чтобы иметь возможность перекрывать в скине не всё подряд, а только некоторые из значений):

@h1Color: #F58024;
@h2Color: #E67820;

В каждом из скинов (skin.less) будет примерно такой код:

@import "..\_custom-parts\default-values.less";
@h1Color: #000;
@h2Color : #707050;
@import "..\_custom-parts\custom-template.less";

Импортируем значения по умолчанию, перекрываем их своими значениями и импортируем шаблон.

Чтобы всё это сгенерировать, пишем такой код в pre build event:

@REM Regenerate customskins using their LESS templates
for /r "%ProjectDir%Skins\" %%i in (*.less) do (
    if "%%~nxi"=="skin.less" call "%ProjectDir%..\Tools\lessc\lessc.cmd" "%%~dpnxi" "%%~dpni.css"
)

На выходе рядом с каждым skin.less получаем skin.css для примера выше такого вида:

h1
{
  color: #000000;
}
h2
{
  color: #707050;
}

Вообще поначалу содержимое наших LESS-файлов (не считая кастомные скины) ничем не отличалось от обычных CSS. За небольшим исключением, когда парсер отказывался воспринимать невалидный код — скажем, такой:

margin-top: -4px\0/IE8+9;

Не уверен, что хак для IE выглядит именно так, но бог с ним. В LESS можно экранировать строку, используя символы ~"":

margin-top: ~"-4px\0/IE8+9";

Всё остальное перешло без проблем. Вскоре начали появляться простенькие переменные:

@сtDefaultFontSize: 14px;

Затем миксины посложнее:

.hAccessibilityHidden()
{
    position: absolute;
    left: -10000px;
    top: -4000px;
    overflow: hidden;
    width: 1px;
    height: 1px;
}

Смысл этого миксина в том, что помимо использования во вспомогательном классе он используется ещё и в некоторых других. В нашем случае стало ещё интереснее. Когда в какой-то момент мы поняли, что не то что переписать, а даже «загримировать» по стилям 12 гридов не представляется возможным, то хорошей идеей стало выносить общие цвета и стили в переменные и миксины. Выходит так, что для старых проектов LESS даже интереснее, чем для новых. Вообще, есть где разгуляться. Например, генерация background-image для кнопок различных типов при наличии спрайта:

.ct-button-helper(@index, @name, @buttonHeight: 30, @buttonBorderThickness: 1)
{
    @className: ~".ct-button-@{name}";
    @offset: (@buttonHeight - 2*@buttonBorderThickness - @buttonIconSize) / 2;
    @positionY: @offset - (@index * (@buttonIconSize + @buttonIconSpacingInSprite));
    
    @{className} { background-position: 8px unit(@positionY, px); }
    @{className}.ct-button-rightimage { background-position: 100% unit(@positionY, px); }
}

Вызываем как-то так:

.ct-button-helper (0, "save");
.ct-button-helper (1, "save[disabled]");
.ct-button-helper (2, "cancel");
.ct-button-helper (3, "cancel[disabled]");

Ещё хорошо генерировать CSS для описаний шрифтов, хотя реализация миксина зачастую зависит от конкретного шрифта.

Резюме

Давайте пройдёмся вкратце по тому, что мы сделали.

  • Мы вычистили гигантское количество неиспользуемых стилей.
  • Удалили большое количество классов, использующихся только в одном месте, заменив их на вспомогательные классы. На самом деле это очень значительная часть всего CSS в проекте, не стоит недооценивать этот пункт.
  • Вынесли общую часть из кастомных скинов, добавив её в конец основного CSS. Этот пункт важно было выполнить перед следующим. Использовали LESS для генерации кастомных скинов.
  • Удалили большое количество перекрытий одного и того же класса несколько раз в одном и том же CSS файле. Тут очень помог WebEssentials. Этот пункт тоже важно было выполнить перед следующим.
  • Разбили общий CSS на части для удобства редактирования. При билде все эти части собираются в один файл.
  • Вынесли перекрытия стилей контролов в сами стили контролов. Удалили копипасту в стилях остальных приложений.
  • Поскольку во всех приложениях использовался единый стиль оформления, общие части (normalize.css, размеры шрифтов, цвета фона) тоже вынесли в библиотеку контролов, удалив куски CSS из всех веб-приложений.
  • Перешли на LESS во всех веб-приложениях.
  • Вынесли часть часто используемых вещей в переменные и миксины в библиотеке контролов, подключили их к каждому веб-приложению, так что использовать их можно везде.
  • Сделали обёртку на C# (простой статический класс со статическими пропертями) для часто используемых CSS-классов. Не для всех — их очень много и не всегда есть смысл.
  • Внедрили префиксы… Не во всех местах, а в часто используемых. Стараемся использовать префиксы для всех новых стилей, но всем пофиг.

В целом мы восстановили контроль над кодом. Последующий редизайн прошёл без проблем. К тому же мы делали его умнее, меняя компонент за компонентом. Не сказать, что код стал идеальным или что его стало в 10 раз меньше. У нас всё равно всего много, и разобраться бывает сложно. Но зато существенно сократилось количество копипасты и, как следствие, стало меньше визуальных различий в различных частях системы, которые по идее должны выглядеть одинаково.

Вам также может понравиться:

Блог Доступный дизайн компонентов на примерах
15 ноября, 2021
Статья об основных руководствах по доступности и о ключевых моментах, на которые стоит обратить внимание, а именно: о порядке фокуса, о клавиатурном взаимодействии и об ARIA-атрибутах.
Блог Разработка системы тестирования SQL запросов. Часть 2
29 сентября, 2021
Продолжение истории о фреймворке, разработанном с целью автоматизации и упрощения процесса тестирования сложных SQL-запросов на крупном проекте.
Блог Техники обработки отказов сервиса в микросервисных архитектурах
07 сентября, 2021
Эта статья может быть полезна для тех, кто пострадал от нестабильной работы внешних API: какие бывают стратегии обработки отказов и какой путь избрали мы.