A step closer towards micro-frontend
In my previous post, I described our way to build a library to share layout, styles, and components in the user interface of our self-contained systems architecture.
This helped us a lot because it reduced the coupling of the shared code to the applications in our system. However, we started to struggle with it again after an inconsistent navigation in the header due to an update in the shared header component. Let's be serious, this is bad and should not happen! It is ok when some little CSS styling is not up to date in the application. Most likely nobody will recognize that. However, the navigation must be consistent in a self-contained system. It is bad when the navigation to an application suddenly disappears when the user navigates through the system. Even worse, when there is a dead link in the navigation.
Dynamically rendered Layout
We discussed two options to solve that problem:
- Extracting the header and footer into its own shared library. All the infrastructure for this approach already exists. It would be easy to build and would make it easier to automatically integrate and update it in all applications. But it still has a dependency on certain npm package versions which can turn an update into a bigger exercise.
- Deploy the header and footer separately and integrate it into the applications during runtime. This is clearly a step towards a micro-frontend architecture. We initially decided against micro-frontends because it would increase the complexity and our project is not large enough that it would be worth using that pattern. But for the current problem, it seemed to be a good fit.
We decided, obviously, to deploy the header and footer separately and integrate it at runtime into our applications. That we would implement it by using WebComponents, was also clear from the beginning. Still open was the Framework we wanted to use. We are using Angular. Angular Elements could be a fit but has some drawbacks.
Our goal was to create a single js file, deploy it to Azure Blob Storage, and dynamically include it via <script>
tag. Let's have a look at the options we evaluated:
Angular Elements, obviously. It is possible to create a single file with ngx-build-plus which is what we would like to have. But Angular Elements is building a complete Angular application into the WebComponent, hence it will be a rather large bundle.
StencilJS, developed by the Ionic team to make their UI components independent of the UI framework.
LitElement, just another lightweight framework to write WebComponents.
We decided to use StencilJS. We think it is a better option than Angular Elements, even though it is actually designed for compile-time integration. We want to have a fast application and the bundle size of Angular Element is way larger than any output from StencilJS.
Create and Stencil project
Let's have a look at our setup with StencilJS and the integration in our Angular applications. We are using NX for our UI workspaces. Let's create an NX workspace with a StencilJS project.
First, we create the NX workspace with typescript presets:
npx create-nx-workspace my-workspace --preset=ts
Afterwards, we are going to create a Stencil project in our freshly created NX workspace by using the NX plugin from Nnext:
npm install @nxext/stencil --save-dev
nx g @nxext/stencil:library my-lib
The Nnext plugin provides a generator that allows to create our first component:
nx g @nxext/stencil:component my-header --project my-lib
This outputs a component skeleton like this:
@Component({
tag: 'my-header',
styleUrl: 'my-header.scss',
shadow: true,
})
export class MyHeader {
@Prop() first: string;
@Prop() middle: string;
@Prop() last: string;
private getText(): string {
return (this.first || '') + (this.middle ? ` ${this.middle}` : '') + (this.last ? ` ${this.last}` : '');
}
render() {
return <div>Hello, World! I'm {this.getText()}</div>;
}
}
I will not going to write a StencilJS tutorial at this place. But one thing I need to mention. Stencil provides several output targets. We actually want to have a single file bundle which would be the dist-custom-elements-bundle
out target. Unfortunately, this has been deprecated. We use its successor dist-custom-elements
. It does basically the same thing but produces a js file per component. This is the better way because it is better to load several smaller bundles than a large one. It will also be better for leveraging http2
. We would prefer a single file because it means less file handling on our frontend, but we can work with that.
outputTargets: [
{
type: 'dist-custom-elements',
},
];
Finally, we are deploying these output files in an Azure Blob Storage. But any static hosting would do the job.
It sounds that easy, but nobody on our team has real experience in StencilJS and is especially lazy load them in an Angular application. One problem worth mentioning is the handling of assets. If the component uses assets that get deployed, you need to make sure to load them with an absolute path. Relative paths get resolved to the URL where the application is deployed. We solved the problem by compiling everything (e.g. SVG icons and JSON translation files) into the bundle. This way, we don't need to load them during the runtime.
Consuming the WebComponents dynamically during run time
After creating and deploying our WebComponents, they need to be integrated into the frontend of our applications. A simple script element loading js bundle will do the job. Then we just can use the custom element:
<script type="module" src='https://cdn.jsdelivr.net/npm/my-name@0.0.1/dist/myname.js'></script>
<my-header></my-header>
We found a nice library, ANGULAR EXTENSIONS ELEMENTS, which makes it even easier to lazy load elements in Angular. It also supports displaying special components for the loading and error cases.
<my-header *axLazyElement="headerUrl; errorTemplate: errorHeader; loadingTemplate: loading; module: true"></my-header>
One thing which needs to be done to make this work is defining the custom elements schema on the corresponding Angular module.
@NgModule({
declarations: [...],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [ LazyElementsModule],
})
Summary
Not all of our problems were solved by introducing a shared Angular library. We came to the conclusion that an independently deployed and lazy loaded header and footer would be the best solution for us. We decided to use WebComponents for that job. Finally, we implemented it with StencilJS and integrated it with the Angular Extension Elements library.
Implementing header and footer as a micro-frontend was not very smooth. We had a steep learning curve and spent a lot more time than expected. But we are very happy with the result and I, personally, would do that definitely again.
Subscribe to my newsletter
Read articles from Michael Lehmann directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Michael Lehmann
Michael Lehmann
My name is Michael Lehmann. I'm a developer, architect, technical team lead and occasional speaker. I work for Zühlke Engineering AG as a Lead Software Architect aiming to help my customers and team mates to design and develop software. I specialised in developing distributed systems with micro-service architectures and modern web technologies.