В предыдущей статье этого цикла я показал, как использовать динамическую федерацию модулей. Это позволяет нам загружать неизвестные микрофронтенды — или пульты, что является более общим термином в Module Federation — во время компиляции. Нам даже не нужно заранее знать количество микрофронтов.
Если в предыдущей статье маршрутизатор использовался для интеграции доступных микрофронтендов или пультов, то в этой статье показано, как загружать отдельные компоненты. В качестве примера используется простой конструктор рабочих процессов на основе плагинов.
Английский перевод оригинальной статьи Манфреда Штайера «Building A Plugin-based Workflow Designer With Angular and Module Federation» обновлен 10-06-2021
Дизайнер рабочего процесса выступает в роли так называемого хоста, который загружает подключаемые задачи, предоставляемые в качестве пультов. Таким образом, они могут быть скомпилированы и развернуты по отдельности. После запуска конструктора рабочих процессов он получает конфигурацию, описывающую доступные плагины:
Обратите внимание, что эти плагины предоставляются из разных источников (http://localhost:4201 и http://localhost:4202), а дизайнер рабочего процесса обслуживается из собственного источника (http://localhost:4200).
📂 Исходный код
Спасибо Заку Джексону и Джеку Херрингтону, которые помогли мне разобраться в новом API rater для Федерации динамических модулей.
Важно: Эта статья написана для Angular и Angular CLI 14.x и выше. Убедитесь, что у вас правильная версия, если вы пробуете описанные здесь примеры.
Создание плагинов
Плагины предоставляются через отдельные приложения Angular. Для простоты все приложения являются частью одного монорежима. Ваша конфигурация webpack использует Module Federation для раскрытия отдельных плагинов, как показано в предыдущих статьях этой серии:
const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
name: 'mfe1',
exposes: {
'./Download': './projects/mfe1/src/app/download.component.ts',
'./Upload': './projects/mfe1/src/app/upload.component.ts'
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});
Одно отличие от конфигураций, показанных в предыдущих статьях, заключается в том, что здесь мы напрямую раскрываем независимые компоненты. Каждый компонент представляет собой задачу, которая может быть помещена в рабочий процесс.
Комбинация singleton: true
и strictVersion: true
заставляет webpack выдавать ошибку времени выполнения, когда оболочка и микрофронтенд(ы) требуют разных несовместимых версий (т.е. двух разных основных версий). Если бы мы опустили strictVersion
или установили значение false, webpack выдал бы только предупреждение во время выполнения.
Загрузка плагинов в конструктор рабочих процессов
Для загрузки плагинов в workflow designer я использую вспомогательную функцию loadRemoteModule
, предоставляемую плагином @angular-architects/module-federation
. Для загрузки мы используем loadRemoteModule
следующим образом
import { loadRemoteModule } from '@angular-architects/module-federation';
[...]
const component = await loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:4201/remoteEntry.js',
exposedModule: './Download'
})
Предоставление метаданных о плагинах
Во время выполнения нам необходимо предоставить конструктору рабочих процессов ключевые данные о плагинах. Тип, используемый для этого, называется PluginOptions
и расширяет LoadRemoteModuleOptions
, показанный в предыдущем разделе, с помощью displayName
и componentName
:
export type PluginOptions = LoadRemoteModuleOptions & {
displayName: string;
componentName: string;
};
Альтернативой этому является расширение Module Federation Manifest, как показано в предыдущей статье о динамической федерации модулей.
В то время как displayName
является именем, представляемым пользователю, componentName
относится к классу TypeScript, который представляет данный компонент Angular.
Для загрузки этих ключевых данных дизайнер рабочего процесса использует LookupService
:
@Injectable({ providedIn: 'root' })
export class LookupService {
lookup(): Promise<PluginOptions[]> {
return Promise.resolve([
{
type: 'module',
remoteEntry: 'http://localhost:4201/remoteEntry.js',
exposedModule: './Download',
displayName: 'Download',
componentName: 'DownloadComponent'
},
[...]
] as PluginOptions[]);
}
}
Для простоты LookupService
предоставляет некоторые уже определенные записи, но в реальном мире он, скорее всего, будет запрашивать эти данные из соответствующей конечной точки HTTP.
Динамическое создание компонента плагина
Конструктор рабочих процессов представляет плагины с помощью PluginProxyComponent
. Он принимает объект PluginOptions
через вход, загружает описанный плагин через Dynamic Module Federation и отображает компонент плагина в placeHolder:
@Component({
standalone: true,
selector: 'plugin-proxy',
template: `
<ng-container #placeHolder></ng-container>
`
})
export class PluginProxyComponent implements OnChanges {
@ViewChild('placeHolder', { read: ViewContainerRef, static: true })
viewContainer: ViewContainerRef;
constructor() { }
@Input() options: PluginOptions;
async ngOnChanges() {
this.viewContainer.clear();
const Component = await loadRemoteModule(this.options)
.then(m => m[this.options.componentName]);
this.viewContainer.createComponent(Component);
}
}
В версиях, предшествующих Angular 13, нам нужно было использовать ComponentFactoryResolver для получения фабрики загруженного компонента:
// Before Angular 13, we needed to retrieve a ComponentFactory
//
// export class PluginProxyComponent implements OnChanges {
// @ViewChild('placeHolder', { read: ViewContainerRef, static: true })
// viewContainer: ViewContainerRef;
// constructor(
// private injector: Injector,
// private cfr: ComponentFactoryResolver) { }
// @Input() options: PluginOptions;
// async ngOnChanges() {
// this.viewContainer.clear();
// const component = await loadRemoteModule(this.options)
// .then(m => m[this.options.componentName]);
// const factory = this.cfr.resolveComponentFactory(component);
// this.viewContainer.createComponent(factory, null, this.injector);
// }
// }
Подключение всех
Теперь пришло время соединить вышеупомянутые части. Для этого AppComponent
дизайнера рабочих процессов получает массив плагинов и рабочих процессов. Первый представляет PluginOptions
доступных плагинов и, следовательно, всех доступных задач, а второй описывает PluginOptions
задач, выбранных в конфигурации:
@Component({ [...] })
export class AppComponent implements OnInit {
plugins: PluginOptions[] = [];
workflow: PluginOptions[] = [];
showConfig = false;
constructor(
private lookupService: LookupService) {
}
async ngOnInit(): Promise<void> {
this.plugins = await this.lookupService.lookup();
}
add(plugin: PluginOptions): void {
this.workflow.push(plugin);
}
toggle(): void {
this.showConfig = !this.showConfig;
}
}
AppComponent
использует внедренный LookupService
для заполнения своего массива плагинов. Когда плагин добавляется в рабочий процесс, метод add
помещает его объект PluginOptions
в массив рабочего процесса.
Для отображения рабочего процесса дизайнер просто итерирует все элементы в массиве рабочего процесса и создает для них плагин-прокси:
<ng-container *ngFor="let p of workflow; let last = last">
<plugin-proxy [options]="p"></plugin-proxy>
<i *ngIf="!last" class="arrow right" style=""></i>
</ng-container>
Как упоминалось выше, прокси загружает плагин (по крайней мере, если он еще не загружен) и отображает его.
Кроме того, чтобы отобразить меню задач, показанное слева, он просматривает все записи в массиве плагинов. Для каждого из них отображается гиперссылка, вызывающая метод add:
<div class="vertical-menu">
<a href="#" class="active">Tasks</a>
<a *ngFor="let p of plugins" (click)="add(p)">Add {{p.displayName}}</a>
</div>
Заключение
Хотя федерация модулей полезна для реализации микрофронтендов, ее также можно использовать для создания подключаемых архитектур. Это позволяет нам расширить существующее решение третьих лиц. Он также кажется подходящим для SaaS-приложений, которые необходимо адаптировать к потребностям различных клиентов.