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,
planet
is very similar to it, butplanet
is more powerful, reliable, productively and more angular.
$ npm i @worktile/planet --save
// or
$ yarn add @worktile/planet
import { 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
- Ivy render engine
- Supports Other frameworks as React and Vue
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!