[Перевод] DRY CSS: Как использовать каждое объявление только один раз
Использование DRY в CSS — это способ максимально избегать повторения в таблицах стилей. Этот подход не панацея, но он достаточно эффективен и является одним из основных методов оптимизации. Поскольку я использовал и изучал его почти 10 лет, в этой статье хочу поделиться своим опытом и знаниями.
А если вам будет интересна тема оптимизации CSS, то я рассказал об основах в моей небольшой книге «CSS Optimization Basics».
Основные шаги
Прежде чем мы углубимся в подробности, как «высушить» таблицы с помощью DRY, обратите внимание, что этот метод нетривиально автоматизировать из-за каскадности. Но, не смотря на это, я постараюсь ничего не упустить.
- Пишите CSS обычным и естественным образом.
- Определитесь с DRY-границами, что вы будете оптимизировать: раздел (функционально разделенные CSS-стили), файл, компонент или @media-запросы. Я работаю на уровне файл/@media, то есть обычно «сушу» всё в максимально возможном объеме.
- Убедитесь, что формат кода является консистентным, так как
border:0;
,border: 0;
иborder: none;
означают одно и тоже, но это значительно усложняет поиск дубликатов. - Найдите повторяющиеся объявления:
- Для новых стилей: после первоначальной инициализации.
- Для новых функций и исправлений: после завершения соответствующих действий.
- Совет: если для изменений в файле недостаточно подсказки системы версионирования, просто временно сделайте отступ для измененных объявлений для последующий проверки на уникальность.
- Разрешите повторяющиеся объявления:
- Проверьте каждое объявление (в новых таблицах стилей) или каждое изменённое объявление на предмет повторений в заданном диапазоне (если дедупликация ограничена отдельными разделами, сузьте поиск этими разделами).
- Для каждого повторного объявления (переход к действиям):
- Определите, какое правило в таблице стилей должно быть первым (для этого вы должны определить, какой путь вы выбираете для сортировки селекторов).
- Если первое правило содержит дополнительные объявления, которые еще не были проверены, то скопируйте всё правило и вставьте его после оригинала. Сохраните обнаруженный дубликат в первом правиле и удалите другие объявления, и сделайте наоборот во втором правиле, чтобы оно было похоже на старое правило, только без объявления, которое будет выполняться более одного раза.
- Скопируйте селекторы других правил, которые содержат соответствующее объявление, в правило, идущее первым.
- Обязательно удалите повторяющиеся объявления, селекторы которых были только что скопированы в таблицу стилей, и удалите правило, если оно состоит только из перемещенного дублирующего объявления.
- (Повторите шаги).
- Убедитесь, что правила, обрабатывающие ранее повторяющиеся объявления, содержат селекторы в правильном порядке.
- Убедитесь, что правила, обрабатывающие ранее повторяющиеся объявления, имеют правильное расположение.
Весь этот процесс, на первый взгляд, может показаться запутанным и пугающим, но сейчас мы погрузимся немного глубже и рассмотрим примеры.
Особые случаи
Существует два сценария, требующих особого внимания:
- Разделение файлов: отдельные CSS-файлы могут быть полезны, особенно при повторной сборке в эксплуатацию, но когда дело доходит до «сушки» объявлений, они создают «жесткий» барьер: требуется много усилий для поиска и удаления повторяющихся объявлений. Если мы работаем с небольшой или средней кодовой базой на CSS, то может быть разумно перейти на одну таблицу стилей. Но когда мы имеем дело со сложными таблицами, то некоторое повторение допустимо.
- Строгость подхода или отступление от него: если мы строго избегаем повторений (то есть полностью хотим удалить дубликаты), мы всё равно будем иногда сталкиваться с исключениями. Эти исключения, кроме структурно-зависимых, таких как @media-запросы, файловые границы, или когда последовательность (каскад) является важной, будут вызывать проблемы с поддержкой. Селекторные хаки также являются исключением, поскольку некоторые селекторы работают таким образом, что фактически не позволяют пользовательскому агенту корректно интерпретировать соответствующее правило. В таких случаях мы не можем избежать совпадений, потому что при объединении соответствующих селекторов мы можем повлиять на их работу и результат будет некорректный.
Примеры
Теперь давайте поработаем с несколькими классическими таблицами без использования препроцессора. Мы сосредоточимся на оптимизации на уровне файла, чтобы можно было представить соответствующий код в виде секции или модуля, который выполняет одно и то же действие.
Оптимизация. Пример первый
Этот пример взят с сайта www.engadget.com, где мы обнаружили 92% повторений, случайный раздел, но по-крайней мере отсортированный в алфавитном порядке. Мы предполагаем, что порядок селекторов нас устраивает, и не будем менять и комментировать имена классов и т. д.
.arrow-left {
border-color: transparent;
border-style: solid;
border-width: 10px 10px 10px 0;
height: 0;
width: 0;
}
.arrow-down {
border-bottom: 10px solid transparent;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid #2b2d32;
bottom: 0;
height: 0;
left: 20px;
width: 0;
}
.faq-list .faq-item-title {
cursor: pointer;
}
.faq-list .faq-item-title:after {
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid #111;
content: '';
display: inline-block;
float: right;
height: 0;
opacity: .5;
vertical-align: top;
width: 0;
}
#contact input:focus,
#contact textarea:focus {
border: 1px solid #3398db;
}
.flickity-slider>.table {
table-layout: fixed;
}
::selection {
background: #9b59b6;
color: #fff;
}
.videoWrapper {
height: 0;
padding-bottom: 56.25%;
position: relative;
}
.videoWrapper iframe {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
.i-rail-followus__socials {
display: table;
}
.i-rail-followus__tw {
vertical-align: sub;
}
Когда мы смотрим на эту таблицу, то сразу хотим быстро удалить повторяющиеся объявления. Но поскольку мы находимся в самом начале «сушки», не будем отходить от алгоритма и просто спускаемся по таблице сверху вниз, просматривая каждое объявление. Первым является border-color: transparent;
. Оно где-нибудь еще используется? Нет, тогда идём к следующему — border-style: solid;
. Оно уникальное. Тоже самое с border-width: 10px 10px 10px 0;
. Затем height: 0;
. Не уникальное.
Соответственно, наша работа начинается с height: 0;
. Заметим, что это объявление используется в трех различных правилах: .arrow-down
, .faq-list .faq-item-title:after
и .videoWrapper
. Поскольку у нас нет правила, которое включает в себя именно эти четыре селектора, мы копируем их, чтобы сформировать новое правило непосредственно перед тем, в котором мы нашли первое вхождение height: 0;( .arrow-left)
. Другими словам, мы пишем новое правило только для height: 0
;.
.arrow-left,
.arrow-down,
.faq-list .faq-item-title:after,
.videoWrapper {
height: 0;
}
Теперь убираем height: 0;
из всех остальных правил. Поскольку ни одно из них не состояло исключительно из этого объявления, мы не можем удалить их целиком (пока). Обычно я делаю это за один шаг: если нахожу «бесспорный» дубликат, то создаю новое правило, ищу больше вхождений, удаляю объявление, которое нужно переместить, и копирую селектор (ы), который нужно вставить. С опытом всё это становится проще.
Продолжим с правилом .arrow-left
, с объявлением, следующим после height: 0; — width: 0
. Дубликат? Да. И это хорошо, потому что когда мы проверяем, где еще используется width: 0;
, то видим, что оно почти такое же, но не идентичное селекторам, использующим height: 0;
. То есть мы начнем с нового правила для width: 0;
, убедившись, что удалили все предыдущие вхождения:
.arrow-left,
.arrow-down,
.faq-list .faq-item-title:after {
width: 0;
}
Это правило идет после созданного для height: 0;
и перед .arrow-letf;
, которое мы только что проверили и оптимизировали. Я считаю, что результирующий порядок полезен, потому что предпочитаю, чтобы правила располагались в порядке важности и воздействия. А поскольку мы объединяли правила, то сделали их более эффективными.
Давайте проработаем правило .arrow-down;
. В нём нет повторений, хотя его можно описать элегантнее: border: 10px solid transparent; border-top-color: #2b2d32;
.
Продолжим с .faq-list .faq-item-title
. Тут нет дубликатов. На самом деле этот фрагмент таблицы стилей был довольно простым, поэтому мы больше не находим дополнительных совпадений.
.arrow-left,
.arrow-down,
.faq-list .faq-item-title:after,
.videoWrapper {
height: 0;
}
.arrow-left,
.arrow-down,
.faq-list .faq-item-title:after {
width: 0;
}
.arrow-left {
border-color: transparent;
border-style: solid;
border-width: 10px 10px 10px 0;
}
.arrow-down {
border-bottom: 10px solid transparent;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid #2b2d32;
bottom: 0;
left: 20px;
}
.faq-list .faq-item-title {
cursor: pointer;
}
.faq-list .faq-item-title:after {
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid #111;
content: '';
display: inline-block;
float: right;
opacity: .5;
vertical-align: top;
}
#contact input:focus,
#contact textarea:focus {
border: 1px solid #3398db;
}
.flickity-slider>.table {
table-layout: fixed;
}
::selection {
background: #9b59b6;
color: #fff;
}
.videoWrapper {
padding-bottom: 56.25%;
position: relative;
}
.videoWrapper iframe {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
.i-rail-followus__socials {
display: table;
}
.i-rail-followus__tw {
vertical-align: sub;
}
Оптимизация. Пример второй
Для второго примера возьмём eBay. На этот раз мы тоже хотим просто переформатировать (отступы, сортировка объявлений по алфавиту), но снова сталкиваемся с практиками, которых следует избегать (особенно c именами классов или отсутствием резервных шрифтов, но для шрифтов вроде Arial это не является проблемой).
.sh-GifCont {
color: #999;
font-family: Verdana !important;
font-size: 10px;
font-weight: normal;
padding: 0 10px 4px 10px;
}
.sh-GetFastImg {
background-image: url('http: //ir.ebaystatic.com/pictures/aw/pics/de/viewitem/spr4VI.png');
background-position: 0 -178px;
background-repeat: no-repeat;
float: left;
height: 16px;
margin-right: 4px;
width: 56px;
}
.sh-float-l {
float: left;
}
.sh-FrZip {
padding: 10px 0 0 15px;
width: 12%;
}
.sh-FrDelLoc {
padding: 10px 15px 0 10px;
width: 10%;
}
.sh-FrCnt {
padding-left: 10px;
padding-right: 0;
text-align: left;
}
.sh-FrZipCnt {
padding: 0 0 0 15px;
}
.sh-FrDelLocCnt {
padding: 0;
}
.sh-FrBtn {
padding: 5px 15px 10px 8px;
}
.sh-FrDelSite {
padding: 6px 0 0;
}
.vi-frs-sh-FrSlctr {
display: inline;
padding: 6px 15px 0 10px;
}
.sh-FrZipDiv {
display: inline;
padding: 6px 15px 0 0;
}
.sh-FrTxt {
color: #333;
font-family: Arial;
font-size: 12px;
font-weight: normal;
padding-left: 15px;
}
.sh-FrLrnMore {
display: inline;
padding: 10px 10px 10px 15px;
}
.sh-FrQuote {
display: inline;
}
.sh-FrLnk {
margin-top: 5px;
}
.sh-Tbl {
padding: 10px;
}
.sh-TblCnt {
color: #333;
font-family: Arial;
font-size: 12px;
font-weight: normal;
padding-left: 15px;
}
.sh-TblHdr {
color: #5d5d5d;
font-family: Verdana;
font-size: 12px;
font-weight: normal;
padding-left: 15px;
}
.sh-Info {
color: #999;
font-family: Arial;
font-size: 11px;
font-weight: normal;
}
.sh-FrSbTxt {
color: #999;
font-family: arial;
font-size: 11px;
font-weight: normal;
padding-left: 15px;
}
.sh-FreightHdr {
color: #333;
font-family: Verdana !important;
font-size: 10px;
font-weight: normal;
padding: 5px 0 5px 23px;
}
.sh-Freight-Hdr {
color: #333;
font-family: Verdana !important;
font-size: 10px;
font-weight: normal;
padding-left: 13px;
}
.sh-Cnt {
color: #5d5d5d;
font-family: Arial;
font-size: small;
font-weight: normal;
padding-left: 13px;
}
.vi-frs-sh-TxtCnt {
color: #333;
font-family: Arial;
font-size: 12px;
font-weight: normal;
}
.sh-BtnTxt {
color: #333;
font-family: Verdana,Tahoma,Arial;
font-size: 12px;
font-weight: normal;
height: 24px;
margin: 0;
padding: 0 3px;
position: relative;
text-decoration: none;
top: 0;
}
.sh-bubble-position {
float: left;
padding-top: 5px;
}
.sh-del-lrge b {
font-size: 15px;
}
.sh-gspFirstLine {
color: #333;
font-family: Arial;
font-size: 15px;
padding: 25px 10px 5px 0;
}
.sh-gspSecondLine {
color: #777;
font-family: Arial;
font-size: 12px;
padding: 0 10px 15px 0;
}
Первый шаг: color: #999;
был использован более одного раза? Да. Итак, мы сначала создаем собственное правило:
.sh-GifCont {
color: #999;
}
Следует добавить селекторы для двух других вхождений:
.sh-GifCont,
.sh-Info,
.sh-FrSbTxt {
color: #999;
}
Здесь нам также помогает предположение, что таблица стилей уже использует тот порядок выбора, который нам нужен, и что смена правил не вызовет проблем. Полезно иметь четкое представление о порядке выбора и о решении каскадных проблем, не перемещая затронутые правила или не объединяя их.
font-family: Verdana !important;
— это объявление используется трижды. Затем font-size: 10px;
— обратите внимание, важно избегать повторения селекторов: это объявление используется теми же селекторами, что мы сгруппировали для «Verdana»
.
.sh-GifCont,
.sh-FreightHdr,
.sh-Freight-Hdr {
font-family: Verdana !important;
}
.sh-GifCont,
.sh-FreightHdr,
.sh-Freight-Hdr {
font-size: 10px;
}
Резюмируем правила:
.sh-GifCont,
.sh-FreightHdr,
.sh-Freight-Hdr {
font-family: Verdana !important;
font-size: 10px;}
Таким образом мы проработаем всю таблицу стилей, пока не получим:
.sh-GifCont,
.sh-Info,
.sh-FrSbTxt {
color: #999;
}
.sh-GifCont,
.sh-FreightHdr,
.sh-Freight-Hdr {
font-family: verdana !important;
font-size: 10px;
}
.sh-GifCont,
.sh-FrTxt,
.sh-TblCnt,
.sh-TblHdr,
.sh-Info,
.sh-FrSbTxt,
.sh-FreightHdr,
.sh-Freight-Hdr,
.sh-Cnt,
.vi-frs-sh-TxtCnt,
.sh-BtnTxt {
font-weight: normal;
}
.sh-GifCont {
padding: 0 10px 4px 10px;
}
.sh-GetFastImg,
.sh-float-l,
.sh-bubble-position {
float: left;
}
.sh-GetFastImg {
background-image: url('http: //ir.ebaystatic.com/pictures/aw/pics/de/viewitem/spr4VI.png');
background-position: 0 -178px;
background-repeat: no-repeat;
height: 16px;
margin-right: 4px;
width: 56px;
}
.sh-FrZip {
padding: 10px 0 0 15px;
width: 12%;
}
.sh-FrDelLoc {
padding: 10px 15px 0 10px;
width: 10%;
}
.sh-FrCnt {
padding-left: 10px;
padding-right: 0;
text-align: left;
}
.sh-FrZipCnt {
padding: 0 0 0 15px;
}
.sh-FrDelLocCnt {
padding: 0;
}
.sh-FrBtn {
padding: 5px 15px 10px 8px;
}
.sh-FrDelSite {
padding: 6px 0 0;
}
.vi-frs-sh-FrSlctr,
.sh-FrZipDiv,
.sh-FrLrnMore,
.sh-FrQuote {
display: inline;
}
.vi-frs-sh-FrSlctr {
padding: 6px 15px 0 10px;
}
.sh-FrZipDiv {
padding: 6px 15px 0 0;
}
.sh-FrTxt,
.sh-TblCnt,
.sh-FreightHdr,
.sh-Freight-Hdr,
.vi-frs-sh-TxtCnt,
.sh-BtnTxt,
.sh-gspFirstLine {
color: #333;
}
.sh-FrTxt,
.sh-TblCnt,
.sh-Info,
.sh-FrSbTxt,
.sh-Cnt,
.vi-frs-sh-TxtCnt,
.sh-gspFirstLine,
.sh-gspSecondLine {
font-family: arial;
}
.sh-FrTxt,
.sh-TblCnt,
.sh-TblHdr,
.vi-frs-sh-TxtCnt,
.sh-BtnTxt,
.sh-gspSecondLine {
font-size: 12px;
}
.sh-FrTxt,
.sh-TblCnt,
.sh-TblHdr,
.sh-FrSbTxt {
padding-left: 15px;
}
.sh-FrLrnMore {
padding: 10px 10px 10px 15px;
}
.sh-FrLnk {
margin-top: 5px;
}
.sh-Tbl {
padding: 10px;
}
.sh-TblHdr,
.sh-Cnt {
color: #5d5d5d;
}
.sh-TblHdr {
font-family: verdana;
}
.sh-Info,
.sh-FrSbTxt {
font-size: 11px;
}
.sh-FreightHdr {
padding: 5px 0 5px 23px;
}
.sh-Freight-Hdr,
.sh-Cnt {
padding-left: 13px;
}
.sh-Cnt {
font-size: small;
}
.sh-BtnTxt {
font-family: verdana,tahoma,arial;
height: 24px;
margin: 0;
padding: 0 3px;
position: relative;
text-decoration: none;
top: 0;
}
.sh-bubble-position {
padding-top: 5px;
}
.sh-del-lrge b,
.sh-gspFirstLine {
font-size: 15px;
}
.sh-gspFirstLine {
padding: 25px 10px 5px 0;
}
.sh-gspSecondLine {
color: #777;
padding: 0 10px 15px 0;
}
Редактирование
То, что мы рассмотрели выше, может показаться большой сложной работой. Но всё, что для этого нужно, это воля, практика и понимание, что сложная и утомительная работа возникает только тогда, когда вы выполняете ее с полностью несортированными, неоптимизированными таблицами стилей. Как только мы проясним наши стандарты написания кода и шаги по оптимизации, обновлять и поддерживать таблицы стилей становится довольно просто. Достаточно будет перепроверить свои правки.
Давайте также рассмотрим это очень кратко в третьем примере, отрывке из Code Responsible.
h1,
h2 {
color: #000;
font-family: futurastd-book, futura, 'droid sans', 'helvetica neue', helvetica, sans-serif;
font-weight: 400;
line-height: 1.13;
}
h1 {
font-size: 1.86em;
margin: 0 0 .53em;
}
h2 {
counter-increment: counter;
font-size: 1.5em;
margin: 1em 0 0;
}
Здесь мы можем легко изменить правило: предположим, что для h1
нужен другой margin
, к примеру, margin: 1em 0 0;
. Многие сразу поймут, что мы можем и должны сделать. А чтобы показать один из возможных способов, с более сложной таблицей стилей я поступил бы так.
Первое, что нужно сделать, это внести изменение, отметить его (мой редактор — обычно IntelliJ IDEA — показывает изменения и упрощает их поиск, но мне всё равно нравится временно отступать от новых или измененных объявлений) и протестировать:
h1,
h2 {
color: #000;
font-family: futurastd-book, futura, 'droid sans', 'helvetica neue', helvetica, sans-serif;
font-weight: 400;
line-height: 1.13;
}
h1 {
font-size: 1.86em;
margin: 1em 0 0;
}
h2 {
counter-increment: counter;
font-size: 1.5em;
margin: 1em 0 0;
}
Во-вторых, после успешного тестирования я бы проверил все эти измененные строки, не нужно ли их оптимизировать еще раз. Здесь мы обнаружим, что поля для h1
и h2
идентичны.
Следуя способу разбивки на отдельные шаги, создадим правило для h1
и h2
. Но у них уже есть собственное правило, поэтому объединим их в одно. Результат:
h1,
h2 {
color: #000;
font-family: futurastd-book, futura, 'droid sans', 'helvetica neue', helvetica, sans-serif;
font-weight: 400;
line-height: 1.13;
margin: 1em 0 0;
}
h1 {
font-size: 1.86em;
}
h2 {
counter-increment: counter;
font-size: 1.5em;
}
Этим я хотел показать, что поддерживать таблицы гораздо легче, чем оптимизировать их.
Подсказки
Хочу поделиться несколькими советами (поскольку я обновляю статьи на meiert.com, я могу со временем изменить эти советы).
- Поиск без учета регистра. Вполне возможно, что различие регистра является не случайным (некоторые области таблицы стилей, такие как сгенерированный контент или URL-адреса, могут быть чувствительны к регистру). eBay является хорошим примером: независимо от отсутствующих резервных шрифтов, некоторые из используемых объявлений шрифтов пишутся с заглавной буквы по-разному. Например, мы находим
font-family: Arial;
иfont-family: arial;
. Их следует объединить, а значит версия с корректировкой на повторение использует только одно объявление и только в одной нотации (нижний регистр). - Закрывайте каждое (предварительное) объявление точкой с запятой. Это упрощает просто копирование и перемещение объявлений, а также избавляет нас от множества ложных срабатываний (а их будет несколько). Тег
!important
является прекрасным примером: если мы ищем «незакрытые» выражения, тоborder: 0
обязательно совпадёт сborder: 0 !important
, и может появиться бесконечное количество других действительно разных объявлений. Это усложнит нашу работу и повысит вероятность ошибок. - Используйте стандартный порядок выбора. Мы не только ограничиваем энтропию таблицы стилей, но и, поскольку новые правила «автоматически» попадают в четко определенные места, получаем естественную линию защиты от повторения селектора. И мы не хотим, чтобы в статьях, подобных этой, селекторы оставались «сухими». Подумайте, можете ли вы использовать мой дизайн упорядочивания селекторов, тем самым помогая сообществу веб-разработчиков стандартизировать его, или создать свой собственный.
Требования к инструментам
Хотя мы продолжаем изучать эффективность этого подхода и способы его улучшения, есть также несколько задач, решение которых наши инструменты могут облегчить.
- Редакторы могут помочь нам избежать повторяющихся объявлений, выделяя их. Лично я думаю, что небольшой символ »!» в конце строки, настраиваемый в каждом редакторе, был бы замечательным решением, как и возможность игнорировать или отключать уведомления для определенных строк. Это значительно упростило бы работу по оптимизации: не только для отслеживания избыточности, но и для получения представления о том, насколько проблематичен тот или иной случай. некоторых случаях таблицы стилей на 90% состоят из повторений.
- Я уверен, у вас остались вопросы;, но надеюсь, что их осталось меньше, чем раньше, когда мы в течение многих лет пренебрегали этой оптимизацией. С моей точки зрения, DRY CSS позволил бы нам более трезво взглянуть на переменные и другие функции, которые попали в спецификации CSS.
Эти вопросы, конечно, следует принимать во внимание, и я надеюсь, что вы поможете нам всем поработать над ними. Пример с Яндексом показал, что отсутствие повторяющихся объявлений — не панацея. Это помогает сделать CSS компактнее и управляемее, но всё же есть крайние случаи, когда он усложняется. Нам будет полезно изучить эти моменты.
Наконец, я считаю, что избегание повторяющихся объявлений является важным способом работы с CSS. Фактически, я не вижу никакого другого метода оптимизации, помимо создания ваших личных рекомендаций по коду, потому что без систематического подхода к на предотвращению повторения вся наша работа просто увеличивает энтропию, которая не помогает писать качественный код. Но именно к нему мы стремимся как профессионалы. Давайте уделим больше внимания качеству кода и парадигме DRY CSS, и посмотрим, что мы можем автоматизировать, не забывая о своем мастерстве.