Если вы разработчик JavaScript, вы, вероятно, слышали о веб-компонентах. Если вы не знакомы с ними, веб-компоненты позволяют определять пользовательские компоненты HTML с помощью JavaScript. Вы когда-нибудь хотели создать тег ‹my-cat /›, который обернет его содержимое рамкой с анимированными танцующими котиками? С веб-компонентами вы можете - и они будут работать в любом браузере, независимо от того, используете вы фреймворк или нет.

Идея компонентной веб-разработки не нова. Он существует почти столько же, сколько и Интернет. Это заманчивое предложение; вместо создания индивидуального HTML для каждого сайта, который вы создаете; Компоненты дают возможность создавать повторно используемые строительные блоки.

Краткая история компонентов в веб-разработке

Первым широко используемым компонентом для создания веб-приложений стала программа NeXT Software WebObjects, которая появилась в 1996 году. Несколько лет спустя на рынок вышли как JavaServer Faces, так и веб-формы ASP.NET. Хотя все эти фреймворки генерируют HTML на стороне сервера и отправляют его в браузер, пользователи современных интерфейсных фреймворков найдут концепции WebObjects, JSF и Web Forms очень знакомыми.

В конце концов, по мере развития браузеров и расширения возможностей движков JavaScript разработчики начали экспериментировать с созданием одностраничных приложений (SPA). Вместо создания HTML на стороне сервера разработчики начали генерировать его на стороне клиента в браузере. Одной из первых компонентных клиентских фреймворков, появившихся в 2006 году, стал Google Web Toolkit - обычно называемый просто GWT. GWT был парой вещей в одном: это был компонентный фреймворк, а также Java-to -JavaScript компилятор.

В 2010 году появились более популярные компонентные фреймворки: BackboneJS и KnockoutJS. В отличие от GWT, это были чистые JavaScript-фреймворки. В 2012 году появился AngularJS, и именно тогда популярность клиентских компонентных фреймворков резко возросла. Через пару лет появились React и Vue, и для многих веб-разработчиков компонентный SPA стал способом разработки новых веб-приложений по умолчанию.

Однако у всех этих клиентских фреймворков есть недостаток: компоненты, созданные в одном фреймворке, не могут быть легко использованы в другом фреймворке. Во многих случаях это не проблема; компания, которая выбирает фреймворк, обычно использует его для всех своих приложений. Но для компаний, проектов с открытым исходным кодом и отдельных лиц, которые хотят создавать многократно используемые библиотеки интерфейсных компонентов, широкий спектр популярных фреймворков представляет проблему: как сделать компоненты, которые может использовать кто угодно, не дублируя свою работу для каждой фреймворка, который вы хотите. поддерживать?

Веб-компоненты предлагают решение. Это встроенный в браузер способ определения пользовательских HTML-элементов, которые можно использовать в любой платформе - или даже без какой-либо внешней среды. Мы будем изучать, как использовать веб-компоненты в Angular 8. Для начала мы рассмотрим создание пары простых веб-компонентов.

Предпосылки

В оставшейся части статьи предполагается, что вы знакомы с современной разработкой на Angular. Мы будем использовать Angular 8, но если вы знакомы с Angular 4+, у вас не возникнет проблем с его выполнением.

Мы также предполагаем, что вы хотя бы немного прочитали о том, как работают веб-компоненты. Хотя мы будем создавать наши собственные веб-компоненты для использования в приложении Angular, у нас не будет времени глубоко погрузиться в веб-компоненты, прежде чем мы начнем.

К счастью, сайт спецификаций веб-компонентов предоставляет отличное введение в основы создания и использования веб-компонентов. В отличие от большинства спецификаций программного обеспечения, этой легко следовать; он все объясняет простым английским языком и включает множество примеров кода.

Создание веб-компонентов

Прежде чем мы сможем поместить веб-компоненты в наше приложение Angular, нам понадобятся некоторые веб-компоненты. Хотя мы могли импортировать что-то из существующей библиотеки компонентов, что это было бы забавно?

Мы собираемся создать два простых компонента счетчика. Один будет обязательным компонентом. У этого компонента будет объектно-ориентированный API, который нам придется вызывать для увеличения или уменьшения счетчика.

Второй компонент будет компонентом декларативного счетчика. Этот компонент имеет значение счетчика, передаваемое через атрибут. Он полагается на это внешнее значение и не может увеличивать или уменьшать его значение. Хотя это может показаться бесполезным, декларативные компоненты распространены в случаях, когда компонент отвечает только за отображение переданных ему данных. Wijmo Gauge - хороший пример этого типа компонентов.

Теперь перейдем к коду нашего пользовательского компонента. Чтобы упростить выполнение этого упражнения, мы поместили весь код в проект StackBlitz, чтобы вы могли просматривать и запускать его в своем веб-браузере. Для начала давайте взглянем на файл ImperativeCounter.ts, расположенный в папке src / app / web-components.

class ImperativeCounter extends HTMLElement {
private readonly shadow: ShadowRoot;
private currentCount: number = 0;
constructor() {
super();
this.shadow = this.attachShadow({ mode: 'open'});
this.update();
}
update() {
const template = `
<style>
.counter {
font-size: 25px;
}
</style>
<div class="counter">
<b>Count:</b> ${this.currentCount}
</div>
`;
this.shadow.innerHTML = template;
}
increment(){
this.currentCount++;
this.update();
}
decrement() {
this.currentCount--;
this.update();
}
}
window.customElements.define('i-counter', ImperativeCounter);

Как и все веб-компоненты, этот начинается с расширения HTMLElement. Мы должны это сделать, иначе браузер не позволит нам зарегистрировать компонент.

Затем мы создаем две переменные экземпляра: shadow, в котором хранится теневой DOM веб-компонента, и currentCount, в котором хранится текущее значение нашего счетчика.

Затем наш конструктор создает теневой DOM и сохраняет его в тени. Конструктор завершается вызовом метода обновления.

В обновлении мы определяем шаблон HTML для нашего элемента, который включает значение currentCount. Затем мы назначаем нашу строку шаблона свойству innerHTML нашего теневого DOM.

Наш класс императивного счетчика завершается определением методов увеличения и уменьшения. Эти методы просто увеличивают или уменьшают значение currentCount, а затем вызывают update, чтобы убедиться, что мы показываем новое значение currentCount в HTML нашего компонента.

Вне объявления класса мы вызываем window.customElements.define, чтобы зарегистрировать наш блестящий новый компонент в браузере. Без этого шага никто не смог бы использовать наш компонент счетчика.

Теперь давайте посмотрим на компонент декларативного счетчика. Обратите внимание, что это похоже на императивный счетчик:

class DeclarativeCounter extends HTMLElement {
private readonly shadow: ShadowRoot;
private currentCount: number = 0;
constructor() {
super();
this.shadow = this.attachShadow({ mode: 'open'});
}
static get observedAttributes() {
return ['count']
}
connectedCallback() {
this.currentCount = parseInt(this.getAttribute('count')) || 0;
this.update();
}
attributeChangedCallback(attrName, oldVal, newVal) {
this.currentCount = newVal;
this.update();
}
update() {
const template = `
<style>
.counter {
font-size: 25px;
}
</style>
<div class="counter">
<b>Count:</b> ${this.currentCount}
`;
this.shadow.innerHTML = template;
}
}
window.customElements.define('d-counter', DeclarativeCounter);

Как и в случае с императивным счетчиком, мы начинаем с определения переменных экземпляра для нашего теневого корня DOM и текущего значения счетчика.

Конструктор похож, но мы не вызываем метод обновления. Это потому, что мы хотим подождать и посмотреть, было ли значение передано в компонент через атрибут count. Это значение не будет доступно во время вызова конструктора компонента. Вместо этого мы используем метод жизненного цикла веб-компонента: connectedCallback.

Этот метод вызывается после того, как компонент был вставлен в DOM. Если мы посмотрим на метод connectedCallback нашего компонента, мы увидим, что он считывает значение атрибута count компонента и использует его для установки значения currentCount. Затем он вызывает update для рендеринга компонента.

Просматривая компонент, мы увидим еще две вещи, которые отличаются от императивного счетчика: статическое свойство с именем ObservatedAttributes и метод attributeChangedCallback. Это обе части API веб-компонентов.

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

Метод attributeChangedCallback вызывается браузером всякий раз, когда изменяется один из атрибутов, перечисленных в attributeChangedCallback. В нашем случае count - единственный атрибут, для которого мы будем получать уведомления. Итак, когда вызывается метод, мы обновляем currentCount новым значением, а затем вызываем update для повторного рендеринга компонента.

Использование наших компонентов в Angular

Теперь, когда мы создали наши компоненты, пришло время использовать их в Angular. После всей проделанной работы использовать наши веб-компоненты в Angular будет так просто, что это покажется антиклиматическим!

В проекте StackBlitz откройте app.module.ts. Вы увидите следующее:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
imports:      [ BrowserModule, FormsModule ],
declarations: [ AppComponent ],
bootstrap:    [ AppComponent ],
schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
})
export class AppModule { }

В целом, это очень похоже на простой модуль приложения, который Angular CLI сгенерирует для вас. Однако есть несколько важных отличий, поэтому давайте их рассмотрим.

Прежде всего следует отметить, что мы импортируем CUSTOM_ELEMENTS_SCHEMA из @ angular / core. Angular использует схемы, чтобы определить, какие имена элементов разрешены внутри модуля. Импорт схемы настраиваемых элементов необходим, потому что без нее компилятор шаблонов Angular сообщит об ошибке, когда обнаружит имя элемента, которое не понимает.

Также обратите внимание, что мы добавили свойство schemas в декоратор NgModule, чтобы сообщить Angular, что нужно использовать схему настраиваемых элементов в нашем модуле приложения.

Затем посмотрите в main.ts и обратите внимание, что мы добавили два импорта:

import "./app/web-components/ImperativeCounter.ts";
import "./app/web-components/DeclarativeCounter.ts";

Эти строки импортируют созданные нами компоненты счетчика и регистрируют их в браузере. Этот шаг важен, потому что без него мы не смогли бы использовать компоненты в нашем приложении Angular!

Давайте посмотрим на app.component.html, чтобы увидеть, как используются наши веб-компоненты:

<h5>Imperative Counter</h5>
<div class="counter-box">
<i-counter #iCounter></i-counter>
</div>
<h5>Declarative Counter</h5>
<div class="counter-box">
<d-counter [count]="count" #dCounter></d-counter>
</div>
<button (click)="increment()" class="btn btn-primary">Increment</button>
<button (click)="decrement()"class="btn btn-primary">Decrement</button>

Как видите, мы визуализируем наши веб-компоненты, используя имена тегов, которые мы зарегистрировали в браузере: i-counter и d-counter. Для декларативного счетчика мы также привязываем его начальный атрибут count к значению переменной count нашего компонента.

Вы увидите, что мы также добавили кое-что к нашим компонентам: #iCounter и #dCounter. Как мы вскоре увидим, эти теги позволяют нашему компоненту Angular получать прямые ссылки на эти элементы, чтобы мы могли их обновить.

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

Теперь перейдем к app.component.ts:

import { Component, ElementRef, ViewChild  } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
private count: number = 0;
@ViewChild("iCounter") iCounter: ElementRef;
@ViewChild("dCounter") dCounter: ElementRef;
increment() {
this.count++;
this.iCounter.nativeElement.increment();
this.dCounter.nativeElement.setAttribute("count", this.count);
}
decrement() {
this.count--;
this.iCounter.nativeElement.decrement();
this.dCounter.nativeElement.setAttribute("count", this.count);
}
}

Начнем с импорта Component, ElementRef и ViewChild из @ angular / core. Нам нужны ElementRef и ViewChild, потому что нам нужно будет напрямую манипулировать нашими веб-компонентами, чтобы изменять их значения. Хотя это немного неудобнее, чем работать с собственными компонентами Angular, это легко сделать.

Внутри класса мы добавляем переменную экземпляра для хранения текущего значения счетчика; затем мы используем декоратор ViewChild для получения ElementRefs для наших двух веб-компонентов с помощью тегов, которые мы только что видели в шаблоне. Экземпляры ElementRef позволяют напрямую использовать базовые собственные элементы. В нашем случае эти элементы являются узлами DOM.

Далее у нас есть методы увеличения и уменьшения. Вот где происходит волшебство. В обоих методах мы изменяем переменную count компонента, а затем обновляем наши веб-компоненты, чтобы использовать новое значение. В случае императивного счетчика мы вызываем его методы увеличения и уменьшения. Что касается декларативного счетчика, мы используем метод DOM setAttribute для обновления значения счетчика.

Вот и все! У нас есть полностью функционирующее приложение Angular, использующее наши вручную созданные веб-компоненты. Вы можете увидеть окончательный результат в действии здесь.

Заключение

На этом мы завершили обзор веб-компонентов и их использования в приложении Angular.

Теперь вы можете добавлять веб-компоненты во все свои приложения Angular, и ваши пользователи будут любить вас даже больше, чем они уже любили! Вы больше не ограничены использованием компонентов Angular в своих приложениях. Хотя Angular Material великолепен, вы можете обнаружить, что один из Материальных веб-компонентов Google больше подходит для вашего приложения.