Microservice Clients with Web Components using Angular Elements: Dreams of the (near) future?

In one of my last blog posts I've compared several approaches for using Single Page Applications, esp. Angular-based ones, in a microservice-based environment. Some people are calling such SPAs micro frontends; other call them micro aps. As you can read in the mentioned post, there is not the one and only perfect approach but several feasible concepts with different advantages and disadvantages.

In this post I'm looking at one of those approaches in more detail: Using Web Components. For this, I'm leveraging the new Angular Elements library (@angular/elements) the Core Team is currently working on. Please note that it's still an Angular Labs Project which means that it's experimental and that there can be breaking changes anytime.

Angular Labs

Angular Elements

To get started with @angular/elements you should have a look at Vincent Ogloblinsky's blog post. It really explains the ideas behind it very well. If you prefer a video, have a look to Rob Wormald's presentation from Angular Connect 2017. Also, my buddy Pascal Precht gave a great talk about this topic at ng-be 2017.

As those resources are really awesome, I won't repeat the information they provide here. Instead, I'm showing how to leverage this know-how to implement microservice clients.

Case Study

The case study presented here is as simple as possible. It contains a shell app that activates microservice clients as well as routes within those microservice clients. They are just called Client A and Client B. In addition, Client B also contains a widget from Client A.

Client A is activated

Client B with widget from Client A

The whole source code can be found in my GitHub repo.

Routing within Microservice Clients

One thing that is rather unusual here, is that whole clients are implemented as Web Components and therefore they are using routing:

@NgModule({ imports: [ ReactiveFormsModule, BrowserModule, RouterModule.forRoot([ { path: 'client-a/page1', component: Page1Component }, { path: 'client-a/page2', component: Page2Component }, { path: '**', component: Page1Component} ], { useHash: true }) ], declarations: [ ClientAComponent, Page1Component, Page2Component, [...] ], entryComponents: [ ClientAComponent, [...] ] }) export class AppModule { ngDoBootstrap() { } }

When bootstrapping such components as Web Components we have to initialize the router manually:

@Component([...]) export class ClientAComponent { constructor(private router: Router) { router.initialNavigation(); // Manually triggering initial navigation for @angular/elements ? } }

Excluding zone.js

Normally, Angular leverages zone.js for change detection. It provides a lot of convenience by informing Angular about all browser events. To be capable of this, it's monkey-patching all browser objects. Especially, when we want to use several microservice clients within a single page it can be desirable to avoid such a behavior. This would also lead to smaller bundle sizes.

Beginning with Angular 5 we can exclude zone.js by setting the property ngZone to noop during bootstrapping:

registerAsCustomElements([ClientAComponent, ClientAWidgetComponent], () => platformBrowserDynamic().bootstrapModule(AppModule, { ngZone: 'noop' }) );

After this, we have to trigger change detection manually. But this is cumbersome and error-prone. There are some ideas to deal with this. A prototypical (!) one comes from Fabian Wiles who is an active community member. It uses a custom push pipe that triggers change detection when an observable yields a new value. It works similar to the async pipe but other than it push also works without zone.js:

@Component({ selector: 'client-a-widget', template: ` <div id="widget"> <h1>Client-A Widget</h1> <input [formControl]="control"> {{ value$ | push }} </div> `, styles: [` #widget { padding:10px; border: 2px darkred dashed } `], encapsulation: ViewEncapsulation.Native }) export class ClientAWidgetComponent implements OnInit { control = new FormControl(); value$: Observable<string>; ngOnInit(): void { this.value$ = this.control.valueChanges; } }

You can find Fabian's push pipe within my github repo.

Build Process

For building the web components, I'm using a modified version of the webpack configuration from Vincent Ogloblinsky's blog post. I've modified it to create a bundle for each microservice client. Normally, they would be build within separate projects but for the sake of simplicity I've put everything into my sample:

const AotPlugin = require('@ngtools/webpack').AngularCompilerPlugin; const path = require('path'); var clientA = { entry: { 'client-a': './src/client-a/main.ts' }, resolve: { mainFields: ['es2015', 'browser', 'module', 'main'] }, module: { rules: [{ test: /\.ts$/, loaders: ['@ngtools/webpack'] }] }, plugins: [ new AotPlugin({ tsConfigPath: './tsconfig.json', entryModule: path.resolve(__dirname, './src/client-a/app.module#AppModule' ) }) ], output: { path: __dirname + '/dist', filename: '[name].bundle.js' } }; var clientB = { entry: { 'client-b': './src/client-b/main.ts' }, resolve: { mainFields: ['es2015', 'browser', 'module', 'main'] }, module: { rules: [{ test: /\.ts$/, loaders: ['@ngtools/webpack'] }] }, plugins: [ new AotPlugin({ tsConfigPath: './tsconfig.json', entryModule: path.resolve(__dirname, './src/client-b/app.module#AppModule' ) }) ], output: { path: __dirname + '/dist', filename: '[name].bundle.js' } }; module.exports = [clientA, clientB];

Loading bundles

After creating the bundles, we can load them into a shell application:

<client-a></client-a> <client-b></client-b> <script src="dist/client-a.bundle.js"></script> <script src="dist/client-b.bundle.js"></script>

In this example the bundles are located via relative paths but you could also load them from different origins. The latter one allows for a separate development and deployment of microservice clients.

In addition to that, we need some kind of meta-routing that makes sure that the microservice clients are only displayed when specific menu items are activated. I've implemented this in VanillaJS. You can look it up in the example provided.

Providing Widgets for other Microservice Clients

A bundle can provide several Web Components. For instance, the bundle for Client A also contains a ClientAWidgetComponent which is used in Client B:

registerAsCustomElements([ClientAComponent, ClientAWidgetComponent], () => platformBrowserDynamic().bootstrapModule(AppModule, { ngZone: 'noop' }) );

When calling it there is one challenge: In Client B, Angular doesn't know anything about Client A's ClientAWidgetComponent. Calling it would therefore make Angular to throw an exception. To avoid this, we can make use of the CUSTOM_ELEMENTS_SCHEMA:

@NgModule({ [...] schemas: [CUSTOM_ELEMENTS_SCHEMA], [...] }) export class AppModule { ngDoBootstrap() { } }

After this, we can call the widget anywhere within Client B:

<h2>Client B - Page 2</h2> <client-a-widget></client-a-widget>

Evaluation

As mentioned, @angular/elements is currently experimental. Therefore this approach is more or less a dream of the (near) future. Besides this, there are some advantages and disadvantages:

Advantages

  • Styling is isolated from other Microservice Clients due to Shadow DOM
  • Allows for separate development and separate deployment
  • Mixing widgets from different Microservice Clients is possible
  • The shell can be a Single Page Application too
  • We can use different SPA frameworks in different versions for our Microservice Clients

Disadvantages

  • Microservice Clients are not completely isolated as it would be the case when using hyperlinks or iframes instead. This means that they could influence each other in an unplanned way. This also means that there can be conflicts when using different frameworks in different versions.
  • Shadow DOM doesn't work with IE 11
  • We need polyfills for some browsers

 

 
Sie wollen mehr zum Thema Microservice Clients with Web Components using Angular Elements: Dreams of the (near) future? wissen? Hier können Sie eine Anfrage für eine unverbindliche Schulung ode Beratung bzw. einen Workshop erstellen.
 
Unverbindliche Anfrage
 
 

Schulung und Beratung

Angular: Strukturierte Einführung

In dieser Schulung erfahren Sie von bekannten Insidern und Angular Experten der ersten Stunde anhand eines durchgängigen Beispiels, welche Konzepte hinter dem modernen Single-Page-Application-Framework aus der Feder von Google stecken und lernen diese für Ihre eigenen Projekte zu nutzen. Zusätzlich werden sie selbst eine erste Angular-Anwendung zu schreiben. Diese orientiert sich an Best Practices und kann somit als Vorlage für eigene Projekte herangezogen werden. Zum Einsatz kommt die jeweils neueste Version von Angular.

Details

Advanced Angular: Architekturen für Enterprise-Anwendungen

In dieser weiterführenden Intensiv-Schulung lernen Sie von namhaften Insidern, wie sich große und skalierbare Geschäftsanwendungen mit Angular entwickeln lassen. Mehrere Architekturansätze und Best Practices werden anhand einer Fallstudie aufgezeigt und diskutiert. Die Fallstudie wird in den einzelnen Übungseinheiten erweitert und kann als Vorlage für eigene Vorhaben dienen.

Details

Weitere Schulungen ...