最近自己在研究Angular的微前端实践,算是比较完整的从零走通了整个流程。了解到很多小伙伴也有这方面的需求,所以整理了一些内容希望对各位小伙伴有帮助。
各位看官时间有限,我们直接进入正题。
ng new project --prefix=prefix
创建三个项目本示例中执行的命令如下:
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项目的配置工作,下面开始进入代码环节。
在head标签下增加
<meta name="importmap-type" content="systemjs-importmap" /> <script type="systemjs-importmap" src="/assets/import-map.json"></script> 复制代码
在body标签下增加
<import-map-overrides-full></import-map-overrides-full> 复制代码
index.html 最终内容如下:
<!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> 复制代码
细心的小伙伴可能会注意到为我们引入了一个还没有创建的文件。
<script type="systemjs-importmap" src="/assets/import-map.json"></script> 复制代码
就是上面这行代码中的JSON文件。那么下一步我们就来创建这个文件。
在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 的微前端实现,如果这些有人看就再整理一篇文章。
第一次在掘金发文章,希望小伙伴们多多支持啊。