|
1 |
| -# Angular Elements Router |
2 |
| - |
3 |
| -   |
4 |
| - |
5 |
| -The angular elements router is a library for using the Angular Router within Angular Elements. |
6 |
| - |
7 |
| -- **Router module usage** — Use the angular router module both in the platform and the micro frontend without interfering each other. |
8 |
| - |
9 |
| -- **Dev platform** — Includes a straight-forward dev platform capable of replacing the production platform for local development. |
10 |
| - |
11 |
| -- **Battle-tested** — This library is based on the efforts at [LeanIX](https://www.leanix.net/en/) where this approach is used to serve micro frontends to 100K users. |
12 |
| - |
13 |
| -- **Non-intrusive** — Use only the features you need, easy opt-out once Angular starts supporting the router in Angular Elements out of the box. |
14 |
| - |
15 |
| -- **No dependencies** — Besides Angular this library does not include any dependencies. |
16 |
| - |
17 |
| -## Installation |
| 1 | +# Microfrontends with Angular |
18 | 2 |
|
19 | 3 | ```
|
20 |
| -$ npm install --save ngx-elements-router |
21 |
| -``` |
22 |
| - |
23 |
| -## Try it |
24 |
| - |
25 |
| -This repo includes an example platform and an example micro frontend. |
26 |
| - |
27 |
| -``` |
28 |
| -$ git clone https://github.com/fboeller/ngx-elements-router.git |
29 |
| -$ cd ngx-elements-router |
| 4 | +$ git clone https://github.com/fboeller/microfrontends-with-angular.git |
| 5 | +$ cd microfrontends-with-angular |
30 | 6 | $ npm install
|
| 7 | +$ npm start |
31 | 8 | ```
|
32 | 9 |
|
33 |
| -### Dev platform with micro frontend |
34 |
| - |
35 |
| -``` |
36 |
| -$ npm start bookings |
37 |
| -``` |
38 |
| - |
39 |
| -A visit to `localhost:4200` shows the dev platform. |
40 |
| -If you click the buttons, you see how the route changes, independent from if the click originated in the platform or the micro frontend. |
41 |
| - |
42 |
| -### Angular platform with micro frontend |
43 |
| - |
44 |
| -``` |
45 |
| -$ npm run build bookings |
46 |
| -$ npm install -g http-server |
47 |
| -$ http-server dist/bookings --port 4201 |
48 |
| -``` |
49 |
| - |
50 |
| -``` |
51 |
| -$ npm start train-platform |
52 |
| -``` |
53 |
| - |
54 |
| -A visit to `localhost:4200` shows the Angular platform. |
55 |
| -The bundle file `main.js` is served from `localhost:4201` without hot-reloading. |
56 |
| - |
57 |
| -## Prerequisites |
58 |
| - |
59 |
| -You have an Angular application that acts as a [platform](./projects/train-platform) and an Angular application that acts as a [micro frontend](./projects/bookings). |
60 |
| -A build of the micro frontend results in a single build that registers custom elements on loading. |
61 |
| - |
62 |
| -## Usage |
63 |
| - |
64 |
| -The general idea of this library is the delegation of the modification of the browser url such that only the platform application modifies it. |
65 |
| -This is achieved by preventing Angular in the micro frontend application from accessing the browser url and instead using inputs and outputs |
66 |
| -of the web component to pass route changes from and to the router module of the platform. |
67 |
| -The micro frontend routes and the platform routes are both defined starting after the base href but the micro frontend routes typically define |
68 |
| -a single top-level route like `/micro-frontend` and only receive routes starting with that route. |
69 |
| -In the component mounted at `/micro-frontend`, it is then possible to use an absolute path `/abc` to refer to a route external to the micro frontend and |
70 |
| -a relative path `./abc` to refer to a route relative to the route `/micro-frontend`. |
71 |
| - |
72 |
| -### Create a host component |
73 |
| - |
74 |
| -To be able to reference your custom element in the routes, you need to create a host component. |
75 |
| -You can use the `aerRouting` on the custom element to pass route changes to the micro frontend and to allow the micro frontend to pass route changes to the platform. |
76 |
| - |
77 |
| -[platform/micro-frontend-host.component.ts](./projects/train-platform/src/app/micro-frontend-host/micro-frontend-host.component.ts) |
78 |
| - |
79 |
| -```typescript |
80 |
| -import { Component } from "@angular/core"; |
81 |
| - |
82 |
| -@Component({ |
83 |
| - selector: "app-host", |
84 |
| - template: `<mf-entry aerRouting></mf-entry>`, |
85 |
| -}) |
86 |
| -export class MicroFrontendHostComponent {} |
87 |
| -``` |
88 |
| - |
89 |
| -### Create a host module |
90 |
| - |
91 |
| -To lazy load your custom element, you need to create a host module in the platform. |
92 |
| -Import `AngularElementsRouterModule` to be able to use the `aerRouting` directive. |
93 |
| -Use the schema `CUSTOM_ELEMENTS_SCHEMA` to make Angular accept the custom element in the host component. |
94 |
| -Use the path `**` to pass all sub paths to the custom element. |
95 |
| - |
96 |
| -[platform/micro-frontend-host.module.ts](./projects/train-platform/src/app/micro-frontend-host/micro-frontend-host.module.ts) |
97 |
| - |
98 |
| -```typescript |
99 |
| -import { AngularElementsRouterModule } from "ngx-elements-router"; |
100 |
| -import { MicroFrontendHostComponent } from "./micro-frontend-host.component"; |
101 |
| - |
102 |
| -const routes: Routes = [ |
103 |
| - { |
104 |
| - path: "**", |
105 |
| - component: MicroFrontendHostComponent, |
106 |
| - }, |
107 |
| -]; |
108 |
| - |
109 |
| -@NgModule({ |
110 |
| - declarations: [MicroFrontendHostComponent], |
111 |
| - imports: [RouterModule.forChild(routes), AngularElementsRouterModule], |
112 |
| - schemas: [CUSTOM_ELEMENTS_SCHEMA], |
113 |
| -}) |
114 |
| -export class MicroFrontendHostModule {} |
115 |
| -``` |
116 |
| - |
117 |
| -### Bind the micro frontend to a route |
118 |
| - |
119 |
| -Choose a route under which your micro frontend should be loaded. |
120 |
| -Use the `LoadBundleGuard` to load the bundle of your micro frontend on the first activation of the route. |
121 |
| - |
122 |
| -[platform/app-routing.module.ts](./projects/train-platform/src/app/app-routing.module.ts) |
123 |
| - |
124 |
| -```typescript |
125 |
| -import { LoadBundleGuard } from "ngx-elements-router"; |
126 |
| - |
127 |
| -const routes: Routes = [ |
128 |
| - { |
129 |
| - path: "micro-frontend", |
130 |
| - canActivate: [LoadBundleGuard], |
131 |
| - data: { |
132 |
| - bundleUrl: "http://localhost:4201/main.js", |
133 |
| - }, |
134 |
| - loadChildren: () => |
135 |
| - import("./micro-frontend-host/micro-frontend-host.module").then( |
136 |
| - (m) => m.MicroFrontendHostModule |
137 |
| - ), |
138 |
| - }, |
139 |
| -]; |
140 |
| -``` |
141 |
| - |
142 |
| ---- |
143 |
| - |
144 |
| -**NOTE** |
145 |
| - |
146 |
| -Since Angular 11, your bundle name might be `main-es2015.js`. |
147 |
| - |
148 |
| ---- |
149 |
| - |
150 |
| -### Register routing in the micro frontend |
151 |
| - |
152 |
| -Use the `EntryRoutingService` in the Angular component representing the custom element. |
153 |
| -This way, route changes are passed to the Angular router in the micro frontend and in the other direction to the platform. |
154 |
| - |
155 |
| -[micro-frontend/entry-component.ts](./projects/bookings/src/app/entry.component.ts) |
156 |
| - |
157 |
| -```typescript |
158 |
| -import { EntryRoutingService } from 'ngx-elements-router'; |
159 |
| - |
160 |
| -@Component({ |
161 |
| - selector: 'mf-angular-entry', |
162 |
| - template: `<router-outlet></router-outlet>`, |
163 |
| -}) |
164 |
| -export class EntryComponent implements OnChanges, OnDestroy { |
165 |
| - @Input() route?: string; |
166 |
| - @Output() routeChange = new EventEmitter<string>(); |
167 |
| - |
168 |
| - route$ = new Subject<string | undefined>; |
169 |
| - |
170 |
| - private readonly subscription: Subscription; |
171 |
| - |
172 |
| - constructor(private entryRoutingService: EntryRoutingService) { |
173 |
| - this.subscription = this.entryRoutingService.registerRouting( |
174 |
| - this.routeChange, |
175 |
| - this.route$ |
176 |
| - ); |
177 |
| - } |
178 |
| - |
179 |
| - ngOnDestroy(): void { |
180 |
| - this.subscription.unsubscribe(); |
181 |
| - } |
182 |
| - |
183 |
| - ngOnChanges() { |
184 |
| - this.route$.next(this.route); |
185 |
| - } |
186 |
| -``` |
187 |
| -
|
188 |
| -### Create a custom element from the entry component |
189 |
| -
|
190 |
| -The module in your micro frontend needs to define the custom element in the browser on bootstrap of the module. |
191 |
| -
|
192 |
| -[micro-frontend/app-module.ts](./projects/bookings/src/app/app.module.ts) |
193 |
| -
|
194 |
| -```typescript |
195 |
| -import { EntryComponent } from "./entry.component"; |
196 |
| -import { createCustomElement } from "@angular/elements"; |
197 |
| - |
198 |
| -@NgModule({ |
199 |
| - declarations: [EntryComponent], |
200 |
| - imports: [BrowserModule], |
201 |
| - providers: [], |
202 |
| -}) |
203 |
| -export class AppModule { |
204 |
| - constructor(private injector: Injector) {} |
205 |
| - |
206 |
| - ngDoBootstrap() { |
207 |
| - const customElement = createCustomElement(EntryComponent, { |
208 |
| - injector: this.injector, |
209 |
| - }); |
210 |
| - window.customElements.define("mf-entry", customElement); |
211 |
| - } |
212 |
| -} |
213 |
| -``` |
214 |
| -
|
215 |
| -### Create a component that sits at your micro frontend route |
216 |
| -
|
217 |
| -The priorly created `EntryComponent` gets the full path starting after the base href. |
218 |
| -In its router outlet, a `MicroFrontendComponent` is mounted if the full path is a path to the micro frontend. |
219 |
| -Inside this component, you can use an absolute path `/abc` to refer to a route outside of the micro frontend. |
220 |
| -You can use a relative path `./abc` to refer to a route relative to the micro frontend route. |
221 |
| -It can itself have a router outlet to mount different components depending on the subpath of the micro frontend. |
222 |
| -
|
223 |
| -[micro-frontend/micro-frontend.component.ts](./projects/bookings/src/app/micro-frontend.component.ts) |
224 |
| -
|
225 |
| -```typescript |
226 |
| -@Component({ |
227 |
| - selector: "mf-micro-frontend", |
228 |
| - template: ` |
229 |
| - <a routerLink="/child">/child</a> |
230 |
| - <a routerLink="./child">/micro-frontend/child</a> |
231 |
| - <router-outlet></router-outlet> |
232 |
| - `, |
233 |
| -}) |
234 |
| -export class MicroFrontendComponent {} |
235 |
| -``` |
236 |
| -
|
237 |
| -### Define the routes in the micro frontend |
238 |
| -
|
239 |
| -The route structure in the micro frontend needs to be defined with the same structure as the platform. |
240 |
| -If the platform delegates all traffic at `/micro-frontend` to the micro frontend, then the micro frontend should define such a route. |
241 |
| -All other traffic needs to go to a route `**` such that the router module of the micro frontend does not discard it as undefined routes. |
242 |
| -This way, you can navigate to links outside of the micro frontend from within the micro frontend. |
243 |
| -
|
244 |
| -[micro-frontend/app-routing.module.ts](./projects/bookings/src/app/app-routing.module.ts) |
245 |
| -
|
246 |
| -```typescript |
247 |
| -import { NoComponent } from "ngx-elements-router"; |
248 |
| - |
249 |
| -const routes: Routes = [ |
250 |
| - { |
251 |
| - path: "micro-frontend", |
252 |
| - component: MicroFrontendComponent, |
253 |
| - children: microfrontendRoutes, |
254 |
| - }, |
255 |
| - { path: "**", component: NoComponent }, |
256 |
| -]; |
257 |
| -``` |
258 |
| -
|
259 |
| -### Prevent direct access to the browser url |
260 |
| -
|
261 |
| -By default, the Angular router within the micro frontend tries to update the browser url. |
262 |
| -Use the `NoopLocationStrategy` to prevent this, such that the platform has the only access. |
263 |
| -
|
264 |
| -[micro-frontend/app-routing.module.ts](./projects/bookings/src/app/app-routing.module.ts) |
265 |
| -
|
266 |
| -```typescript |
267 |
| -import { NoopLocationStrategy } from "ngx-elements-router"; |
268 |
| - |
269 |
| -@NgModule({ |
270 |
| - imports: [RouterModule.forRoot(routes)], |
271 |
| - providers: [{ provide: LocationStrategy, useClass: NoopLocationStrategy }], |
272 |
| - exports: [RouterModule], |
273 |
| -}) |
274 |
| -export class AppRoutingModule {} |
275 |
| -``` |
276 |
| -
|
277 |
| -### Setup a dev platform within the micro frontend |
278 |
| -
|
279 |
| -For the independent development of the micro frontend, a minimal dev platform consisting of an index.html with some Javascript can be of advantage. |
280 |
| -This dev platform can be used both locally and also be deployed and used together with the bundle. |
281 |
| -
|
282 |
| -To use it, include the `dev-platform.js` in the scripts of your micro frontend in the `angular.json`. |
283 |
| -
|
284 |
| -[angular.json](./angular.json) |
285 |
| -
|
286 |
| -```json |
287 |
| -{ |
288 |
| -"build": { |
289 |
| - "builder": "ngx-build-plus:build", |
290 |
| - "options": { |
291 |
| - "singleBundle": true, |
292 |
| - "outputHashing": "none", |
293 |
| - ..., |
294 |
| - "scripts": [ |
295 |
| - "node_modules/ngx-elements-router/src/dev-platform.js" |
296 |
| - ] |
297 |
| - }, |
298 |
| -} |
299 |
| -``` |
300 |
| -
|
301 |
| -Setup an `index.html` in the micro frontend app. |
302 |
| -
|
303 |
| -[micro-frontend/index.html](./projects/bookings/src/index.html) |
304 |
| -
|
305 |
| -```html |
306 |
| -<!DOCTYPE html> |
307 |
| -<html lang="en"> |
308 |
| - <head> |
309 |
| - <meta charset="utf-8" /> |
310 |
| - <title>Example Micro Frontend</title> |
311 |
| - <base href="/" /> |
312 |
| - <meta name="viewport" content="width=device-width, initial-scale=1" /> |
313 |
| - <link rel="icon" type="image/x-icon" href="favicon.ico" /> |
314 |
| - <script src="scripts.js"></script> |
315 |
| - </head> |
316 |
| - <body> |
317 |
| - <button onclick="router.changeRoute('/')">Go to platform main page</button> |
318 |
| - <button onclick="router.changeRoute('/micro-frontend')"> |
319 |
| - Go to micro frontend main page |
320 |
| - </button> |
321 |
| - <div id="router-outlet"></div> |
322 |
| - <script> |
323 |
| - const router = registerRouting("/micro-frontend", "mf-entry"); |
324 |
| - </script> |
325 |
| - </body> |
326 |
| -</html> |
327 |
| -``` |
328 |
| -
|
329 |
| -### Pass Zone.js events |
330 |
| -
|
331 |
| -Zone.js registers itself globally on the window object. |
332 |
| -If the platform and the micro frontend are both Angular projects relying on Zone.js, then they concurrently access it and interfere with each others change detection cycles. |
333 |
| -To mitigate that, you can pass Zone.js microtask empty events to the micro frontend. These events are the trigger of a change detection cycle. |
334 |
| -
|
335 |
| -You can use the `EntryZoneService` in the Angular component representing the custom element. |
336 |
| -This way, Zone.js micro task empty events are passed to the micro frontend and a change detection cycle is triggered. |
337 |
| -
|
338 |
| -[micro-frontend/entry-component.ts](./projects/bookings/src/app/entry.component.ts) |
339 |
| -
|
340 |
| -```typescript |
341 |
| -import { EntryZoneService } from "ngx-elements-router"; |
342 |
| - |
343 |
| -@Component({ |
344 |
| - selector: "mf-angular-entry", |
345 |
| - template: `<router-outlet></router-outlet>`, |
346 |
| -}) |
347 |
| -export class ExampleComponent implements OnChanges, OnDestroy { |
348 |
| - @Input() microtaskEmpty$?: Observable<void>; |
349 |
| - microtaskEmpty$$ = new Subject<Observable<void>>(); |
350 |
| - |
351 |
| - constructor(private entryZoneService: EntryZoneService) { |
352 |
| - this.subscription = this.entryZoneService.registerZone( |
353 |
| - this.microtaskEmpty$$ |
354 |
| - ); |
355 |
| - } |
356 |
| - |
357 |
| - ngOnDestroy() { |
358 |
| - this.subscription.unsubscribe(); |
359 |
| - } |
360 |
| - |
361 |
| - ngOnChanges() { |
362 |
| - this.microtaskEmpty$$.next(this.microtaskEmpty$); |
363 |
| - } |
364 |
| -} |
365 |
| -``` |
366 |
| -
|
367 |
| -Use the `aerRouting` on the custom element to pass micro task empty events to the micro frontend. |
368 |
| -
|
369 |
| -[platform/micro-frontend-host.component.ts](./projects/train-platform/src/app/micro-frontend-host/micro-frontend-host.component.ts) |
370 |
| -
|
371 |
| -```typescript |
372 |
| -import { Component } from "@angular/core"; |
373 |
| - |
374 |
| -@Component({ |
375 |
| - selector: "app-host", |
376 |
| - template: `<mf-entry aerZone></mf-entry>`, |
377 |
| -}) |
378 |
| -export class MicroFrontendHostComponent {} |
379 |
| -``` |
380 |
| -
|
381 |
| -## Limitations |
382 |
| -
|
383 |
| -Note that this library is a workaround to make the most common use cases of the Angular router accessible in a micro frontend. |
384 |
| -It has not been tried so far, if this approach works with more advanced features like named router outlets or router child modules. |
385 |
| -Various navigation options like `skipLocationChange` might not be propagated properly. |
386 |
| -Feel free to open issues or contribute pull requests for functionality that you think should be supported! |
| 10 | +A visit to `localhost:4200` shows the application. |
0 commit comments