Angular

Познакомьтесь с Angular Elements

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

Если вы не знакомы с Angular Elements, я крайне рекомендую почитать про него. Вкратце, Angular Elements —  это приложение Angular, использующее стандарт веб-компонентов, позволяя приложению Angular определяться как HTML-тег. Angular Elements от приложения Angular отличает способ начальной загрузки:

export class AppModule {
  constructor(private injector: Injector) { }

  ngDoBootstrap(): void {
    const applicationWrapper = createCustomElement(AppComponent, { injector: this.injector });
    customElements.define('custom-element', applicationWrapper);
  }
}

Angular отвечает за создание оболочки приложения, которая в дальнейшем используется при регистрации пользовательского элемента в браузере. Функция customElements.define() регистрирует приложение Angular с конкретным пользовательским элементом custom-element. Все, что вам нужно, это добавить <custom-element> </ custom-element> к разметке страницы, и Angular приложение отобразится внутри этого тега.

customElements.define() — это часть стандарта ECMAScript и поддерживается всеми крупными браузерами.

Этот подход открывает новые горизонты в использовании приложений Angular. Я расскажу, как использовать Angular Elements с расширениями Chrome. Часто суть подобных расширений в том, чтобы расширить представление страницы путем добавления HTML-тега. С Angular Elements добавление и обработка элементов страницы становятся довольно простыми.

Специфика расширений Chrome

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

Контекст страницы и контекст расширения
Контекст страницы и контекст расширения

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

  • контекст страницы: есть доступ к объекту windowи элементам DOM, но нет доступа к Chrome API;
  • контекст расширения: есть независимый объект window, который не имеет ничего общего с объектом windowработающей страницы. Нет доступа к странице javascript, но можно манипулировать DOM и вызывать Chrome API.

Так как загружать скрипты в каждом из контекстов?

Для выполнения скрипта в контексте страницы он должен быть загружен следующим образом:

let s = document.createElement('script');
s.type = 'text/javascript';
s.src = chrome.extension.getURL('angular.elements.bundle.js');
s.onload = function() {
    this.parentNode.removeChild(this);
};
try {
    (document.head || document.documentElement).appendChild(s);
} catch (e) {
    console.log(e);
}

Когда мы добавляем тег <script /> к заголовку страницы, браузер загружает и исполняет скрипт в том же контексте, что и загруженная страница.

Чтобы запустить файл скрипта в контексте расширения, нужно указать его в manifest.json:

{
  "content_scripts": [
    {
      "js": [
        "contentScript.js"  
      ]
    }
  ]
}

Таким образом файл, указанный в content_scripts, будет загружен в контексте расширения.

Angular Elements должны быть запущены в контексте страницы, иначе браузер просто не увидит зарегистрированный веб-компонент. Это происходит потому, что в контексте расширения есть выделенный объект window, отличный от объекта windowзагруженной страницы. Если загрузить Angular Elements с использованием content_scripts в manifest.json, веб-компонент будет зарегистрирован в неверном объекте window. При анализе HTML-разметки браузер найдет элемент <custom-element />, но не сможет найти подходящий для него веб-компонент.

Загрузка Angular Elements в расширении Chrome

Обычно приложение Angular компилируется в несколько бандлов, что может быть неудобно при использовании расширений Chrome — каждый файл нужно будет загружать отдельно. Эту проблему можно решить, скомпилировав приложение Angular внутрь файла javascript. Можно использовать npm пакеты ngx-build-plus илиconcat.

Итак, один файл bundle можно загрузить на страницу. Как уже упоминалось, файл загружается добавлением <script /> к странице, поэтому нужно указать местонахождение файла bundle в атрибуте src. Теперь bundle принадлежит расширению Chrome, он загрузится из файловой системы. По умолчанию такая загрузка скрипта запрещена и требует дополнительной настройки в файле manifest.json :

{
  "content_scripts": [
    {
      "js": [
        "angularBundleWrap.js"  
      ]
    }
  ],
  "web_accessible_resources": [
    "angular.elements.bundle.js"
  ]
}

Web_accessible_resources разрешает доступ к файлам вне расширения. Добавив файл bundle в этот массив, мы сможем внедрить его на страницу, используя тег <script />.

Такой конфигурации будет достаточно, чтобы использовать Angular Elements в расширении Chrome (<custom-element /> — это Angular Elements):

Angular Elements рендерится внутри тега custom-element

Какие существуют проблемы?

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

Если запустить расширения Chrome на сайте, написанном на Angular, можно увидеть, что Angular Elements не загружается, а в html-разметке — пустой <custom-element />. Почему так происходит?

Angular CLI использует WebPack, который создает глобальную переменную webpackJsonp со скомпилированным приложением внутри:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["main"],{

Так как Angular Elements — это полноценное приложение Angular, оно тоже компилируется с помощью Webpack и использует ту же глобальную переменнуюwebpackJsonp. При загрузке Angular Elements bundle используется уже существующая переменнаяwebpackJsonp, которая смешивается с двумя приложениями. Проблема в том, что метод ngDoBootstrap() не вызывается для Angular Elements и, следовательно, не инициализируется.

В репозитории Angular на GitHub есть тикет, описывающий такую же проблему. В качестве решения предлагается переименовать переменную webpackJsonp для Angular Elements.

Этого можно добиться, настроив конфигурационный файл webpack или просто переименовав переменную webpackJsonp в итоговом bundle. Оба решения работают и позволяют загружать Angular Elements на странице Angular.

Вы используете `window.ng`?

Если для отладки приложений вы используете объект window.ng, в частности ng.probe(), будьте готовы, что этот мощный инструмент перестанет работать. Причина в том, что ng принадлежит BrowserModule и создается, когда инициализируется этот модуль:

Объект отладки ng object — часть BrowserModule

Так как ng — глобальный объект, он перезаписывается, когда Angular Elements загружается на страницу. Значит, ng.probe будет работать, но не так, как вы ожидаете: с ng вы сможете запустить отладку только приложения Angular Elements.

Плохая новость в том, что невозможно избежать создания ELEMENT_PROBE_PROVIDERS, даже enableProdMode() не сработает.

Когда ng.probe перестанет работать, все расширения Chrome, использующие ng, тоже перестанут работать, в том числе и Augury.

За и против использования Angular Elements в расширениях Chrome

За 👍

  • простой путь добавления пользовательского html-кода к странице;
  • удобство в разработке благодаря полноценному приложению Angular со всеми преимуществами;
  • минимум кода на стороне расширений Chrome.

Против 👎

  • ограничение при использовании на сайтах, написанных на Angular;
  • вероятность поломки других расширений при использовании ng.probe().

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


Пример проекта можно посмотреть здесь.

Перевод статьи Oleksandr Reznichenko: Using Angular-Elements in a Chrome Extension