微前端架構(gòu)之single-spa
single-spa是什么
Single-spa 是一個將多個單頁面應(yīng)用聚合為一個整體應(yīng)用的 JavaScript 微前端框架。
好處:
- 在同一頁面上使用多個前端框架 而不用刷新頁面 (React, AngularJS, Angular, Ember, 你正在使用的框架)
- 獨立部署每一個單頁面應(yīng)用
- 新功能使用新框架,舊的單頁應(yīng)用不用重寫可以共存
- 改善初始加載時間,遲加載代碼
single-spa做了什么
single-spa是一個頂層路由。當(dāng)路由處于活動狀態(tài)時,它將下載并執(zhí)行該路由下的相關(guān)代碼。
路由的代碼被稱為應(yīng)用,每個代碼都可以(可選)擁有自己的git倉庫、CI進(jìn)程,并且可以獨立部署。這些應(yīng)用即可以用相同框架實現(xiàn),也可以用不同框架實現(xiàn)。
single-spa包括些什么:
- 1、Applications,每個應(yīng)用程序本身就是一個完整的 SPA (某種程度上)。 每個應(yīng)用程序都可以響應(yīng) url 路由事件,并且必須知道如何從 DOM 中初始化、掛載和卸載自己。 傳統(tǒng) SPA 應(yīng)用程序和 Single SPA 應(yīng)用程序的主要區(qū)別在于,
它們必須能夠與其他應(yīng)用程序共存,而且它們沒有各自的 html 頁面。
例如,React 或 Vue spa 就是應(yīng)用程序。 當(dāng)激活時,它們監(jiān)聽 url 路由事件并將內(nèi)容放在 DOM上。 當(dāng)它們處于非活動狀態(tài)時,它們不偵聽 url 路由事件,并且完全從 DOM 中刪除。
- 一個 single-spa-config配置, 這是html頁面和向Single SPA注冊應(yīng)用程序的JavaScript。每個應(yīng)用程序都注冊了三件東西
- A name (應(yīng)用的標(biāo)識)
- A function (加載應(yīng)用程序的代碼)
- A function (確定應(yīng)用程序何時處于活動狀態(tài)/非活動狀態(tài))
single-spa的使用方式
Single-spa 適用于 ES5、 ES6 + 、 TypeScript、 Webpack、 SystemJS、 Gulp、 Grunt、 Bower、 ember-cli 或 任何可用的構(gòu)建系統(tǒng)。 您可以 npm 安裝它,jspm 安裝它,如果您愿意,甚至可以使用 <script> 標(biāo)簽。
新項目中使用single-spa
1、創(chuàng)建相當(dāng)簡單 create-single-spa cli
https://github.com/single-spa/create-single-spa/
# 全局安裝
npm install --global create-single-spa
# or
yarn global add create-single-spa
# 之后執(zhí)行
create-single-spa
# 本地安裝
npm init single-spa
# or
npx create-single-spa
# or
yarn create single-spa
推薦設(shè)置
我們建議使用瀏覽器內(nèi)ES模塊 + import maps (或者SystemJS填充這些,如果你需要更好的瀏覽器支持)的設(shè)置。這種設(shè)置有幾個優(yōu)點:
- 公共模塊易于管理,并且只下載一次。如果使用SystemJS,也可以預(yù)加載它們來提高速度。
- 共享代碼/函數(shù)/變量就像導(dǎo)入/導(dǎo)出一樣簡單,就像在一個整體中設(shè)置一樣。
- 延遲加載應(yīng)用程序很容易,這使您能夠加速初始加載時間。
- 每個應(yīng)用程序(又名微服務(wù),又名ES模塊)都可以獨立開發(fā)和部署。團(tuán)隊可以按照自己的進(jìn)度工作、實驗(在組織定義的合理范圍內(nèi))、QA和部署。這通常也意味著發(fā)布周期可以縮短到幾天,而不是幾周或幾個月。
- 很棒的開發(fā)人員體驗(DX):轉(zhuǎn)到dev環(huán)境并添加一個導(dǎo)入映射,該映射將應(yīng)用程序的url指向您的本地主機(jī)。請參閱下面的章節(jié)了解詳細(xì)信息。
single-spa中微前端的類型
- single-spa applications:為一組特定路由渲染組件的微前端。
- single-spa parcels: 不受路由控制,渲染組件的微前端。
- utility modules: 非渲染組件,用于暴露共享javascript邏輯的微前端。
| 容器Root | 應(yīng)用程序 | 沙箱 | 公共模塊 |
|---|---|---|---|
| 主路由 | 有多個路由 | 無路由 | 無路由 |
| API | 聲明API | 必要的API | 沒有single-spa API |
| 渲染UI | 渲染UI | 渲染UI | 不直接渲染UI |
| 生命周期 | single-spa管理生命周期 | 自定義管理生命周期 | 沒有生命周期 |
| 什么時候用 | 核心構(gòu)建模塊 | 僅在多個框架中需要 | 共享通用邏輯時使用 |
應(yīng)用程序
single-spa 提供 registerApplication API注冊應(yīng)用
沙箱
主要是讓您在多個框架中編寫應(yīng)用程序時可以在應(yīng)用程序之間重用UI。
管理parcels的生命周期
mountParcel 或 mountRootParcel 將立即掛載parcel并返回這個parcel對象。 需要卸載需要手動調(diào)用 parcel的 unmount.
Parcels 最適合在框架之間共享UI部分 ???
如: application1 用Vue編寫,包含創(chuàng)建用戶的所有UI和邏輯。 application2是用React編寫的,需要創(chuàng)建一個用戶。 使用single-spa parcels可以讓您包裝application2Vue組件。盡管框架不同,但它可以在`application2'內(nèi)部運行。 將Parcels視為Web組件的single-spa特定實現(xiàn)。
公共模塊
共享通用邏輯,可以是一個普通的js對象
如: 登錄授權(quán)、 讀取數(shù)據(jù)fetch
1、每個應(yīng)用都訪問服務(wù)器,這會在每個應(yīng)用中創(chuàng)建重復(fù)的工作;
2、使用公共模塊,創(chuàng)建一個實現(xiàn)授權(quán)邏輯的模塊,通過導(dǎo)出/導(dǎo)入的方式使用這些授權(quán)
Root Config
根目錄下的兩個配置,用于啟動single-spa應(yīng)用
- 所有微前端應(yīng)用共享的根Html頁面 【index.ejs】
- 調(diào)用
singleSpa.registerApplication()的js 【study-root-config.js】
// single-spa-config.js
import { registerApplication, start } from 'single-spa';
// param1: 一個應(yīng)用的標(biāo)識
// param2: Function 一個應(yīng)用要執(zhí)行的代碼
// param3: Function 何時激活這些應(yīng)用:主路由
// param4: 可選的擴(kuò)展參數(shù)
registerApplication(
'app2',
() => import('src/app2/main.js'),
(location) => location.pathname.startsWith('/app2'),
{ some: 'value' }
);
registerApplication({
name: 'app1',
app: () => import('src/app1/main.js'),
activeWhen: '/app1',
customProps: {
some: 'value',
}
);
start();
參數(shù)說明
name:
應(yīng)用的標(biāo)識,必須Sting-
Loading Function or Application
registerApplication可以是一個Promise類型的 加載函數(shù),也可以是一個已經(jīng)被解析的應(yīng)用。const application = { bootstrap: () => Promise.resolve(), //bootstrap function mount: () => Promise.resolve(), //mount function unmount: () => Promise.resolve(), //unmount function } registerApplication('applicationName', application, activityFunction) 加載函數(shù)
registerApplication的第二個參數(shù)必須是返回promise的函數(shù)(或"async function"方法)。這個函數(shù)沒有入?yún)?,會在?yīng)用第一次被下載時調(diào)用。返回的Promise resolve之后的結(jié)果必須是一個可以被解析的應(yīng)用。常見的實現(xiàn)方法是使用import加載:() => import('/path/to/application.js')-
激活函數(shù)
第3個參數(shù)要求是一個純函數(shù)(只依賴參數(shù),不產(chǎn)生副作用), 根據(jù) location.path決定哪個應(yīng)用被激活。
single-spa根據(jù)頂級路由查找應(yīng)用,每個應(yīng)用自己處理自身的子路由。
支持通配符方式配置:'/users/:userId/profile'
支持多路徑方式配置:['/pathname/#/hash', '/app1']包含以下情況
1、
hashchange or popstate事件觸發(fā)時
2、pushState or replaceState被調(diào)用時
3、在single-spa上手動調(diào)用[triggerAppChange] 方法
4、checkActivityFunctions方法被調(diào)用時 -
自定義屬性
第4個參數(shù):參數(shù)會傳給single-spa的lifecycle函數(shù)singleSpa.registerApplication({ name: 'myApp', app: () => import('src/myApp/main.js'), activeWhen: ['/myApp', (location) => location.pathname.startsWith('/some/other/path')], customProps: { some: 'value', }, }); singleSpa.registerApplication({ name: 'myApp', app: () => import('src/myApp/main.js'), activeWhen: ['/myApp', (location) => location.pathname.startsWith('/some/other/path')], // 函數(shù)時,參數(shù)1:應(yīng)用名:myapp, 參數(shù)2: window.location customProps: (name, location) => ({ some: 'value', }), }); -
最后調(diào)用
singleSpa.start()
start()方法,必須被single-spa的配置文件調(diào)用, 這樣應(yīng)用才會真的被掛載。 在start被調(diào)用之前,應(yīng)用先被下載,但不會初始化/掛載/卸載。import { start } from 'single-spa'; /*在注冊應(yīng)用之前調(diào)用start意味著single-spa可以立即安裝應(yīng)用,無需等待單頁應(yīng)用的任何初始設(shè)置。*/ start(); // 注冊應(yīng)用。。。。 同時注冊兩個路由
一個path的變動,同時兩個應(yīng)用被激活?? 可以。
<div>需要一個id,這個id的以single-spa-application前綴開頭,后面接著你的應(yīng)用的名字。比如,如果你的應(yīng)用名字叫做app-name,就創(chuàng)建一個id為 single-spa-application:app-name的div。
<div id="single-spa-application:app-name"></div>
<div id="single-spa-application:other-app"></div>
構(gòu)建應(yīng)用
single-spa 應(yīng)用與普通的單頁面是一樣的,只不過它沒有HTML頁面。在一個single-spa中,有N多被注冊的應(yīng)用,這些應(yīng)用可以框架不同,自己維護(hù)自己的路由,只需要掛載便可以渲染自己的頁面及功能。
“掛載”(mounted)的概念指的是被注冊的應(yīng)用內(nèi)容是否已展示在DOM上。我們可通過應(yīng)用的activity function來判斷其是否已被掛載。未掛載前,一直休眠。
創(chuàng)建并注冊應(yīng)用
要添加一個應(yīng)用,首先需要注冊該應(yīng)用。一旦應(yīng)用被注冊后,必須在其入口文件(entry point)實現(xiàn)下面提到的各個生命周期函數(shù)。
生命周期
- 下載(loaded): 注冊的應(yīng)用在第1次 activity時開始下載,下載過程中盡可能執(zhí)行少的操作,如果需要下載時執(zhí)行的操作,可以放到子應(yīng)用入口文件中。
- 初始化(bootstrap/initialized):required 第1次被掛載前執(zhí)行一次
- 被掛載(mounted) required 應(yīng)用被激活時執(zhí)行,會根據(jù)當(dāng)前url激活主路由,創(chuàng)建dom,監(jiān)聽事件,render等,子路由的改變(如:hashchange 或 popstate)不會再觸發(fā),需要應(yīng)用自己處理
- 卸載(unmounted) required 應(yīng)用由激活變?yōu)槲醇せ顣r觸發(fā),會清理掛載應(yīng)用的dom,event,內(nèi)存,全局變量,消息訂閱等
- 被移除(unloaded) 可選 無代表應(yīng)用無需被移除,移除的應(yīng)用,下次激活時,會重新初始化??梢詫崿F(xiàn) 熱下載。
注:
1、bootstrap, mount, and unmount的實現(xiàn)是必須的,unload則是可選的
2、生命周期函數(shù)必須有返回值,可以是Promise或者async函數(shù)
3、如果導(dǎo)出的是函數(shù)數(shù)組而不是單個函數(shù),這些函數(shù)會被依次調(diào)用,對于promise函數(shù),會等到resolve之后再調(diào)用下一個函數(shù)
4、如果 single-spa 未啟動,各個應(yīng)用會被下載,但不會被初始化、掛載或卸載。
超時配置
millis: 最終控制臺輸出的警告毫秒數(shù)
warningMillis: 警告每隔多少毫秒輸出一次
切換應(yīng)用時的過渡
在生命周期函數(shù)中自己實現(xiàn)過濾效果
demo:
https://github.com/frehner/singlespa-transitions
https://github.com/reactjs/react-transition-group
舊項目遷移至single-spa
拆分應(yīng)用
前端系統(tǒng)應(yīng)用
-
1、一個代碼倉庫, 一個build包
優(yōu)點:容易部署,有單一版本控制的優(yōu)點(monorepo)
不足:項目越大時,打包越慢;構(gòu)建部署在捆綁在一起,不能臨時發(fā)版 -
2、NPM包
優(yōu)點:開發(fā)熟悉,易實現(xiàn);發(fā)布到npm前可以分別打包
不足:父應(yīng)用必須重裝子應(yīng)用重新構(gòu)建部署 -
2、動態(tài)加載模塊
優(yōu)點:靈活,代碼獨立
不足:搭建難度稍大
實現(xiàn):- web服務(wù)器,創(chuàng)建動態(tài)腳本加載子應(yīng)用正確版本;
- 使用模塊加載,如: systemJs在瀏覽器動態(tài)下載并執(zhí)行js
遷移現(xiàn)在應(yīng)用
三步
1、創(chuàng)建一個single-spa配置
2、將spa應(yīng)用轉(zhuǎn)為注冊應(yīng)用
3、調(diào)整html,使用single-spa配置生效
1、實現(xiàn)生命周期
single-spa 生態(tài)系統(tǒng) 包含了single-spa對大部分框架的支持
https://single-spa.js.org/docs/ecosystem/
自己實現(xiàn),就需要在 unmount 中,能夠清理其 DOM 節(jié)點,DOM 事件偵聽(所有的事件偵聽,尤其是 hashchange 和 popstate)以及釋放內(nèi)存。
2、解決css、font、script依賴問題
現(xiàn)有spa應(yīng)用轉(zhuǎn)為無html應(yīng)用后,這些資源依賴問題都需要解決:一種方案全部打包到j(luò)s中; 其他方案呢?
沙箱 Parcels
single-spa的一個高級特性,與框架無關(guān),api與注冊應(yīng)用一致,不同的是:parcel組件需要手動掛載,而不是通過 activity 方法被動激活。在不熟悉它之前,盡量不要用。
示例
// parcel 的實現(xiàn)
const parcelConfig = {
bootstrap() {
// 初始化
return Promise.resolve()
},
mount() {
// 使用某個框架來創(chuàng)建和初始化dom
return Promise.resolve()
},
unmount() {
// 使用某個框架卸載dom,做其他的清理工作
return Promise.resolve()
}
}
// 如何掛載parcel
const domElement = document.getElementById('place-in-dom-to-mount-parcel')
const parcelProps = {domElement, customProp1: 'foo'}
const parcel = singleSpa.mountRootParcel(parcelConfig, parcelProps)
// parcel 被掛載,在mountPromise中結(jié)束掛載
parcel.mountPromise.then(() => {
console.log('finished mounting parcel!')
// 如果我們想重新渲染parcel,可以調(diào)用update生命周期方法,其返回值是一個 promise
parcelProps.customProp1 = 'bar'
return parcel.update(parcelProps)
})
.then(() => {
// 在此處調(diào)用unmount生命周期方法來卸載parcel. 返回promise
return parcel.unmount()
})
Pacel配置
一個parcel只是一個由3到4個方法組成的對象。每個方法返回的都是一個prmise。 生命周期與應(yīng)用基本一致。
- 初始化(Bootstrap) 在parcel第一次掛載前調(diào)用一次
- 掛載(mount) 在mountParcel方法被調(diào)用且parcel未掛載時觸發(fā),一般會創(chuàng)建DOM元素、初始化事件監(jiān)聽等,從而為用戶提供展示內(nèi)容。
- 卸載(unmount) parcel已經(jīng)被掛載,且滿足下列某個條件:1、unmount()被調(diào)用; 2、父parcel或者應(yīng)用被卸載
- 更新(Update) 可選 調(diào)用parcel.update()時觸發(fā),使用者調(diào)用前需確認(rèn)parcel已實現(xiàn)
single-spa的API
參考文檔: https://zh-hans.single-spa.js.org/docs/api
single-spa的擴(kuò)展
一般來說,微前端需要解決的問題分為兩大類:
1、應(yīng)用的加載與切換
2、應(yīng)用的隔離與通信
應(yīng)用的加載與切換需要解決的問題包括:路由問題、應(yīng)用入口、應(yīng)用加載;應(yīng)用的隔離與通信需要解決的問題包括:js隔離、css樣式隔離、應(yīng)用間通信。
single-spa很好地解決了路由和應(yīng)用入口兩個問題,但并沒有解決應(yīng)用加載問題,而是將該問題暴露出來由使用者實現(xiàn)(一般可以用system.js或原生script標(biāo)簽來實現(xiàn));qiankun在此基礎(chǔ)上封裝了一個應(yīng)用加載方案(即import-html-entry),并給出了js隔離、css樣式隔離和應(yīng)用間通信三個問題的解決方案,同時提供了預(yù)加載功能。
single-spa原理
應(yīng)用入口
single-spa采用的是協(xié)議入口,即只要實現(xiàn)了single-spa的入口協(xié)議規(guī)范,它就是可加載的應(yīng)用。single-spa的規(guī)范要求應(yīng)用入口必須暴露出以下三個生命周期鉤子函數(shù),且必須返回Promise,以保證single-spa可以注冊回調(diào)函數(shù):
應(yīng)用加載
<script type="systemjs-importmap">
{
"imports": {
"app1": "http://localhost:8080/app1.js",
"app2": "http://localhost:8081/app2.js",
"single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js"
}
}
</script>
... // system.js的相關(guān)依賴文件
<script>
(function(){
// 加載single-spa
System.import('single-spa').then((res)=>{
var singleSpa = res;
// 注冊子應(yīng)用
singleSpa.registerApplication('app1',
() => System.import('app1'),
location => location.hash.startsWith(`#/app1`);
);
singleSpa.registerApplication('app2',
() => System.import('app2'),
location => location.hash.startsWith(`#/app2`);
);
// 啟動single-spa
singleSpa.start();
})
})()
</script>
// single-spa 的start方法
export function start(opts) {
started = true;
if (opts && opts.urlRerouteOnly) {
setUrlRerouteOnly(opts.urlRerouteOnly);
}
if (isInBrowser) {
reroute();
}
}
single-spa的弊端:
首先我們必須手動實現(xiàn)應(yīng)用加載邏輯,挨個羅列子應(yīng)用需要加載的資源,這在大型項目里是十分困難的(特別是使用了文件名hash時);另外它只能以js文件為入口,無法直接以html為入口,這使得嵌入子應(yīng)用變得很困難,也正因此,single-spa不能直接加載jQuery應(yīng)用。
single-spa只是負(fù)責(zé)把應(yīng)用加載到一個頁面中,至于應(yīng)用能否協(xié)同工作,是很難保證的
qiankun解決方案
https://github.com/umijs/qiankun
1、應(yīng)用加載
使用npm插件 import-html-entry
主要方法:importHTML(url, opts = {})
簡單點說:importHtml 通過fetch獲取遠(yuǎn)程的腳本、樣式文件內(nèi)容, 然后通過正則表達(dá)式,把js,css提取出來,放到各自的數(shù)組里,js能過eval執(zhí)行,并導(dǎo)出供其他模塊調(diào)用
2、css,js隔離
- 通過importHtml 加載html并把外部樣式轉(zhuǎn)為內(nèi)部樣式(使用類個shandow dom 或 vue scope)方式, 實現(xiàn)樣式隔離
- execScripts方法: 為應(yīng)用生成一個window的代理對象,作為參數(shù)傳入,以保證不影響全局window; 在ie11通過快照方式實現(xiàn)隔離
//正常實現(xiàn)js隔離
(function(window, arguments){
// do something
})(window)
// execScripts
(function(window, arguments){
// do something
})(window.proxy)
4、應(yīng)用通信
// 基座中
import { initGlobalState, MicroAppStateActions } from 'qiankun';
const initialState = {};
const actions: MicroAppStateActions = initGlobalState(initialState);
export default actions;
// 子應(yīng)用中監(jiān)聽
actions.onGlobalStateChange (globalState, oldGlobalState) {
...
}
// 子應(yīng)用中修改
actions.setGlobalState(...);
webpack5 模塊聯(lián)邦 VS single-spa
模塊聯(lián)邦: webpack 受打包工具 和 生態(tài)的限制,
single-spa: 已經(jīng)有一些成熟的解決方案:qiankun & 京東的MicroApp
京東出品微前端框架MicroApp介紹與落地實踐
https://mp.weixin.qq.com/s/6A6TqQpWgN1_KoxUMx3FFw
QA:
如何在應(yīng)用程序間共享狀態(tài)
1、建議盡量避免應(yīng)用共享狀態(tài),如果出現(xiàn),可以優(yōu)先考慮重新劃分應(yīng)用的邊界
2、實現(xiàn)方案:
- 創(chuàng)建可以緩存請求及其響應(yīng)的共享API請求庫。如果同一個API被多個應(yīng)用重復(fù)命中,則使用緩存數(shù)據(jù)。
- 將共享狀態(tài)公開為導(dǎo)出,其他的庫可以導(dǎo)入它??捎^測值(如:RxJS) 在這里很有用,因為他們能夠?qū)⑿轮盗魇絺鬏斀o訂閱服務(wù)器。
- 使用custom browser events來交流。
- 使用cookies, local/session storage或其他能夠存取狀態(tài)的工具。
參考文檔:
https://single-spa.js.org/
https://single-spa.js.org/docs/examples/
https://github.com/systemjs/systemjs
SystemJS >=3 已實現(xiàn)IE11的polyfill 目前已到 6.10.1