Хабрахабр

[Из песочницы] Сборка библиотеки angular-компонентов в виде веб-компонентов

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

Ура, компонент работает!.. Но, как правило, эти материалы ограничиваются рассмотрением довольно утопичной ситуации: мы делаем отдельный проект, создаем angular-компонент, настраиваем проект на сборку Elements и, наконец, компилируем несколько JS-файлов, подключение которых к обычной странице даст нам необходимый результат.

image

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

Подготовка модулей

Для начала давайте вспомним, как должен выглядеть модуль для компиляции Angular Elements.

@NgModule({ imports: [BrowserModule], entryComponents: [SomeComponent],
})
export class AppModule ); customElements.define('some-component', ngElement); } ngDoBootstrap() {}
}

Нам необходимо:

  1. Добавить в entryComponents компонент, который мы планируем сделать angular-элементом, импортировать необходимые для компонента модули.
  2. Создать angular-элемент с помощью createCustomElement и инжектора.
  3. Объявить веб-компонент в customElements браузера.
  4. Переопределить метод ngDoBootstrap на пустой.

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

export abstract class MyElementModule { constructor(injector: Injector, component: InstanceType<any>, name: string) { const ngElement = createCustomElement(component, { injector, }); customElements.define(`${MY_PREFIX}-${name}`, ngElement); } ngDoBootstrap() {}
}

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

@NgModule({ imports: [BrowserModule, MyButtonModule], entryComponents: [MyButtonComponent],
})
export class ButtonModule extends MyElementModule { constructor(injector: Injector) { super(injector, MyButtonComponent, 'button'); }
}

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

Таким образом мы можем создать несколько модулей и по очереди собирать из них веб-компоненты. Модуль готов к сборке и выдаст нам набор JS-файлов, которые можно сложить в отдельный веб-компонент.

Собираем несколько компонентов

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

Структура элементов выходит примерно такой:

image

А отдельный файл компиляции в самом простом варианте будет выглядеть так:

enableProdMode(); platformBrowserDynamic() .bootstrapModule(ButtonModule) .catch(err => console.error(err));

Такой подход поможет легко обходить заготовленные модули и поддерживать структуру проекта с Elements понятной и простой.

В настройках билда angular.json укажем путь собранного файла в некую временную папку внутри dist:

"outputPath": "projects/elements/dist/tmp"

Туда будет падать набор выходных файлов после сборки модуля.

Для самой сборки воспользуемся обычной командой build в angular-cli:

ng run elements:build:production --main='projects/elements/src/${project}/${component}/compile.ts'

Отдельный элемент будет финальным продуктом, поэтому включаем флаги production с Ahead-of-Time-компиляцией, а после подставляем путь к исполняемому файлу, который состоит из проекта и названия компонента.

Для этого воспользуемся обычным cat’ом: Теперь соберем полученный результат в отдельный файл, который и будет финальным бандлом нашего отдельного веб-компонента.

cat dist/tmp/runtime.js dist/tmp/main.js > dist/tmp/my-${component}.js

Тут важно заметить, что мы не закладываем файл polyfills.js в бандл каждого компонента, потому что получим дублирование, если будем использовать несколько компонентов на одной странице в дальнейшем. Разумеется, стоит отключать опцию outputHashing в angular.json.

Например, так: Получившийся бандл перенесем из временной папки в папку для складирования компонентов.

cp dist/tmp/my-${component}.js dist/components/

Осталось только собрать всё воедино — и скрипт компиляции готов:

// compileComponents.js projects.forEach(project => { const components = fs.readdirSync(`src/${project}`); components.forEach(component => compileComponent(project, component));
}); function compileComponent(project, component) { const buildJsFiles = `ng run elements:build:production --aot --main='projects/elements/src/${project}/${component}/compile.ts'`; const bundleIntoSingleFile = `cat dist/tmp/runtime.js dist/tmp/main.js > dist/tmp/my-${component}.js`; const copyBundledComponent = `cp dist/tmp/my-${component}.js dist/components/`; execSync(`${buildJsFiles} && ${bundleIntoSingleFile} && ${copyBundledComponent}`);
}

Теперь у нас есть аккуратная папочка с набором веб-компонентов:

image

Подключаем компоненты на обычную страницу

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

<my-elements-input id="input">Поле ввода</<my-elements-input> <script src="my-input.js"></script>

Чтобы не тащить весь zone.js с каждым компонентом, мы единожды подключаем его в начале документа:

<script src="zone.min.js"></script>

Компонент отображается на странице, и всё хорошо.

А давайте добавим еще и кнопку:

<my-elements-button size="l" onclick="onClick()">Кнопка</my-elements-button> <script src="my-button.js"></script>

Запускаем страничку и…

image

Ой, всё сломалось!

Если мы заглянем в бандл, то обнаружим там такую неприметную строчку:

window.webpackJsonp=window.webpackJsonp||[]

image

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

Для решения этой проблемы нам необходимо использовать custom-webpack:

  1. Добавляем custom-webpack к проекту с elements:

    ng add @angular-builders/custom-webpack --project=elements

  2. Конфигурируем angular.json:

    "builder": "@angular-builders/custom-webpack:browser", "options": { "customWebpackConfig": { "path": "./projects/elements/elements-webpack.config.js" },
    ...

  3. Создаем файл с конфигурацией custom-webpack:

    module.exports = { output: { jsonpFunction: 'myElements-' + uuidv1(), library: 'elements', },
    };

    В нем нам необходимо генерировать уникальные id для каждой сборки любым удобным способом. Я воспользовался uuid.

Можно снова запускать скрипт сборки — новые компоненты отлично уживаются друг с другом на одной страничке.

Наводим красоту

В наших компонентах используются глобальные CSS-переменные, задающие цветовую тему и размеры компонентам.

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

// compileHelpers.js
compileMainTheme(); function compileMainTheme() { const pathFrom = `../../main-project/styles/themes`; const pathTo = `dist/helpers`; execSync( `lessc ${pathFrom}/theme-default-vars.less ${pathTo}/main-theme.css`,; );
}

Мы используем less, поэтому просто компилируем наши переменные lessc и кладем получившийся файл в папку helpers.

Такой подход позволяет управлять стилизацией всех веб-компонентов страницы без необходимости их перекомпиляции.

Финальный скрипт

Фактически весь описанный выше процесс сборки элементов можно свести к набору действий:

#!/bin/sh rm -r -f dist/ &&
mkdir -p dist/components &&
node compileElements.js &&
node compileHelpers.js &&
rm -r -f dist/tmp

Осталось только вызывать этот скрипт из основного package.json, чтобы свести весь процесс компиляции актуальных angular-компонентов к запуску одной команды.

Все описанные выше скрипты, а также демо странички использования компонентов angular и нативных компонентов, можно найти на github.

Итого

Мы организовали процесс, при котором добавление нового веб-компонента занимает буквально пару минут, сохраняя при этом структуру основных angular-проектов, из которых они берутся.

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть