
Это очень короткая и интересная для меня тема. В Angular каждый компонент может иметь свои собственные прикрепленные стили. Стили могут находиться в одном файле или в отдельных файлах, подобно шаблонам.
Кроме того, мы можем инкапсулировать стили. Существует два метода инкапсуляции: Эмулированный и ShadowDom. В этой статье описаны оба типа инкапсуляции, а также их влияние на производительность всего приложения.
Прежде чем мы приступим к "сложным" вещам, я хочу упомянуть один из мифов, связанных с местом, куда можно поместить стиль.
Несколько раз я слышал, что стили нужно хранить в файле с логикой и шаблоном. Этот миф легко проверить и подтвердить.
Во-первых, я создал два компонента с кодом, как показано ниже.
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-styles',
template: `<p>styles works!</p>`,
styles: [`
.app-styles {
display: flex;
}
`]
})
export class StylesComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}Я не вижу смысла вставлять сюда содержимое файла ./style-urls.component.css. Поверьте, что он выглядит почти так же, как и класс .app-styles css, как в коде выше. Это не так важно для нашего исследования.
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-style-urls',
template: `<p>style-urls works!</p>`,
styleUrls: ['./style-urls.component.css']
})
export class StyleUrlsComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}Ничего особенного, верно? Итак, после этого я отключил минификацию артефактов в файле angular.json и запустил ng build. Эта команда должна выдать javascript-код в директории /dist, и для нас наиболее интересным файлом является main.js.
Используя вашу IDE, вы можете поискать строки со словами StylesComponent и StyleUrlsComponent.
StylesComponent.ɵcmp = /*@__PURE__*/ _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵdefineComponent"]({ type: StylesComponent, selectors: [["app-styles"]], decls: 2, vars: 0, template: function StylesComponent_Template(rf, ctx) { if (rf & 1) {
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](0, "p");
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](1, " styles works! ");
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"]();
} }, styles: [".app-styles[_ngcontent-%COMP%] {\n display: flex;\n }"] });
// ...
StyleUrlsComponent.ɵcmp = /*@__PURE__*/ _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵdefineComponent"]({ type: StyleUrlsComponent, selectors: [["app-style-urls"]], decls: 2, vars: 0, template: function StyleUrlsComponent_Template(rf, ctx) { if (rf & 1) {
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](0, "p");
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](1, " style-urls works! ");
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"]();
} }, styles: [".app-style-urls[_ngcontent-%COMP%] {\n display: flex;\n}"] });Ага, тот же результат для стилей и styleUrls. Нужны ли нам еще доказательства того, что это миф и стили не быстрее, чем styleUrls...?
Теперь мы можем углубиться в тему инкапсуляции.
Чтобы проверить различия между типами инкапсуляции, я использую тот же метод, что и в предыдущей главе этой статьи. Я сгенерировал три компонента, по одному для каждого возможного значения из перечня ViewEncapsulation, изменил настройки в angular.json и проверил результат.
export enum ViewEncapsulation {
Emulated = 0,
// Historically the 1 value was for `Native` encapsulation which has been removed as of v11.
None = 2,
ShadowDom = 3
}После сборки javascript должен выглядеть как в приведенном ниже фрагменте кода.
NoEncapsulationComponent.ɵcmp = /*@__PURE__*/ _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵdefineComponent"]({ type: NoEncapsulationComponent, selectors: [["app-no-encapsulation"]], decls: 2, vars: 0, template: function NoEncapsulationComponent_Template(rf, ctx) { if (rf & 1) {
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](0, "p");
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](1, " no-encapsulation works! ");
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"]();
} }, styles: ["\n .app-no-encapsulation {\n display: flex;\n }\n "], encapsulation: 2 });
// ...
EmulatedComponent.ɵcmp = /*@__PURE__*/ _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵdefineComponent"]({ type: EmulatedComponent, selectors: [["app-emulated"]], decls: 2, vars: 0, template: function EmulatedComponent_Template(rf, ctx) { if (rf & 1) {
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](0, "p");
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](1, " emulated works! ");
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"]();
} }, styles: [".app-emulated[_ngcontent-%COMP%] {\n display: flex;\n }"] });
// ...
ShadowComponent.ɵcmp = /*@__PURE__*/ _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵdefineComponent"]({ type: ShadowComponent, selectors: [["app-shadow"]], decls: 2, vars: 0, template: function ShadowComponent_Template(rf, ctx) { if (rf & 1) {
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](0, "p");
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](1, " shadow works! ");
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"]();
} }, styles: ["\n .app-shadow {\n display: flex;\n }\n "], encapsulation: 3 });Самое заметное отличие - это, конечно, часть [_ngcontent-%COMP%]. Она была сгенерирована компилятором, но почему...?
Чтобы проверить это, мы снова должны вернуться к классу Renderer2. Если вы новичок, посмотрите мой предыдущий пост о нем.
Для каждого компонента я проверил реализацию Renderer2. Результаты были следующими:
OK. Мы знаем, что для каждого компонента с инкапсуляцией, установленной на Emulated, Angular генерирует то, чего не было в исходном коде. Из CSS мы знаем, что добавленная часть делает так, что элементы с классом app-emulated и атрибутом будут использовать этот стиль. Это выглядит подозрительно, но это не более чем HTML-атрибут со странным названием.
.app-emulated[_ngcontent-%COMP%] {
display: flex;
}Зная это, мы можем ожидать, что Angular добавит этот атрибут во время выполнения. На самом деле, так оно и есть. Посмотрите код EmulatedEncapsulationDomRenderer2, который используется фреймворком для компонентов эмулированной инкапсуляции.
Две части этого кода важны. Во-первых, в конструкторе класс генерирует свойство contentAttr. Это поле используется позже в переопределенном методе createElement, каждый элемент, созданный этой реализацией Renderer2, будет добавлять этот специальный атрибут по умолчанию.
class EmulatedEncapsulationDomRenderer2 extends DefaultDomRenderer2 {
private contentAttr: string;
private hostAttr: string;
constructor(
eventManager: EventManager, sharedStylesHost: DomSharedStylesHost,
private component: RendererType2, appId: string) {
super(eventManager);
const styles = flattenStyles(appId + '-' + component.id, component.styles, []);
sharedStylesHost.addStyles(styles);
this.contentAttr = shimContentAttribute(appId + '-' + component.id);
this.hostAttr = shimHostAttribute(appId + '-' + component.id);
}
applyToHost(element: any) {
super.setAttribute(element, this.hostAttr, '');
}
override createElement(parent: any, name: string): Element {
const el = super.createElement(parent, name);
super.setAttribute(el, this.contentAttr, '');
return el;
}
}Я ответил, где находится код, отвечающий за добавление этого атрибута, но у нас остался один важный вопрос: ПОЧЕМУ?
В принципе, все очень просто. Angular добавляет этот атрибут в CSS код и во время выполнения к компонентам, это очень умный и оригинальный способ убедиться, что добавленный стиль будет применен только к элементам из области видимости приложения Angular. Вы просто не можете использовать его где-то вне приложения, например, непосредственно в файле index.html, потому что вы не можете знать имя атрибута до сборки.
Как я уже сказал, это очень умный способ инкапсуляции стилей.
Другой подход к инкапсуляции заключается в использовании Shadow DOM. Честно говоря, возможно, я живу в очень однообразной среде, или это очень редко используется, потому что я видел использование этой стратегии всего несколько раз. Вся идея использует Shadow DOM API браузера, прямо сейчас; согласно caniuse.com почти все современные браузеры поддерживают его.
ShadowDomRenderer в конструкторе прикрепляет тень к хосту, а затем добавляет стили к созданному корню тени. Нет необходимости создавать странные именованные атрибуты или что-то в этом роде, просто немного кода javascript.
class ShadowDomRenderer extends DefaultDomRenderer2 {
private shadowRoot: any;
constructor(
eventManager: EventManager, private sharedStylesHost: DomSharedStylesHost,
private hostEl: any, component: RendererType2) {
super(eventManager);
this.shadowRoot = (hostEl as any).attachShadow({mode: 'open'});
this.sharedStylesHost.addHost(this.shadowRoot);
const styles = flattenStyles(component.id, component.styles, []);
for (let i = 0; i < styles.length; i++) {
const styleEl = document.createElement('style');
styleEl.textContent = styles[i];
this.shadowRoot.appendChild(styleEl);
}
}
private nodeOrShadowRoot(node: any): any {
return node === this.hostEl ? this.shadowRoot : node;
}
override destroy() {
this.sharedStylesHost.removeHost(this.shadowRoot);
}
override appendChild(parent: any, newChild: any): void {
return super.appendChild(this.nodeOrShadowRoot(parent), newChild);
}
override insertBefore(parent: any, newChild: any, refChild: any): void {
return super.insertBefore(this.nodeOrShadowRoot(parent), newChild, refChild);
}
override removeChild(parent: any, oldChild: any): void {
return super.removeChild(this.nodeOrShadowRoot(parent), oldChild);
}
override parentNode(node: any): any {
return this.nodeOrShadowRoot(super.parentNode(this.nodeOrShadowRoot(node)));
}
}Я решил не заострять внимание на этой стратегии, потому что, как я уже говорил, я не уверен, что она очень популярна.
Если вы хотите потратить немного времени на изучение работы Shadow DOM, вот вам очень хорошая статья из MDN; developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM .
Еще одна интересная часть стилизации, которую стоит знать, - это часть добавления стилей на страницу силами фреймворка. Вы когда-нибудь задумывались, как это делает Angular? Загружаются ли стили с нетерпением, используя тег HTML-ссылки? Может быть, это какая-то динамическая/ленивая загрузка?
Чтобы успокоить вас, конечно, стили компонентов не загружаются с нетерпением. Это было бы потенциально легко нарушить производительность.
И снова я собираюсь вставить некоторый код из источников Angular :) Пожалуйста, простите меня за это, но я все еще думаю, что вы не должны доверять мне. Помните: "Доверяйте коду".
На этот раз мы снова начнем с EmulatedEncapsulationDomRenderer2. В конструкторе у нас очень мало инжектируемых вещей, и одним из аргументов является экземпляр класса DomSharedStylesHost. В третьей строке конструктора вызывается метод addStyles. Это наша точка входа в исследование.
class EmulatedEncapsulationDomRenderer2 extends DefaultDomRenderer2 {
private contentAttr: string;
private hostAttr: string;
constructor(
eventManager: EventManager, sharedStylesHost: DomSharedStylesHost,
private component: RendererType2, appId: string) {
super(eventManager);
const styles = flattenStyles(appId + '-' + component.id, component.styles, []);
sharedStylesHost.addStyles(styles);
this.contentAttr = shimContentAttribute(appId + '-' + component.id);
this.hostAttr = shimHostAttribute(appId + '-' + component.id);
}
// ...
}И, конечно, код для него можно найти в исходниках. Важно знать, что DomSharedStylesHost расширяет класс SharedStylesHost, есть еще один класс, который расширяет его ServerStylesHost. Можно предположить, что для конфигурации рендеринга на стороне сервера эта операция реализована по-другому.
Как всегда, я призываю вас проверить его, но сейчас я сосредоточусь на браузерной версии, потому что это, вероятно, наиболее часто используемая реализация.
@Injectable()
export class DomSharedStylesHost extends SharedStylesHost implements OnDestroy {
// Maps all registered host nodes to a list of style nodes that have been added to the host node.
private _hostNodes = new Map<Node, Node[]>();
constructor(@Inject(DOCUMENT) private _doc: any) {
super();
this._hostNodes.set(_doc.head, []);
}
private _addStylesToHost(styles: Set<string>, host: Node, styleNodes: Node[]): void {
styles.forEach((style: string) => {
const styleEl = this._doc.createElement('style');
styleEl.textContent = style;
styleNodes.push(host.appendChild(styleEl));
});
}
addHost(hostNode: Node): void {
const styleNodes: Node[] = [];
this._addStylesToHost(this._stylesSet, hostNode, styleNodes);
this._hostNodes.set(hostNode, styleNodes);
}
removeHost(hostNode: Node): void {
const styleNodes = this._hostNodes.get(hostNode);
if (styleNodes) {
styleNodes.forEach(removeStyle);
}
this._hostNodes.delete(hostNode);
}
override onStylesAdded(additions: Set<string>): void {
this._hostNodes.forEach((styleNodes, hostNode) => {
this._addStylesToHost(additions, hostNode, styleNodes);
});
}
ngOnDestroy(): void {
this._hostNodes.forEach(styleNodes => styleNodes.forEach(removeStyle));
}
}Итак, давайте начнем с конструктора. Здесь инжектируется только один объект. Это глобальный объект документа. Почему он типизирован как any...? Возможно, есть причина, но я ее не знаю и не хочу создавать новый миф об этом.
В классе DomSharedStylesHost методы _addStylesToHost и onStylesAdded используются для добавления нового <style> элемента с контентом стилей, когда это необходимо.
Но когда это необходимо...? Отличный вопрос!
Angular должен добавить стиль, когда был вызван метод addStyles (обратитесь к конструктору EmulatedEncapsulationDomRenderer2). Этот метод реализован в базовом классе SharedStylesHost.
Посмотрите код ниже.
@Injectable()
export class SharedStylesHost {
/** @internal */
protected _stylesSet = new Set<string>();
addStyles(styles: string[]): void {
const additions = new Set<string>();
styles.forEach(style => {
if (!this._stylesSet.has(style)) {
this._stylesSet.add(style);
additions.add(style);
}
});
this.onStylesAdded(additions);
}
onStylesAdded(additions: Set<string>): void {}
getAllStyles(): string[] {
return Array.from(this._stylesSet);
}
}Из реализации метода whe известно несколько вещей:
К сожалению, вы не можете самостоятельно использовать SharedStylesHost для добавления стилей "вручную". Этот класс не является частью публичного API Angular, и он не экспортируется.
В конце концов, код очень прост, и вы можете реализовать его в своем проекте. Зачем...? Хороший вариант использования - когда вы хотите загружать стили динамически с помощью кода и динамических импортов webpack.
Я описал все стили инкапсуляции, предоставляемые фреймворком Angular. После прочтения тонн кода пришло время сравнить производительность каждого метода.
Как всегда, я создаю репозиторий с глупым простым приложением, которое можно использовать для тестирования. Вы можете найти его здесь github.com/galczo5/experiment-angular-encapsulation .
Чтобы понять тест, необходимо разобраться в компонентах. Ниже я вставил код компонента AppComponent и его шаблона.
Чтобы хорошо протестировать его, я решил запустить его в цикле со 100 итерациями; каждая итерация отрисовывает 10 000 компонентов. Похоже, этого достаточно для получения удовлетворительных результатов.
import {ChangeDetectionStrategy, ChangeDetectorRef, Component} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
type: 'clear' | 'emulated' | 'shadow' | 'none' = 'clear';
array = new Array(10000).fill(0);
constructor(
private readonly changeDetectorRef: ChangeDetectorRef
) {
}
render(type: 'clear' | 'emulated' | 'shadow' | 'none'): void {
let testTime = 0;
for (let i = 0; i < 100; i++) {
const start = performance.now();
this.type = type;
this.changeDetectorRef.detectChanges();
const end = performance.now();
testTime += (end - start);
this.type = 'clear';
this.changeDetectorRef.detectChanges();
}
console.info('TOTAL', testTime);
console.info('AVG', testTime / 100);
}
}И, конечно же, шаблон:
<h1>Styles encapsulation</h1>
<div>
<h2>Emulated</h2>
<app-emulated></app-emulated>
</div>
<div>
<h2>None</h2>
<app-no-encapsulation></app-no-encapsulation>
</div>
<div>
<h2>ShadowDom</h2>
<app-shadow></app-shadow>
</div>
<div>
<h2>Test - 100 * Create 10000 components</h2>
<button (click)="render('clear')">Clear</button>
<button (click)="render('emulated')">Emulated</button>
<button (click)="render('none')">None</button>
<button (click)="render('shadow')">ShadowDom</button>
</div>
<div *ngIf="type === 'emulated'">
<app-emulated *ngFor="let x of array"></app-emulated>
</div>
<div *ngIf="type === 'shadow'">
<app-shadow *ngFor="let x of array"></app-shadow>
</div>
<div *ngIf="type === 'none'">
<app-no-encapsulation *ngFor="let x of array"></app-no-encapsulation>
</div>Если вы не хотите проводить тесты на своем компьютере, вот мои результаты. Если вы хотите сделать это, не забудьте открыть dev tools. Там будет выведено время теста.

Разницы между эмулированной и отключенной инкапсуляцией практически нет. Разница между ShadowDom и остальными типами инкапсуляции огромна.
Меня немного пугает, что я написал статью об инкапсуляции и ее производительности, где тест составляет очень малую часть всего текста. Я просто хотел хорошо описать различия :D

Подводя итог, можно сказать, что инкапсуляция Emulated по умолчанию работает так же быстро, как и без инкапсуляции вообще. Похоже, что это можно объяснить реализацией. В конце концов, это всего лишь умный способ использования родных селекторов атрибутов css. Способ ShadowDom значительно медленнее, возможно, это причина, почему он не очень популярен в моем окружении.

20.08.2023 18:21
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в 2026-2027 годах? Или это полная лажа?".

20.08.2023 17:46
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.

19.08.2023 18:39
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.
19.08.2023 17:22
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для чтения благодаря своей простоте. Кроме того, мы всегда хотим проверить самые последние возможности в наших проектах!

18.08.2023 20:33
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий их языку и культуре.

14.08.2023 14:49
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип предназначен для представления неделимого значения.