最近本身在研究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 的微前端實現,若是這些有人看就再整理一篇文章。
第一次在掘金髮文章,但願小夥伴們多多支持啊。