[](https://circleci.com/gh/worktile/ngx-planet) [![Coverage Status][coveralls-image]][coveralls-url] [](https

[![Coverage Status][coveralls-image]][coveralls-url]


!npm bundle size (scoped) 
[coveralls-image]: https://coveralls.io/repos/github/worktile/ngx-planet/badge.svg?branch=master
[coveralls-url]: https://coveralls.io/github/worktile/ngx-planet
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
- 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, but planet is more powerful, reliable, productively and more angular.
``bash`
$ npm i @worktile/planet --save
// or
$ yarn add @worktile/planet
!ngx-planet-micro-front-end.gif
`
import { NgxPlanetModule } from '@worktile/planet';
@NgModule({
imports: [
CommonModule,
NgxPlanetModule
]
})
class AppModule {}
`
`ts
@Component({
selector: 'app-portal-root',
template:
})
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();
}
}
`$3
for NgModule application:
`ts
defineApplication('app1', {
template: ,
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)
`ts
defineApplication('standalone-app', {
template: ,
bootstrap: (portalApp: PlanetPortalApplication) => {
return bootstrapApplication(AppRootComponent, {
providers: [
{
provide: PlanetPortalApplication,
useValue: portalApp
},
{
provide: AppRootContext,
useValue: portalApp.data.appRootContext
}
]
}).catch(error => {
console.error(error);
return null;
});
}
});
`Documents
$3
| 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 | 是否串行加载脚本静态资源 |$3
`
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);
}
}
`$3
`
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`html
// or
planetComponentOutletApp="app1"
[planetComponentOutletInitialState]="{ term: 'xxx' }"
(planetComponentLoaded)="planetComponentLoaded($event)">
`Load
app1-project-list component of app1 in other app via PlanetComponentLoader, must be call dispose`ts
@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();
}
}
`FAQ
$3
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
}
};
`$3
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
...
}
}
...
`$3
this is TypeScript's issue, details see an-accessor-cannot-be-declared
should setting skipLibCheck as true
`
"compilerOptions": {
"skipLibCheck": true
}
`$3
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" }
`Development
`
npm run start // open http://localhost:3000or
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!