Мифический Angular - Миф Angular: стили компонентов

RedDeveloper
15.02.2023 20:16
Мифический Angular - Миф Angular: стили компонентов

Мифический Angular - Миф Angular: стили компонентов

Это очень короткая и интересная для меня тема. В Angular каждый компонент может иметь свои собственные прикрепленные стили. Стили могут находиться в одном файле или в отдельных файлах, подобно шаблонам.

Кроме того, мы можем инкапсулировать стили. Существует два метода инкапсуляции: Эмулированный и ShadowDom. В этой статье описаны оба типа инкапсуляции, а также их влияние на производительность всего приложения.

Использование стилей вместо styleUrls полезно для производительности

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

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

Во-первых, я создал два компонента с кодом, как показано ниже.

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. Результаты были следующими:

  • ViewEncapsulation.Emulated = EmulatedEncapsulationDomRenderer2
  • ViewEncapsulation.None = DefaultDomRenderer2
  • ViewEncapsulation.ShadowDom = ShadowDomRenderer

EmulatedEncapsulationDomRenderer2

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, потому что вы не можете знать имя атрибута до сборки.

Как я уже сказал, это очень умный способ инкапсуляции стилей.

ShadowDomRenderer

Другой подход к инкапсуляции заключается в использовании 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 известно несколько вещей:

  • Существует глобальный объект set, который хранит все стили, добавленные в приложение.
  • Стили всегда фильтруются с использованием этого набора, поэтому Angular никогда не будет добавлять один и тот же стиль дважды.
  • В конце метода вызывается onStylesAdded, который вызовет одну из реализаций "браузера" или "сервера".

К сожалению, вы не можете самостоятельно использовать 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 значительно медленнее, возможно, это причина, почему он не очень популярен в моем окружении.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?

20.08.2023 18:21

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

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией

20.08.2023 17:46

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

Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox

19.08.2023 18:39

Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.

Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest

19.08.2023 17:22

В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для чтения благодаря своей простоте. Кроме того, мы всегда хотим проверить самые последние возможности в наших проектах!

Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️

18.08.2023 20:33

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

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL

14.08.2023 14:49

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