Это очень короткая и интересная для меня тема. В 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 в 2023-2024 годах? Или это полная лажа?".
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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип предназначен для представления неделимого значения.