A powerful, reliable, fully-featured and production ready Micro Frontend library for Angular.
APIs consistent with angular style, currently only supports Angular, other frameworks are not supported.
English | ไธญๆๆๆกฃ
- Rendering multiple applications at the same time
- Support two mode, coexist and default that switch to another app and destroy active apps
- Support application preload
- Support style isolation
- Built-in communication between multiple applications
- Cross application component rendering
- Comprehensive examples include routing configuration, lazy loading and all features
- Introduce
- Getting Started
- Development and Build
- Data shared and Communication
- Cross Application Component rendering
- API References
- single-spa: A javascript front-end framework supports any frameworks.
- mooa: A independent-deployment micro-frontend Framework for Angular from single-spa,
planetis very similar to it, butplanetis more powerful, reliable, productively and more angular.
$ npm i @worktile/planet --save // or $ yarn add @worktile/planetimport { NgxPlanetModule } from '@worktile/planet'; @NgModule({ imports: [ CommonModule, NgxPlanetModule ] }) class AppModule {} @Component({ selector: 'app-portal-root', template: ` <nav> <a [routerLink]="['/app1']" routerLinkActive="active">ๅบ็จ1</a> <a [routerLink]="['/app2']" routerLinkActive="active">ๅบ็จ2</a> </nav> <router-outlet></router-outlet> <div id="app-host-container"></div> <div *ngIf="!loadingDone">ๅ ่ฝฝไธญ...</div> ` }) export class AppComponent implements OnInit { title = 'ngx-planet'; get loadingDone() { return this.planet.loadingDone; } constructor( private planet: Planet ) {} ngOnInit() { this.planet.setOptions({ switchMode: SwitchModes.coexist, errorHandler: error => { console.error(`Failed to load resource, error:`, error); } }); this.planet.registerApps([ { name: 'app1', hostParent: '#app-host-container', hostClass: 'thy-layout', routerPathPrefix: '/app1', preload: true, entry: "/static/app2/index.html" }, { name: 'app2', hostParent: '#app-host-container', hostClass: 'thy-layout', routerPathPrefix: '/app2', preload: true, entry: { basePath: "/static/app1/" manifest: "index.html" scripts: [ 'main.js' ], styles: [ 'styles.css' ] } } ]); // start monitor route changes // get apps to active by current path // load static resources which contains javascript and css // bootstrap angular sub app module and show it this.planet.start(); } }for NgModule application:
defineApplication('app1', { template: `<app1-root class="app1-root"></app1-root>`, bootstrap: (portalApp: PlanetPortalApplication) => { return platformBrowserDynamic([ { provide: PlanetPortalApplication, useValue: portalApp }, { provide: AppRootContext, useValue: portalApp.data.appRootContext } ]) .bootstrapModule(AppModule) .then(appModule => { return appModule; }) .catch(error => { console.error(error); return null; }); } });for Standalone application: (>= 17.0.0)
defineApplication('standalone-app', { template: `<standalone-app-root></standalone-app-root>`, bootstrap: (portalApp: PlanetPortalApplication) => { return bootstrapApplication(AppRootComponent, { providers: [ { provide: PlanetPortalApplication, useValue: portalApp }, { provide: AppRootContext, useValue: portalApp.data.appRootContext } ] }).catch(error => { console.error(error); return null; }); } });| Name | Type | Description | ไธญๆๆ่ฟฐ |
|---|---|---|---|
| name | string | Application's name | ๅญๅบ็จ็ๅๅญ |
| routerPathPrefix | string | Application route path prefix | ๅญๅบ็จ่ทฏ็ฑ่ทฏๅพๅ็ผ๏ผๆ นๆฎ่ฟไธชๅน้ ๅบ็จ |
| selector | string | selector of app root component | ๅญๅบ็จ็ๅฏๅจ็ปไปถ้ๆฉๅจ๏ผๅ ไธบๅญๅบ็จๆฏไธปๅบ็จๅจๆๅ ่ฝฝ็๏ผๆไปฅไธปๅบ็จ้่ฆๅ ๅๅปบ่ฟไธช้ๆฉๅจ่็น๏ผๅๅฏๅจ AppModule |
| entry | string | PlanetApplicationEntry | entry for micro app, contains manifest, scripts, styles | ๅ ฅๅฃ้ ็ฝฎ๏ผๅฆๆๆฏๅญ็ฌฆไธฒ่กจ็คบๅบ็จๅ ฅๅฃ index.html๏ผๅฆๆๆฏๅฏน่ฑก, manifest ไธบๅ ฅๅฃ html ๆ่ json ๆไปถๅฐๅ๏ผscripts ๅ styles ไธบๆๅฎ็่ตๆบๅ่กจ๏ผๆชๆๅฎไฝฟ็จ manifest ๆฅๅฃไธญ่ฟๅ็ๆๆ่ตๆบ๏ผbasePath ไธบๅบๆฌ่ทฏ็ฑ๏ผๆๆ็่ตๆบ่ฏทๆฑๅฐๅๅไผๅธฆไธ basePath |
| manifest | string | manifest json file path deprecated please use entry | manifest.json ๆไปถ่ทฏๅพๅฐๅ๏ผๅฝ่ฎพ็ฝฎไบ่ทฏๅพๅไผๅ ๅ ่ฝฝ่ฟไธชๆไปถ๏ผ็ถๅๆ นๆฎ scripts ๅ styles ๆไปถๅๅปๆพๅฐๅน้ ็ๆไปถ๏ผๅ ไธบ็ไบง็ฏๅข็้ๆ่ตๆไปถๆฏ hash ไนๅ็ๅฝๅ๏ผ้่ฆๅจๆ่ทๅ |
| scripts | string[] | javascript static resource paths deprecated please use entry.scripts | JS ้ๆ่ตๆบๆไปถ่ฎฟ้ฎๅฐๅ |
| styles | string[] | style static resource paths deprecated please use entry.styles | ๆ ทๅผ้ๆ่ตๆบๆไปถ่ฎฟ้ฎๅฐๅ |
| resourcePathPrefix | string | path prefix of scripts and styles deprecated please use entry.basePath | ่ๆฌๅๆ ทๅผๆไปถ่ทฏๅพๅ็ผ๏ผๅคไธช่ๆฌๅฏไปฅ้ฟๅ ้ๅคๅๅๆ ท็ๅ็ผ |
| hostParent | string or HTMLElement | parent element for render | ๅบ็จๆธฒๆ็ๅฎนๅจๅ ็ด , ๆๅฎๅญๅบ็จๆพ็คบๅจๅชไธชๅ ็ด ๅ ้จ |
| hostClass | string | added class for host which is selector | ๅฎฟไธปๅ ็ด ็ Class๏ผไนๅฐฑๆฏๅจๅญๅบ็จๅฏๅจ็ปไปถไธ่ฟฝๅ ็ๆ ทๅผ |
| switchMode | default or coexist | it will be destroyed when set to default, it only hide app when set to coexist | ๅๆขๅญๅบ็จ็ๆจกๅผ๏ผ้ป่ฎคๅๆขไผ้ๆฏ๏ผ่ฎพ็ฝฎ coexist ๅๅชไผ้่ |
| preload | boolean | start preload or not | ๆฏๅฆๅฏ็จ้ขๅ ่ฝฝ๏ผๅฏๅจๅๅทๆฐ้กต้ข็ญๅฝๅ้กต้ข็ๅบ็จๆธฒๆๅฎๆฏๅ้ขๅ ่ฝฝๅญๅบ็จ |
| loadSerial | boolean | serial load scripts | ๆฏๅฆไธฒ่กๅ ่ฝฝ่ๆฌ้ๆ่ตๆบ |
import { GlobalEventDispatcher } from "@worktile/planet"; // app1 root module export class AppModule { constructor(private globalEventDispatcher: GlobalEventDispatcher) { this.globalEventDispatcher.register('open-a-detail').subscribe(event => { // dialog.open(App1DetailComponent); }); } } // in other apps export class OneComponent { constructor(private globalEventDispatcher: GlobalEventDispatcher) { } openDetail() { this.globalEventDispatcher.dispatch('open-a-detail', payload); } } import { PlanetComponentLoader } from "@worktile/planet"; // in app1 export class AppModule { constructor(private planetComponentLoader: PlanetComponentLoader) { this.planetComponentLoader.register([App1ProjectListComponent]); } } Load app1-project-list (selector) component of app1 in other app via PlanetComponentOutlet
<ng-container *planetComponentOutlet="'app1-project-list'; app: 'app1'; initialState: { search: 'xxx' }"></ng-container> // or <ng-container planetComponentOutlet="app1-project-list" planetComponentOutletApp="app1" [planetComponentOutletInitialState]="{ term: 'xxx' }" (planetComponentLoaded)="planetComponentLoaded($event)"> </ng-container>Load app1-project-list component of app1 in other app via PlanetComponentLoader, must be call dispose
@Component({ ... }) export class OneComponent { private componentRef: PlanetComponentRef; constructor(private planetComponentLoader: PlanetComponentLoader) { } openDetail() { this.planetComponentLoader.load('app1', 'app1-project-list', { container: this.containerElementRef, initialState: {} }).subscribe((componentRef) => { this.componentRef = componentRef; }); } ngOnDestroy() { this.componentRef?.dispose(); } }Because the portal app and sub app are packaged through webpack, there will be conflicts in module dependent files, we should set up additional config runtimeChunk through @angular-builders/custom-webpack, we expect webpack 5 to support micro frontend better.
// extra-webpack.config.js { optimization: { runtimeChunk: false } }; Similar to the reasons above, we should set vendorChunk as false for build and serve in angular.json
... "build": { "builder": "@angular-builders/custom-webpack:browser", "options": { "customWebpackConfig": { "path": "./examples/app2/extra-webpack.config.js", "mergeStrategies": { "module.rules": "prepend" }, "replaceDuplicatePlugins": true }, ... "vendorChunk": false, ... }, }, "serve": { "builder": "@angular-builders/custom-webpack:dev-server", "options": { ... "vendorChunk": false ... } } ... this is TypeScript's issue, details see an-accessor-cannot-be-declared should setting skipLibCheck as true
"compilerOptions": { "skipLibCheck": true } In webpack 4 multiple webpack runtimes could conflict on the same HTML page, because they use the same global variable for chunk loading. To fix that it was needed to provide a custom name to the output.jsonpFunction configuration, details see Automatic unique naming.
you should set a unique name for each sub application in extra-webpack.config.js
output: { jsonpFunction: "app1" } npm run start // open http://localhost:3000 or npm run serve:portal // 3000 npm run serve:app1 // 3001 npm run serve:app2 // 3002 // test npm run test Thanks goes to these wonderful people (emoji key):
why520crazy ๐ฌ ๐ผ ๐ป ๐จ ๐ ๐ ๐ ๐ง ๐ ๐ | Walker ๐ป ๐ก ๐ง ๐ | whyour ๐ป | ๅผ ๅจ ๐ป | luxiaobei ๐ | mario_ma ๐ป |
This project follows the all-contributors specification. Contributions of any kind welcome!
