Angular pluggable architecture?

Pluggable Architecture – A Software Architecture that allows to plug functionality using PluggableModules. Ward Cunningham

One of the projects I am involved in these days is composed of multiple applications each of them with its own front-end implemented in Angular, all using consistent UX patterns and framework. The common user flows like managing projects or navigating to the applications is handled in a central application. At the same time, common components like navbar or sidebars are reused across.

This front-end separation by applications or large features has prevented us from creating a big monolithic web app and our teams have the kind of independence and control in the front-end we get in the back-end with micro-services.

All good so far, but let’s say that now we want to have a dashboard application composed of widgets and one of the requirements is that multiple teams can implement and deploy their own widgets independently. Hence, without the need of modifying the dashboard application code or redeploying it when a new widget comes or gets updated.

Can an Angular CLI application load and render external widgets at runtime?

The short answer is yes, it is possible. You can find a working demo in this repo https://github.com/paucls/angular-pluggable-architecture

The long answer is that there is not a straightforward solution ;-), it took me a little bit of research to find out how.

First of all, the application needs a way to load remote widget bundles at runtime. This sounds like lazy loading, but that is not going to help us, lazy loading is for loading modules linked to child routes, but the modules still need to belong to the same application and be known at build-time.

The solution is to use SystemJS, but only at runtime and only to load the widgets. We do not need to configure our CLI project to switch from Webpack to SystemJS.

Loading a bundle will look like this:

const module = await SystemJS.import('/path-to-a-module-bundle');

The next problem is that the bundle contains a Module and a Component for the widget but they have dependencies themselves to other modules. The smallest component has at least dependencies to @angular/core.

import { Component, OnInit } from '@angular/core';

To stop SystemJS from trying to find and fetch those vendor modules from the network we need to set SystemJS with the modules already available.

/**
 * Set existing vendor modules into SystemJS registry.
 * This way SystemJS won't make HTTP requests to fetch imported modules
 * needed by the dynamicaly loaded Widgets.
 */
import { System } from 'systemjs';
declare const SystemJS: System;

import * as angularCore from '@angular/core';
import * as angularCommon from '@angular/common';
import * as angularCommonHttp from '@angular/common/http';

SystemJS.set('@angular/core', SystemJS.newModule(angularCore));
SystemJS.set('@angular/common', SystemJS.newModule(angularCommon));
SystemJS.set('@angular/common/http', SystemJS.newModule(angularCommonHttp));

The second part is compiling and rendering each widget. For this, we need to make use of the Angular JIT compiler to compile the Angular module and component of the widget. And then create the widget component in ViewChield target div.

@Component({
  selector: 'app-dashboard',
  template: '<div #content></div>'
})
export class DashboardComponent implements AfterViewInit {

  @ViewChild('content', { read: ViewContainerRef }) content: ViewContainerRef;

  constructor(private compiler: Compiler, private dashboardService: DashboardService,
    private injector: Injector) { }

  ngAfterViewInit() {
    this.loadWidgets();
  }

  private async loadWidgets() {
    const widgets = await this.dashboardService.getWidgetConfigs().toPromise();
    widgets.forEach((widget) => this.createWidget(widget));
  }

  private async createWidget(widget: WidgetConfig) {
    // import external module bundle
    const module = await SystemJS.import(widget.moduleBundlePath);

    // compile module
    const moduleFactory = await this.compiler.compileModuleAsync(module[widget.moduleName]);

    // resolve component factory
    const moduleRef = moduleFactory.create(this.injector);
    const componentProvider = moduleRef.injector.get(widget.name);
    const componentFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(componentProvider);

    // compile component
    this.content.createComponent(componentFactory);
  }

}

Note that to be able to use the JIT compiler our build needs to have AOT disabled what it is not ideal. There is an open issue (https://github.com/angular/angular/issues/20875) to allow the use of the compiler with AOT, I tried some of the temporal solutions suggested like defining custom providers for JIT in the app module but I could not make it work yet.

References:
– Developing with Angular, Denys Vuika. https://github.com/DenisVuyka/developing-with-angular/tree/master/angular/plugins
– As busy as a bee — lazy loading in the Angular CLI, David Herges https://blog.angularindepth.com/as-busy-as-a-bee-lazy-loading-in-the-angular-cli-d2812141637f … how does lazy loading in the Angular CLI work under the hood?
– How to load dynamic external components into Angular application? https://stackoverflow.com/questions/45503497/how-to-load-dynamic-external-components-into-angular-application/45506470#45506470
– Here is what you need to know about dynamic components in Angular, Maxim Koretskyi https://blog.angularindepth.com/here-is-what-you-need-to-know-about-dynamic-components-in-angular-ac1e96167f9e
– Modules are not what you think they are, Maxim Koretskyi https://youtu.be/pERhnBBae2k
– Extension mechanism demo https://github.com/maximusk/extension-mechanism-demo
– JIT Compiler needed with AOT Build for Dynamic Component. https://github.com/angular/angular/issues/20875

Advertisements