最近本身在研究Angular的微前端实践,算是比较完整的从零走通了整个流程。了解到不少小伙伴也有这方面的需求,因此整理了一些内容但愿对各位小伙伴有帮助。javascript
各位看官时间有限,咱们直接进入正题。css
ng new project --prefix=prefix
建立三个项目本示例中执行的命令以下:html
ng new container --prefix=slb
ng new app1 --prefix=app1
ng new app2 --prefix=app2
npm i single-spa --save
npm i systemjs --save
npm i import-map-overrides --save
将build下的scripts修改以下:前端
"scripts": [
"node_modules/systemjs/dist/system.min.js",
"node_modules/systemjs/dist/extras/amd.min.js",
"node_modules/systemjs/dist/extras/named-exports.min.js",
"node_modules/systemjs/dist/extras/named-register.min.js",
"node_modules/import-map-overrides/dist/import-map-overrides.js"
]
复制代码
以上咱们就完成了container项目的配置工做,下面开始进入代码环节。java
在head标签下增长node
<meta name="importmap-type" content="systemjs-importmap" />
<script type="systemjs-importmap" src="/assets/import-map.json"></script>
复制代码
在body标签下增长webpack
<import-map-overrides-full></import-map-overrides-full>
复制代码
index.html 最终内容以下:web
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Container</title>
<base href="/">
<meta name="importmap-type" content="systemjs-importmap" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<script type="systemjs-importmap" src="/assets/import-map.json"></script>
</head>
<body>
<slb-root></slb-root>
<import-map-overrides-full></import-map-overrides-full>
</body>
</html>
复制代码
细心的小伙伴可能会注意到为咱们引入了一个尚未建立的文件。typescript
<script type="systemjs-importmap" src="/assets/import-map.json"></script>
复制代码
就是上面这行代码中的JSON文件。那么下一步咱们就来建立这个文件。npm
在assets目录下新建import-map.json文件,内容以下。
{
"imports": {
"app1": "http://localhost:4201/main.js",
"app2": "http://localhost:4202/main.js"
}
}
复制代码
在demo中咱们都是本地服务加载这些文件,因此这里的地址都是localhost
。4201
和4202
分别是两个微前端项目的端口。
执行ng g c spa-host
angular-cli 会帮助咱们建立一个spa-host component。这个组件会是咱们挂载微前端的地方。
在html 页面建立两个挂载元素。
<div #app1></div>
<div #app2></div>
复制代码
挂载点的数量与咱们须要挂载的微前端个数一致,在当前demo中咱们须要挂载两个项目,分别为app1和app2。
先获取挂载点:
@ViewChild('app1', { static: true }) private app1: ElementRef;
@ViewChild('app2', { static: true }) private app2: ElementRef;
复制代码
为了上述代码可以运行,咱们须要引入依赖。
import { Component, OnInit, ViewChild, ElementRef, OnDestroy } from '@angular/core';
复制代码
在获取挂载点以后,咱们就能够将另外的两个前端项目进行挂在了。
接下来咱们须要一个方法来挂载项目。
在src下建立service文件夹
建立 single-spa.service.ts
在这里service中咱们须要两个方法,一个是挂载,一个是卸载。
因此这个service的核心方法只有 mount
和 unmount
。
这里项目的挂载咱们须要依赖single-spa
提供的mountRootParcel
方法来实现。
mountRootParcel(app, { domElement });
复制代码
这个方法接受两个参数,第一个是须要挂载的项目,第二个是一个options,为咱们须要传的就是这个domElement,也就是咱们的挂载点。
这个方法会返回一个挂载的Parcel 对象,内容以下:
type Parcel = {
mount(): Promise<null>;
unmount(): Promise<null>;
update(customProps: object): Promise<any>;
getStatus():
| "NOT_LOADED"
| "LOADING_SOURCE_CODE"
| "NOT_BOOTSTRAPPED"
| "BOOTSTRAPPING"
| "NOT_MOUNTED"
| "MOUNTING"
| "MOUNTED"
| "UPDATING"
| "UNMOUNTING"
| "UNLOADING"
| "SKIP_BECAUSE_BROKEN"
| "LOAD_ERROR";
loadPromise: Promise<null>;
bootstrapPromise: Promise<null>;
mountPromise: Promise<null>;
unmountPromise: Promise<null>;
};
复制代码
从这里咱们能够发现,Parcel是咱们卸载app的依据。
因此咱们在卸载应用的时候须要执行的就是Parcel.unmount()
;
到这里咱们基本清楚咱们的挂载和卸载的实现了,下面上代码:
import { Injectable } from '@angular/core';
import { Parcel, mountRootParcel, } from 'single-spa';
import { Observable, from } from 'rxjs';
import { mapTo, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class SingleSpaService {
private loadedParcels: {
[appName: string]: Parcel
} = {};
constructor() { }
mount(appName: string, domElement: HTMLElement): Observable<void> {
return from(window.System.import(appName))
.pipe(
tap(app => {
this.loadedParcels[appName] = mountRootParcel(app, { domElement });
}),
mapTo(null)
);
}
unmount(appName: string): Observable<void> {
return from(this.loadedParcels[appName].unmount()).pipe(
tap(() => delete this.loadedParcels[appName]),
mapTo(null)
);
}
}
复制代码
tips:
loadedParcels
是咱们存储已经挂载的应用的变量。
建立完成 single-spa
service以后咱们回到 spa-host
组件来完成咱们页面的挂载和卸载。
constructor(private service: SingleSpaService) { }
复制代码
this.service.mount('app1', this.app1.nativeElement).subscribe();
this.service.mount('app2', this.app2.nativeElement).subscribe();
复制代码
在咱们的demo 中,由于是假的项目和固定的挂载数目,因此我将挂载方法写在了onInit
方法内,可是在实际的项目中挂载方法的执行应该是在你获取到数据以后。
zip(
this.service.unmount('app1'),
this.service.unmount('app2')
).toPromise();
复制代码
关于卸载的处理若是项目是挂载一次的,那么都应该在onDestory
的时候统一卸载全部挂载应用。若是是页面动态变化的,那么卸载也会发生在onChange
的时候。
import { Component, OnInit, ViewChild, ElementRef, OnDestroy } from '@angular/core';
import { SingleSpaService } from '../../service/single-spa.service';
import { zip } from 'rxjs';
@Component({
selector: 'slb-spa-host',
templateUrl: './spa-host.component.html',
styleUrls: ['./spa-host.component.scss']
})
export class SpaHostComponent implements OnInit, OnDestroy {
constructor(private service: SingleSpaService) { }
@ViewChild('app1', { static: true }) private app1: ElementRef;
@ViewChild('app2', { static: true }) private app2: ElementRef;
ngOnInit() {
this.service.mount('app1', this.app1.nativeElement).subscribe();
this.service.mount('app2', this.app2.nativeElement).subscribe();
}
async ngOnDestroy() {
await zip(
this.service.unmount('app1'),
this.service.unmount('app2')
).toPromise();
}
}
复制代码
至此,咱们就作完了spa-host
component 的所有改动。
咱们既然已经建立完这个component,接下来固然是让它起做用。
确认 SpaHostComponent
已经被引入并声明完成。若是没有那就手动完成一下。
引入component
import { SpaHostComponent } from './spa-host/spa-host.component';
复制代码
加到declarations 中
declarations: [
AppComponent,
SpaHostComponent
],
复制代码
完整代码:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { SpaHostComponent } from './spa-host/spa-host.component';
@NgModule({
declarations: [
AppComponent,
SpaHostComponent
],
imports: [
BrowserModule,
AppRoutingModule
],
providers: [
],
bootstrap: [AppComponent]
})
export class AppModule { }
复制代码
将SpaHostComponent
挂在跟路由下
const routes: Routes = [
{
path: '',
component: SpaHostComponent
}
];
复制代码
完整代码
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SpaHostComponent } from './spa-host/spa-host.component';
const routes: Routes = [
{
path: '',
component: SpaHostComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
复制代码
删除默认添加的内容只剩余router-outlet
<router-outlet></router-outlet>
复制代码
在main.js 中添加下列代码,启动single-spa
import * as singleSpa from 'single-spa';
singleSpa.start();
复制代码
上面就是所有的container 项目的改动了。
下面咱们开始修改微前端项目。在咱们demo 里面两个微前端项目是彻底相同的,因此下面咱们以app1来举例。
执行命令 ng add single-spa-angular
这条命令会帮咱们完成一下内容
single-spa-angular
src/main.single-spa.ts
src/single-spa/single-spa-props.ts
src/single-spa/asset-url.ts
EmptyRouteComponent
并引入到app-routing.module.ts
build:single-spa
和 serve:single-spa
extra-webpack.config.js
tips
关于webpack config这部分Angular 的7以及以前版本和8+的处理上不一样。
上面的命令增长了两个npm script, 可是里面的端口号是默认的4200,咱们须要修改成咱们真正使用的。这里4200是咱们的container的端口号,因此这里咱们使用4201.
将这两个脚本修改成:
"build:single-spa": "ng build --prod --deploy-url http://localhost:4201/",
"serve:single-spa": "ng serve --disable-host-check --port 4201 --deploy-url http://localhost:4201/ --live-reload false",
复制代码
将路由指向咱们建立的EmptyRouteComponent
,修改路由为以下。
const routes: Routes = [
{
path: '**',
component: EmptyRouteComponent
}
];
复制代码
providers 修改成以下
providers: [{ provide: APP_BASE_HREF, useValue: '/' }]
复制代码
完整代码:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { APP_BASE_HREF } from '@angular/common';
import { EmptyRouteComponent } from './empty-route/empty-route.component';
const routes: Routes = [
{
path: '**',
component: EmptyRouteComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: [{ provide: APP_BASE_HREF, useValue: '/' }]
})
export class AppRoutingModule { }
复制代码
最后,咱们修改一下app.component.html
,删除以前的内容。
修改成
<h1>Mien's first Micro Front-end project</h1>
复制代码
这就是为前端部分的所有改动。一样的咱们须要对app2也作一样的修改。
而后让咱们运行一下看看吧~
告诉我,你也看到了下面的内容对吗?
以上即是Angular 微前端实践 之 Single-SPA 手把手教程(上) 的所有内容的,本文的下半部分还在整理中,若是感兴趣的话请评论告诉我。
对本文中的问题,也欢迎留言提问。
若有错误,欢迎指正。
另外还有不使用single-spa 的微前端实现,若是这些有人看就再整理一篇文章。
第一次在掘金发文章,但愿小伙伴们多多支持啊。