Концепция плагинов всегда была популярной и продуктивной в области разработки программного обеспечения. Масштабируемость и возможность коллективной разработки необходимы для приложений уровня предприятия, когда каждая команда разработчиков может представлять своё направление бизнеса и разрабатывать свой собственный плагин для веб-портала. Angular, являясь одним из наиболее подходящих фреймворков для построения больших браузерных информационных систем, предлагает прекрасные возможности разделения приложения на части прямо «из коробки» при помощи технологии ленивой загрузки. Результатом этого должно быть приемлемое время загрузки даже больших приложений при использовании должного подхода к их дизайну. При этом стандартная реализация ленивой загрузки подразумевает жёсткое задание маршрутов в центральной части приложения, что может приводить к трудностям с разработкой несколькими командами и невозможности добавления новых модулей без изменений в коде. В этой статье я собираюсь объяснить и продемонстрировать как можно создать приложение Angular, поддерживающее плагины, описанные исключительно в конфигурационном файле, когда центральная часть приложения и подключаемые модули полностью независимы друг от друга и даже могут быть расположены в разных местах. Чтобы сделать хорошую иллюстрацию использованного подхода, я написал минимально возможный пример приложения, который расположен здесь. Он был создан и протестирован на Angular 11. Начнём с создания приложения Angular. Последующие команды создадут 2 библиотеки, которые мы можем использовать в качестве плагинов. Первый плагин будет содержать в себе 2 компонента, тогда как второй будет только с одним простым компонентом. Нам надо изменить стандартный файл main-screen.component.html чтобы иметь возможность отображать второй компонент, а также картинку, с использованием базового пути модуля для того, чтобы её загрузить, последнее будет более детально объяснено позднее. Второй компонент может быть прост, насколько это возможно. Нам также надо добавить модули маршрутизации в плагины, эти модули будут отвечать за отображение необходимых компонентов, когда пользователь будет перемещаться по маршруту плагина, определённому в центральной части приложения. В нашем демо-приложении у нас будет маршрут по умолчанию, отображающий главный компонент библиотеки, и дочерний маршрут для отображение дочернего компонента. Эти маршруты добавлены в импорт модуля маршрутизации стандартным образом. А модуль маршрутизации, в свою очередь, добавлен в импорт главного модуля библиотеки. Поскольку плагин содержит картинку, нам нужно создать специальную папку assets и изменить соответствующий файл ng-package.json для того, чтобы сказать компилятору как ему следует вести себя с данной папкой. Второй плагин должен быть подготовлен походим образом. Как только это будет сделано, мы можем перейти на главную часть приложения. Вполне понятно, что центральная часть приложения и плагины будут иметь какой-то общий код. В первую очередь это должен быть сервис для загрузки и хранения файла конфигурации плагинов. Также нам надо задать injection token для хранения базового пути плагина и пару интерфейсов. Шаги, приведённые ниже, будут иллюстрировать создание общей библиотеки. Интерфейс, описывающий плагин, должен быть объявлен как показано ниже. Второй интерфейс используется для задания настроек окружения. Это позволит вам создавать множество окружений, имеющих свою конфигурацию, что полезно, потому что вам может понадобиться задать несколько место расположения плагинов. Сейчас мы уже можем создать сервис конфигурации плагинов. Цель сервиса – хранение данных конфигурации плагинов, которые должны быть единожды загружены сразу после старта приложения. Наконец, нам надо задать injection token базового пути модуля чтобы облегчить загрузку объектов, таких как картинки, в плагинах. Немного теории. Когда мы создаём библиотек Angular, у ней всегда есть зависимости. Эти зависимости должны быть заданы в файле приложения package.json в одноимённом разделе. Если вы активно используете компоненты сторонних производителей, список зависимостей может быть довольно внушительным. По умолчанию Angular не включает собственный код зависимости в откомпилированную библиотеку. Это может выглядеть не вполне понятным, но это сделано для того, чтобы уменьшить общий размер приложения и рассчитать оптимальный вариант ленивой загрузки. Есть опция, позволяющее обойти этот принцип, которая называется ‘budledDependencies’, но далеко не всегда будет хорошей идеей её использовать, потому что вам может понадобиться обращаться к одним и тем же библиотекам из нескольких плагинов. В этом случае приложению может понадобиться загружать один и тот же код каждый раз с загрузкой нового плагина. Поскольку основная часть приложения должна предоставлять эти зависимости плагинам, она должна их заранее импортировать. К сожалению, это может привести к значительному увеличению размера исполняемого файла начальной загрузки. Это, в свою очередь, будет замедлять загрузку приложения. Принимая во внимание соображения выше, было бы разумно применить ленивую загрузку к зависимостям, когда это возможно. Angular предоставляет нам такую возможность. Нам только необходимо создать новый модуль в центральной части приложения, который будет импортировать все зависимости, и применять к нему ленивую загрузку, когда нам необходимо получить доступ к плагинам. Теперь мы можем перейти к наиболее интересной части и запрограммировать модуль загрузки плагинов. Модуль должен содержать файл конфигурации зависимостей. Ниже представлен пример такого файла. Как правило, вы должны включить в файл все зависимости, которые могут иметь ваши плагины. Если здесь окажутся такие, которые не импортированы главным модулем, они будут по крайней мере подвергнуты ленивой загрузке, что может оказать положительное влияние на время загрузки приложения. Самая главная часть модуля загрузчика плагинов – это его конфигурация маршрутов, расположенная в файле plugin-loader-routing.module.ts. Начнём с функции загрузки плагина. Функция попросту загружает файл плагина, создаёт объект экспорта, который будет содержать экспорт загруженного модуля, переопределяет ключевое слово ‘require’ и вызывает функцию eval JavaScript. Если плагину необходима зависимость, не заданная в конфигурационном файле, будет отображена ошибка в консоли браузера, которая позволит вам понять, как решить проблему. Сам модуль маршрутизации содержит конфигурацию роутера, которая определяет, какой плагин должен быть загружен и выполняет загрузку с помощью вызова функции ‘loadModule’. Как вы можете видеть, мы вынуждены прибегнуть к injection token ROUTES, потому что нам нужен доступ к сервису конфигурации плагинов, который содержит информацию о конфигурации плагинов, загруженную при запуске приложения. Это будет разъяснено немного позже. Также нам надо задать пути для маршрутов, потому что они должны быть заданы в корневой конфигурации роутера главного модуля, если мы хотим использовать простые маршруты, такие как ‘http://localhost:4260/plugin1’. Неплохой способ определения, какой плагин должен быть загружен, состоит в использовании параметров функции URL matcher. Конечно же, модуль маршрутизации загрузчика плагинов должен быть импортирован модулем загрузчика плагинов. В приложении нам необходимо загрузить JSON-файл конфигурации плагинов и задать конфигурацию корневого маршрутизатора. Поскольку мы обязаны загрузить конфигурацию ранее всего остального, injection token APP_INITIALIZER является неплохим вариантом, чтобы сделать эту работу. Ваши файлы конфигурации окружения и плагинов должны выглядеть приблизительно как примеры ниже. Конфигурационные файлы окружения являются стандартными и создаются автоматически Angular CLI. Можно расположить файл конфигурации плагинов в папке assets приложения. К сожалению, APP_INITIALIZER не предотвращает запуск конфигурации роутера до окончания инициализации, так что мы вынуждены опять использовать injection token ROUTES. В этом примере у нас нет корневых маршрутов помимо плагинов, так что массив staticRoutes остаётся пустым. Обычно он включает ваши статические (т. е. жёстко заданные) маршруты. App component, очевидно, должен быть изменён. Нам надо внедрить туда сервис конфигурации плагинов как зависимость. HTML-шаблон компонента нуждается в некоторых изменениях для отображения ссылок на плагины, которые заданы в конфигурационном файле плагинов. Когда плагин загружен, часто необходимо знать его URL чтобы иметь возможность загружать ресурсы такие как картинки, видео, файлы локализации и т. д. По умолчанию базовый путь будет такой же, как и у модуля, загрузившего плагин. Это неприемлемо, и мы должны предоставить модулю верную информацию о его базовом URL. Насколько вы помните, мы уже создали injection token под названием MODULE_BASE_PATH. Добавим его по модуль плагина №1 для иллюстрации того, каким путём это можно сделать. В первую очередь нам надо создать функцию-фабрику. Потом добавим нового провайдера в декоратор модуля. Injection token добавлен. Он может быть внедрён в компоненты плагина при помощи добавления его в параметры конструктора класса, как это показано ниже. Чтобы загрузить картинку вам надо просто использовать внедрённое значение В HTML-шаблоне компонента. Для того, чтобы запустить приложение, мы должны настроить конфигурационные файлы. Вы можете поменять порт по умолчанию, используемый с командой ng serve, в файле angular.json. Путь настройки – projects.ng-plugin-demo.architect.serve.options.port. Немного больше изменений нужно сделать с файлом package.json. Для того, чтобы запущенное локально приложение могло загружать плагины, надо установить HTTP-сервер. Чтобы это сделать, выполните следующую команду. После установки он будет автоматически добавлен к секции devDependencies. Довольно удобно добавить новую команду в раздел scripts. Наконец, было бы прекрасно в той же секции назначить единую команду чтобы компилировать всё приложение целиком, включая библиотеки. Теперь, для того, чтобы запустить демо, вам нужно открыть 2 окна терминала и запустить команды ‘npm run serve:plugins’ и ‘ng serve --hmr’ соответственно. По умолчанию Angular CLI создаёт новое приложение с определёнными предустановленными настройками компилятора. Попробуем поменять две из них, а именно ‘enableIvy’ и ‘aot’ (AOT означает Ahead Of Time compilation). Эти настройки могут быть найдены в файлах angular.json и tsconfig.json. AOT включен, Ivy включён – работает без проблем. AOT выключен, Ivy выключен. В этой конфигурации вы встретите некоторые проблемы. Чтобы избавится от ошибок и успешно скомпилировать приложение, вам надо сделать внести изменения. В файле plugin1.module.ts поменяйте стрелочные функции на обычные экспортируемые функции и уберите из них стрелочный синтаксис, как это показано ниже. В файлах plugin1-routing.module.ts и plugin1-routing.module.ts уберите вызов функции из секции импорта декоратора. Результат должен выглядеть как показано ниже. В файлах app-routing.module.ts и plugin-loader-routing.module.ts вам надо добавить ‘useValue’ к провайдеру ROUTES. Я включил эти изменения в демо в репозитории. После успешной компиляции приложение работает так, как предполагалось. 3.      AOT выключен, Ivy включён. Если вы запустите приложение в этой конфигурации без каких-либо изменений, то получите ошибку ‘ERROR Error: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'plugin1'’. Чтобы от неё избавиться, вам надо убрать useValue: [] из провайдера ROUTES. После этого приложение будет работать как задумано. 4.      AOT включён, Ivy выключен. Пожалуйста, снова добавьте useValue: [] в файл plugin-loader-routing.module.ts. В этой конфигурации вы должны натолкнуться на ошибку выполнения. Это произойдёт из-за того, что вам необходимо вручную скомпилировать плагины. К счастью, это возможно. Добавьте новую функцию как рекомендовано ниже в файл plugin-loader.module.ts. Также добавьте 3 провайдера COMPILER_OPTIONS, CompilerFactory и Compiler в тот же модуль. Когда вы всё это сделаете, внесите следующие изменения в провайдер ROUTES для того, чтобы вручную компилировать загруженные модули. Кажется, мы больше не можем использовать injection token ROUTES в главном модуле, потому что в этом случае Angular не понимает, что модуль загрузки плагинов должен быть подвержен ленивой загрузке и это выразится в ошибке ‘Runtime compiler is not loaded’. Выходит так, что мы должны прямо прописать маршруты для того, чтобы избежать данной ошибки. Таким образом, чтобы обойти проблему, закомментируйте массив провайдеров в декораторе модуля и добавьте переменную для обращения к сервису конфигурации плагинов. Также понадобится функция для сопоставления маршрутов плагинов. Измените массив импорта в декораторе модуля. И, наконец, добавьте конструктор в класс модуля, потому что нам надо присвоить значение переменной сервиса. После всех манипуляций приложение должно быть способно загружать плагины как предполагается. Я бы не рекомендовал описанную опцию из-за некоторой неряшливости кода. Загрузка плагинов не поддерживается Angular «из коробки», несмотря на тот факт, что данная техника программирования может быть востребована командами разработчиков уровня предприятия. Тем не менее, такую задачу можно выполнить в различных конфигурациях при помощи схожих методов. Возможно, в будущем это может стать частью стандартного фреймворка Angular. Я надеюсь, что данная статья будет вам полезной и поможет создавать лучшие приложения. Developer