Angular + веб-компоненты: полное руководство
В этой статье вы узнаете, как включить один компонент Angular внутри страницы с помощью Angular elements. Мы подробно рассмотрим, как это работает, и как отлаживать приложение с помощью веб-компонентов.
В прошлом году мне было поручено включить несколько компонентов Angular в существующее приложение ASP.Net. Уже было реализовано беспорядочное решение (гигантский скрипт, который загружал классическое приложение Angular со всеми компонентами, независимо от их соответствия текущей странице), которое, очевидно, было «немного» неэффективным с точки зрения скорости. И, конечно же, этот подход был адом с точки зрения процесса разработки программного обеспечения, который требовал, чтобы я запускал сценарий Gulp, перестраивал приложение ASP.Net и жестко перезагружал страницу каждый раз, когда я менял крошечный бит CSS. Команда не была довольна ни производительностью приложения, ни процессом разработки, поэтому моей целью было
- Создайте новое решение, которое позволит включить один компонент Angular внутри страницы, а не все приложение.
- Работал бы быстрее и не загружал огромные ненужные скрипты.
- Это позволит ускорить сборку разработчика без необходимости постоянного повторного запуска скрипта.
Поэтому, естественно, я обратился к веб-компонентам.
Но что такое веб-компоненты?
Веб-компоненты - это концепция компонентов из таких фреймворков, как React или Angular, но построенных для Интернета в целом, независимо от фреймворка. Что это значит?
Мы привыкли к нашим простым тегам HTML в нашем пользовательском интерфейсе. Например, мы знаем, такие теги как div
, span
и другие. Они имеют предопределенное поведение и могут использоваться в разных местах нашего проекта.
Веб-компоненты по существу позволяют нам создавать новые теги / элементы HTML с помощью JavaScript. Давайте посмотрим на небольшой пример того, как это делается исключительно с помощью JavaScript.
class SpanWithText extends HTMLSpanElement {
constructor() {
super();
this.innerText = `Hello, I am a Web Component!`;
}
}
customElements.define('span-with-text', SpanWithText);
Здесь мы создаем класс (точно так же, как мы пишем класс для компонентов в Angular), который расширяется от HTMLElement
(в нашем случае HTMLSpanElement
, если быть более точным), и всякий раз, когда этот элемент создается, он будет выполнять innerText со значением Hello, I am a Web Component
. Итак, всякий раз, когда мы используем наш элемент в DOM, внутри него уже будет заполненный текст.
<span-with-text></span-with-text>
Круто, могу ли я использовать его с Angular?
Конечно, было бы здорово иметь возможность превратить наши компоненты Angular в веб-компоненты и использовать их где угодно, независимо от среды. Оказывается, с помощью @ angular/elements мы можем сделать именно это.
Как это работает:
Перво-наперво, мы собираемся запустить новое приложение Angular. Мы собираемся создать его со специальным флагом, чтобы он создавал только файлы конфигурации, а не шаблонный код (AppModule / AppComponent и т.д.).
ng new web-components --createApplication=false
Теперь давайте создадим наш первый веб-компонент; внутри нашего нового каталога проекта:
ng generate application FirstWebComponent --skipInstall=true
Добавьте этот флаг, чтобы пропустить переустановку любых зависимостей.
Итак, эта команда создает папку projects
внутри нашего корневого каталога, и в ней будет папка с именем FirstWebComponent
. Если мы заглянем внутрь, мы увидим типичное приложение Angular: файлы main.ts
, app.module
, app.component
и другие. Теперь нам нужно получить библиотеку @angular/elements
, поэтому мы запускаем:
ng add @angular/elements
Это перенесет библиотеку в нашу папку node_modules
, чтобы мы могли использовать ее для превращения наших компонентов в веб-компоненты.
Важно понимать, что компоненты Angular превращены в веб-компоненты и являются простыми компонентами Angular - ничто в них не должно отличаться для использования в качестве веб-компонентов. Вы можете сделать что - либо внутри них, что вы бы делали с обычным Angular компонентом, и он будет работать правильно. Любые шаги по настройке, которые необходимо выполнить, чтобы заставить ваши компоненты работать, выполняются на уровне модуля; в самих компонентах ничего не меняется. Это означает, что вы можете без особых хлопот превратить практически любой существующий компонент Angular в веб-компонент.
На следующем этапе давайте создадим компонент Angular, который станет нашим веб-компонентом; внутри нашего нового проекта:
ng generate component UIButton
Теперь нам нужно заставить Angular понять, что мы не хотим, чтобы он рассматривал этот компонент как обычный компонент Angular, а скорее как нечто иное. Это делается на уровне начальной загрузки модуля - что нам нужно сделать, так это реализовать метод ngDoBootstrap
в нашем AppModule
и сообщить нашему модулю, чтобы он определил настраиваемый элемент. Для этого воспользуемся функцией createCustomElement
из пакета @angular/elements
.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, DoBootstrap, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';
@NgModule({
declarations: [
UIButtonComponent,
],
imports: [
BrowserModule,
],
entryComponents: [UIButtonComponent],
})
export class AppModule implements DoBootstrap {
constructor(private injector: Injector) {
const webComponent = createCustomElement(UIButtonComponent, {injector});
customElements.define('ui-button', webComponent);
}
ngDoBootstrap() {}
}
Обратите внимание на несколько важных моментов:
- Мы должны передать в
injector
наш веб-компонент вручную. Это необходимо для обеспечения работы внедрения зависимостей во время выполнения. - Мы должны поместить наш компонент в массив
entryComponents
. Это необходимо для начальной загрузки веб-компонента. createCustomElement
- это функция, которая фактически превращает наш собственный компонент в веб-компонент - мы передаем результат этой функцииcustomElements.define
, а не нашему компоненту Angular.- Селектор нашего компонента Angular не имеет значения. То, как он будет вызываться из другого шаблона HTML, определяется строкой, которую мы передаем функции
customElements.define
. В этом случае он будет называться<ui-button></ui-button>
. - Селектор, который мы передаем функции
customElements.define
, должен состоять из двух или более слов, разделенных тире. Это скорее требование API пользовательских элементов (чтобы он мог отличать пользовательские элементы от собственных HTML-тегов), чем причуда самого Angular.
Наш следующий шаг - создание приложения!
ng build FirstWebComponent
Заглянув внутрь папки dist
, мы увидим следующее:
Это файлы, которые нам нужно включить в другое приложение, чтобы иметь возможность использовать наш веб-компонент. Для этого мы можем скопировать эти файлы в другой проект и включить его разными способами:
1. Если мы собираемся использовать его в приложении React, мы можем установить полифилл, а затем включить наши файлы с помощью простых операторов import
.
2. Чтобы использовать его в простом HTML-приложении, мы должны включить все наши сгенерированные файлы через теги скрипта, а затем использовать компонент:
<html>
<head>
<script src="./built-files/polyfills.js"></script>
<script src="./built-files/vendor.js"></script>
<script src="./built-files/runtime.js"></script>
<script src="./built-files/styles.js"></script>
<script src="./built-files/scripts.js"></script>
</head>
<body>
<ui-button></ui-button>
</body>
</html>
3. Чтобы использовать его в другом приложении Angular, нам действительно не нужно создавать компонент; поскольку это простой компонент Angular, мы можем просто импортировать и использовать его. Но если у нас есть доступ только к скомпилированным файлам, мы можем включить их в поле scripts
в нашем файле angular.json
в целевой проект и добавить schemas: [CUSTOM_ELEMENTS_SCHEMA]
, в наш файл app.module.ts
.
Итак, первая часть была легкой. Несколько замечаний:
1. Если у нас есть @Input
внутри нашего веб-компонента, шаблон именования изменяется, когда мы его используем. Мы используем camelCase в нашем компоненте Angular, но для доступа к этому вводу из другого файла HTML нам нужно будет использовать kebab-case:
@Component({/* metadata */})
export class UIButtonComponent {
@Input() shouldCountClicks = false;
}
<ui-button should-count-clicks="true"></ui-button>
2. Если у нас есть @Output
внутри нашего компонента Angular, мы можем прослушивать генерируемые синтетические события через addEventListener
. Другого пути нет, в том числе и React - обычный on<EventName={callback}>
не сработает, так как это синтетическое мероприятие.
Но как насчет совместимости браузера?
Пока все хорошо - мы создали наше приложение и успешно протестировали его в крупном современном браузере, например Chrome. Но как насчет проклятия всех веб-разработчиков - IE? Если мы откроем наше приложение, в котором мы используем веб-компонент, мы увидим, что наша кнопка со счетчиком успешно отрисована. Итак, мы в порядке? Не совсем.
Если бы у нас была функция, которая заставляет компонент подсчитывать клики по кнопке, то после того, как мы щелкнем в Internet Explorer, мы увидим, что счетчик не обновляется. Чтобы избавить вас от дальнейших исследований, проблема в том, что обнаружение изменений в веб-компонентах Angular не работает в IE. Итак, должны ли мы забыть о веб-компонентах в IE? Нет, к счастью, есть простой обходной путь. Мы должны либо реализовать новую стратегию зоны обнаружения изменений, либо импортировать ее. Есть небольшой пакет с тем, что мы хотим:
npm i elements-zone-strategy
И немного изменим наш файл app.module.ts
:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, DoBootstrap, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';
@NgModule({
declarations: [
UIButtonComponent,
],
imports: [
BrowserModule,
],
entryComponents: [UIButtonComponent],
})
export class AppModule implements DoBootstrap {
constructor(private injector: Injector) {
const strategyFactory = new ElementZoneStrategyFactory(UIButtonComponent, injector);
const webComponent = createCustomElement(UIButtonComponent, {injector, strategyFactory});
customElements.define('log-activity', webComponent);
}
ngDoBootstrap() {}
}
Здесь мы просто вызвали конструктор ElementZoneStrategyFactory
, получили новый strategyFactory
и передали его функции createCustomElement
вместе с injector
.
Теперь вы можете открыть тот же компонент в IE и - удивительно - он работает! Обнаружение изменений теперь работает в IE, как и ожидалось.
Не забываем про полифиллы
Вы будете регулярно посещать этот файл polyfills.ts
. Мой совет - настраивайте и не загружайте сразу все полифиллы, а только те, которые необходимы.
Как мне отлаживать?
Отладка (и сообщения об ошибках в целом) - это небольшая проблема в веб-компонентах Angular. У вас нет горячей перезагрузки, и вы запускаете свои встроенные компоненты в другой среде, а не в приложении Angular, поэтому вы можете задаться вопросом, как вы могли бы их получить. Фактически, вы можете немного изменить файл index.html
и поставить селектор своего веб-компонента вместо app-root
:
Затем запустите приложение как обычно:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>UIButton</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<ui-button></ui-button>
</body>
</html>
ng serve UIButton
Это создаст приложение Angular, которое содержит только ваш веб-компонент. Положительным моментом является то, что у вас будет горячая перезагрузка (это большое дело). Но есть и обратная сторона: сообщения об ошибках часто перевариваются: например, если вы забыли предоставить один из своих сервисов, в обычном приложении Angular вы получите ошибку с StaticInjectorError
подробным описанием того, какой сервис не была предоставлен. В нашем случае нам просто будет представлен пустой экран и никаких ошибок - загадка, которую нужно исследовать и из-за которой можно расстраиваться.
Вывод
Эта базовая настройка дает нам все необходимое для создания и развертывания приложений, использующих компоненты Angular и веб-компоненты. Но эта установка далека от идеала - нам бы хотелось иметь такие скрипты как:
- Автоматическое создание новых веб-компонент
- Скрипты для автоматического запуска нашего процесса сборки
- Горячая перезагрузка