Добрый день.
В данной публикации хочу рассмотреть механизм работы стилей в Angular, поделится своим опытом и виденьем архитектуры стилей. Понимание этого позволит писать чистый, структурированный и поддерживаемый код стилей.
Если вы ведете разработку на Angular, уверен не раз встречались с ситуацией, когда применение стилей к селектору не давали ни какого эффекта. Часто это решают выносом стилей в глобальные, применение селектора ::ng-deep
или что еще хуже полным отключением инкапсуляции без понимания механизма его работы. В то время как Angular дает мощный механизм по работе с разделением и инкапсуляцией стилей.
Собственно об этом механизме и сценариях его использования поговорим. Но сначала взглянем на классический способ работы со стилями.
Классический подход к стилям (глобальные стили)
При данном подходе все стили подключаются одним файлом. В этом файле собраны стили всего проекта, всех его страниц, в не зависимости от того, нужны ли они в данный момент. Есть конечно разные подходы по оптимизации и разделения их на несколько, но это не решает большинства проблем данного подхода, которые становятся критическими на средних и больших проектах:
-
Большой размер файла стилей, влекущий за собой:
Увеличение времени загрузки.
Увеличение времени парсинга стилей (особенно в случае их плохого качества). Нужно разобрать стили в зависимости от каскада и специфичности.
-
Мусорный классы:
Загрузка не используемых стилей на данной странице и участие их в парсинге.
При рефакторинге и удалении элементов, стили могут быть не удалены и просто лежать мусором.
-
Неверное использование каскада:
Опора на стили других элементов, не имеющих отношение к текущему. Можно при удалении стилей одного элемента, поломать другие.
-
Возможность пересечение имен классов:
Вероятность при написании стилей к одному элементу, сломать стили других.
!important-hell и прочие методы борьбы за специфичность.
Если первый пункт ведет только к чуть долгой загрузке страницы, что нивелируется оптимизациями браузера и техниками написания и деления стилей. То остальные пункты ведут к потенциальному уменьшению отказоустойчивости системы и увеличению расходов на тестирование.
Компонентный подход и разделение стилей в Angular
В Angular реализован компонентный подход к разделению кода, в том числе и стилей. Стили компонентов как правило находятся в папке компонента, к которому они относятся. Так же имеется файл глобальных стилей, о котором поговорим позже.
Инкапсуляция стилей в компоненте
Стили в Angular описываются непосредственно в компоненте, к которому они относятся.
Стили компонента, по умолчанию инкапсулированы внутри него и не воздействуют на элементы других компонентов. Но Angular позволяет изменять данное поведение через свойство декоратора компонента encapsulation. Есть три варианта:
ViewEncapsulation.Emulated
ViewEncapsulation.None
ViewEncapsulation.ShadowDom
Emulated (по умолчанию)
@Component({
encapsulation: ViewEncapsulation.Emulated
})
Данный тип изолирует стили компонента от остальных путем добавления к селекторам стилей уникального атрибута компонента. Соответствующий атрибут добавляется ко всем элементам компонента.
<p _ngcontent-hip-c18>text</p>
p {
color: red;
}
На выходе получаем следующие стили:
p[_ngcontent-hip-c18] {
color: red;
}
Данный вид инкапсуляции позволяет:
Ограничить взаимное воздействие стилей разных компонентов друг с другом.
Писать более короткие и менее специфичные селекторы.
Решение проблем с специфичностью и !important-hell.
Решить проблему пересечения имен.
Использовать селекторы из спецификации ShadowDOM (:host, :host-context()).
С данным типом инкапсуляции можно использовать специфичный для Angular селектор ::ng-deep
.
None
@Component({
encapsulation: ViewEncapsulation.None
})
Данный тип отключает инкапсуляцию у компонента. На элементы не вешаются атрибуты, к селекторам не добавляются атрибуты.
В результате стили становятся глобальными, как если бы они сразу были прописаны в глобальном файле стилей. И можно подумать, что это не плохой способ прописать глобальных стилей в случае необходимости. И при этом они лежат в папке компонента к которому относятся. Но нет! Кроме того, что стили компонента хранятся в папке с компонентом, это влечет только проблемы.
Если все же нужны глобальные стили, нужно делать это явно. Они загрузятся сразу и их поведение прогнозируемо.
Ввиду компонентной работы Angular стили компонента подгружаются, только при рендеринге компонента и при определенных обстоятельствах в разном порядке. Что может повлечь за собой трудно отлаживаемые и тестируемые баги, завязанные на пользовательский путь. Что сделает поведение стилей просто не предсказуемым. Так как "в каскаде победит последний из сильнейших".
На этом конечно можно построить какую-нибудь интересную логику стилизации или пасхалок компонента завязанную на пользовательский путь. Но мы сейчас не касаемся магии.
ShadowDom
@Component({
encapsulation: ViewEncapsulation.ShadowDom
})
Данный тип реализует честную инкапсуляцию через ShadowDOM. На самом деле при Emulated под капотом Angular она тоже присутствует и уже поверх нее он собирает стили компонента.
По документации рекомендуется применять только для библиотек. Но есть нюансы.
В одну и туже областьShadowDOM могут попадать стили и из Emulated . Что в случае пересечения имен это может привести к традиционной битве стилей, результат которой известен "в каскаде победит последний из сильнейших" .
Это не правильное поведение вызвано тем что, под капотом Angular при создание компонента в любом случае использует ShadowDom и кастомный компонент с открытым контекстом (иначе до него не возможно было бы достучаться и взаимодействовать).
Селектор ::ng-deep
Это мощный инструмент, позволяющий точечно отключать инкапсуляцию у селектора. Но необходимо понимать для чего и как его применять, иначе это может стать источником трудно отслеживаемых ошибок и не прогнозируемого поведения.
Не смотря на мощь данного селектора, он делает очень простую вещь. При компиляции на селекторы вложенные в ::ng-deep
не вешаются уникальный атрибут компонента. Это делает данные стили глобальными.
div {
color: blue;
}
::ng-deep {
p {
color: red;
}
}
На выходе получаем следующие стили:
div[_ngcontent-hip-c18] {
color: blue;
}
p {
color: red;
}
Основной и по моему мнению единственный сценарий применения - это изменение стилей дочернего компонента из родительского.
Но это плохая практика, так как источник изменения должен быть один. Но если это требуется, то лучше рассмотреть возможность расширения интерфейса компонента, на который требуется воздействовать. Для этого есть как минимум два способа:
Привязаться к наличию родительского класса через селектор :host-context().
Более функциональный способ (можно завязать не только стили но и логику), расширить интерфейс компонента через декоратор @Input.
Единственный вариант, в котором можно использовать ::ng-deep
, это воздействие на вложенные компоненты сторонних библиотек не предоставляющих интерфейса для их кастомизации.
В этом случае согласно документации, нужно оборачивать ::ng-deep
в :host
или другие селекторы, на которых имеется инкапсуляция. Это ограничит распространение глобального стиля только данным компонентом.
Если же вы пытаетесь через него воздействовать не на дочерний компонент (на пример диалог, который обычно рендерится в самом низу DOM-дерева), то стоит эти стили вынести в глобальные стили явно.
Глобальные стили
В Angular имеется файл для глобальных стилей, styles.css.
В глобальные стили рекомендую выносить:
Основные стили проекта (глобальные css-переменные, сброс стилей).
Стили компонентов, которые могут дублироваться и быть вызваны в любой момент в любом месте проекта (например стили форм).
Стили сторонних библиотек, место и контекст рендеринга которых нет возможности контролировать.
Заключение
В публикации я описал мой взгляд на функционал работы со стилями и механизм работы в Angular. Это позволит строить осмысленную и отказоустойчивую архитектуру стилей. Тезисно повторю основные моменты:
По максимуму инкапсулируйте стили компонентов.
В глобальные стили помещаются действительно общие стили для всего сайта, так-же туда могут попасть стили сторонних библиотек.
Для применения
::ng-deep
должна быть веская причина. Он не должен становится "швейцарским ножом" в тех случаях когда не удается изменить стили другого компонента. Angular предоставляет достаточно способов, что-бы можно было обойтись и без него.
MherArsh
Привет, спасибо за статью!
У меня такой вопрос - а как быть с повторениями? У меня есть 2 компоненты где специфичная стилизация таблиц, 90% кода совпадает, этого недостаточно чтобы их вывести в глобальной стиль но и непонятно как объединит общую часть стилей, и ещё вопрос - а нужно ли? Ведь это разные компоненты по сути.
Хотелось бы услышать ваше мнение.
constantos Автор
Добрый день!
Я бы предложил сделать миксин или отдельный файл с повторяющимися стилями и подключать его:
В стилевом файле компонента (если это миксин)
В массиве стилей компонента (если это отдельный файл)
Но если у вас помимо стилей совпадаем еще также и разметка с логикой, я бы сделал из общей части отдельный компонент.
MherArsh
Для построения UI использую библиотеку, в некоторых случаях приходится изменять стили вложенных элементов компоненты (то есть нет возможности непосредственно изменить сам компонент), логика где компонент это одно целое очень хорошая, но ведет к повторению. Тоже думал на тему чтобы использовать миксины, хотя - опять таки, сложно вывести какую то общую часть в стилях не говорю уже об общей компоненте. Видимо с этим только жить ))
Спасибо!