當(dāng)學(xué)習(xí)成為了習(xí)慣,知識(shí)也就變成了常識(shí)。 感謝各位的 點(diǎn)贊、收藏和評(píng)論。
新視頻和文章會(huì)第一時(shí)間在微信公眾號(hào)發(fā)送,歡迎關(guān)注:李永寧lyn
文章已收錄到 github 倉(cāng)庫(kù) liyongning/blog,歡迎 Watch 和 Star。
簡(jiǎn)介
從基本使用 -> 部署 -> 框架源碼分析 -> 手寫(xiě)框架,帶你全方位刨析 single-spa 框架
前序
目的
會(huì)使用
single-spa
開(kāi)發(fā)項(xiàng)目,然后打包部署上線刨析
single-spa
的源碼原理手寫(xiě)一個(gè)自己的
single-spa
框架
過(guò)程
編寫(xiě)示例項(xiàng)目
打包部署
框架源碼解讀
手寫(xiě)框架
關(guān)于微前端
的介紹這里就不再贅述了,網(wǎng)上有很多的文章,本文的重點(diǎn)在于刨析微前端框架single-spa
的實(shí)現(xiàn)原理。
single-spa
是一個(gè)很好的微前端基礎(chǔ)框架,qiankun
框架就是基于single-spa
來(lái)實(shí)現(xiàn)的,在single-spa
的基礎(chǔ)上做了一層封裝,也解決了single-spa
的一些缺陷。
因?yàn)?code>single-spa是一個(gè)基礎(chǔ)的微前端框架,了解了它的實(shí)現(xiàn)原理,再去看其它的微前端框架,就會(huì)非常容易了。
提示
先熟悉基本使用,熟悉常用的API,可通過(guò)示例項(xiàng)目 + 官網(wǎng)相結(jié)合來(lái)達(dá)成
如果基礎(chǔ)比較好,可以先讀后面的
手寫(xiě) single-spa 框架
部分,再回來(lái)閱讀源碼,效果可能會(huì)更好文章中涉及到的所有代碼都在 github(示例項(xiàng)目 +
single-spa
源碼分析 + 手寫(xiě)single-spa
框架 +single-spa-vue
源碼分析)
示例項(xiàng)目
新建項(xiàng)目目錄,接下來(lái)的所有代碼都會(huì)在該目錄中完成
mkdir micro-frontend && cd micro-frontend
示例代碼都是通過(guò)vue
來(lái)編寫(xiě)的,當(dāng)然也可以采用其它的,比如react
或者原生JS
等
子應(yīng)用 app1
新建子應(yīng)用
vue create app1
按圖選擇,去除一切項(xiàng)目不需要的干擾項(xiàng),后面一路回車(chē),等待應(yīng)用創(chuàng)建完畢
配置子應(yīng)用
以下所有的操作都在項(xiàng)目根目錄
/micro-frontend/app1
下完成
vue.config.js
在項(xiàng)目根目錄下新建vue.config.js
文件
const package = require('./package.json')
module.exports = {
// 告訴子應(yīng)用在這個(gè)地址加載靜態(tài)資源,否則會(huì)去基座應(yīng)用的域名下加載
publicPath: '//localhost:8081',
// 開(kāi)發(fā)服務(wù)器
devServer: {
port: 8081
},
configureWebpack: {
// 導(dǎo)出umd格式的包,在全局對(duì)象上掛載屬性package.name,基座應(yīng)用需要通過(guò)這個(gè)全局對(duì)象獲取一些信息,比如子應(yīng)用導(dǎo)出的生命周期函數(shù)
output: {
// library的值在所有子應(yīng)用中需要唯一
library: package.name,
libraryTarget: 'umd'
}
}
}
安裝single-spa-vue
npm i single-spa-vue -S
single-spa-vue
負(fù)責(zé)為vue
應(yīng)用生成通用的生命周期鉤子,在子應(yīng)用注冊(cè)到single-spa
的基座應(yīng)用時(shí)需要用到
改造入口文件
// /src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue'
Vue.config.productionTip = false
const appOptions = {
el: '#microApp',
router,
render: h => h(App)
}
// 支持應(yīng)用獨(dú)立運(yùn)行、部署,不依賴于基座應(yīng)用
if (!window.singleSpaNavigate) {
delete appOptions.el
new Vue(appOptions).$mount('#app')
}
// 基于基座應(yīng)用,導(dǎo)出生命周期函數(shù)
const vueLifecycle = singleSpaVue({
Vue,
appOptions
})
export function bootstrap (props) {
console.log('app1 bootstrap')
return vueLifecycle.bootstrap(() => {})
}
export function mount (props) {
console.log('app1 mount')
return vueLifecycle.mount(() => {})
}
export function unmount (props) {
console.log('app1 unmount')
return vueLifecycle.unmount(() => {})
}
更改視圖文件
<!-- /views/Home.vue -->
<template>
<div class="home">
<h1>app1 home page</h1>
</div>
</template>
<!-- /views/About.vue -->
<template>
<div class="about">
<h1>app1 about page</h1>
</div>
</template>
環(huán)境配置文件
.env
應(yīng)用獨(dú)立運(yùn)行時(shí)的開(kāi)發(fā)環(huán)境配置
NODE_ENV=development
VUE_APP_BASE_URL=/
.env.micro
作為子應(yīng)用運(yùn)行時(shí)的開(kāi)發(fā)環(huán)境配置
NODE_ENV=development
VUE_APP_BASE_URL=/app1
.env.buildMicro
作為子應(yīng)用構(gòu)建生產(chǎn)環(huán)境bundle
時(shí)的環(huán)境配置,但這里的NODE_ENV
為development
,而不是production
,是為了方便,這個(gè)方便其實(shí)single-spa
帶來(lái)的弊端(js entry的弊端)
NODE_ENV=development
VUE_APP_BASE_URL=/app1
修改路由文件
// /src/router/index.js
// ...
const router = new VueRouter({
mode: 'history',
// 通過(guò)環(huán)境變量來(lái)配置路由的 base url
base: process.env.VUE_APP_BASE_URL,
routes
})
// ...
修改package.json
中的script
{
"name": "app1",
// ...
"scripts": {
// 獨(dú)立運(yùn)行
"serve": "vue-cli-service serve",
// 作為子應(yīng)用運(yùn)行
"serve:micro": "vue-cli-service serve --mode micro",
// 構(gòu)建子應(yīng)用
"build": "vue-cli-service build --mode buildMicro"
},
// ...
}
啟動(dòng)應(yīng)用
應(yīng)用獨(dú)立運(yùn)行
npm run serve
當(dāng)然下面的啟動(dòng)方式也可以,只不過(guò)會(huì)在pathname
的開(kāi)頭加了/app1
前綴
npm run serve:micro
作為子應(yīng)用運(yùn)行
npm run serve:micro
作為獨(dú)立應(yīng)用訪問(wèn)
子應(yīng)用 app2
在/micro-frontend目錄下新建子應(yīng)用
app2,步驟同
app1,只需把過(guò)程中出現(xiàn)的'app1'字樣改成'app2'即可,
vue.config.js中的
8081改成
8082`
啟動(dòng)應(yīng)用,作為獨(dú)立應(yīng)用訪問(wèn)
子應(yīng)用 app3(react)
這部分內(nèi)容于
2020/08/30
添加,為什么后來(lái)添加這部分內(nèi)容呢?是因?yàn)橛型瑢W(xué)希望增加一個(gè)react
項(xiàng)目的示例,他們?cè)诩?code>react項(xiàng)目時(shí)遇到了一些困難,于是找時(shí)間就加了這部分內(nèi)容;發(fā)現(xiàn)網(wǎng)上single-spa
集成react
的示例非常少,僅有的幾個(gè)看了下也是對(duì)官網(wǎng)示例的抄寫(xiě)。
示例項(xiàng)目是基于react
腳手架cra
創(chuàng)建的,整個(gè)集成的過(guò)程中難點(diǎn)有兩個(gè):
webpack
的配置,這部分內(nèi)容官網(wǎng)有提供子應(yīng)用入口的配置,單純看官方文檔的示例項(xiàng)目根本跑不起來(lái),或者即使跑起來(lái)也有問(wèn)題,
react
和vue
的集成還不一樣,react
需要在主項(xiàng)目的配置中也加一點(diǎn)東西,這部分官網(wǎng)配置沒(méi)說(shuō),是通過(guò)single-spa-react
源碼看出來(lái)的
接下來(lái)就開(kāi)始吧,在/micro-frontend
目錄下通過(guò)cra
腳手架新建子應(yīng)用app3
安裝 app3
create-react-app app3
以下所有操作都在/micro-frontend/app3
目錄下進(jìn)行
安裝react-router-dom
、single-spa-react
npm i react-router-dom single-spa-react -S
打散配置
打散項(xiàng)目的配置,方便更改webpack
的配置內(nèi)容,當(dāng)然通過(guò)react-app-rewired
覆寫(xiě)默認(rèn)配置應(yīng)該也是可以的,官網(wǎng)也有提到,不過(guò)我這里沒(méi)試,采用的是直接打散配置
npm run eject
更改 webpack 配置文件
/config/webpack.config.js,官網(wǎng)
刪掉
optimization
部分,這部分配置和chunk
有關(guān),有動(dòng)態(tài)生成的異步chunk
存在,會(huì)導(dǎo)致主應(yīng)用無(wú)法配置,因?yàn)?code>chunk的名字會(huì)變,其實(shí)這也是single-spa
的缺陷,或者說(shuō)采用JS entry
的缺陷,JS entry
建議將所有內(nèi)容都打成一個(gè)bundle
-app.js
更改
entry
和output
部分
{
...
entry: [
paths.appIndexJs,
].filter(Boolean),
output: {
path: isEnvProduction ? paths.appBuild : undefined,
filename: 'js/app.js',
publicPath: '//localhost:3000',
jsonpFunction: `webpackJsonp${appPackageJson.name}`,
library: 'app3',
libraryTarget: 'umd'
},
...
}
項(xiàng)目入口文件改造
我這里將無(wú)關(guān)緊要的內(nèi)容都刪了,只留了/src/index.js
和/src/index.css
/src/index.js
由于文章內(nèi)容太多,字?jǐn)?shù)超出限制,這部分代碼就通過(guò)圖片的形式來(lái)展示了,如果需要拷貝可去 github
/src/index.css
body {
text-align: center;
}
啟動(dòng)子應(yīng)用
npm run start
瀏覽器訪問(wèn)localhost:3000
基座應(yīng)用 layout
在/micro-frontend
目錄下新建基座應(yīng)用,為了簡(jiǎn)潔明了,新建項(xiàng)目時(shí)選擇的配置項(xiàng)和子應(yīng)用一樣;在本示例中基座應(yīng)用采用了vue
來(lái)實(shí)現(xiàn),用別的方式或者框架實(shí)現(xiàn)也可以,比如自己用webpack
構(gòu)建一個(gè)項(xiàng)目。
以下操作都在
/micro-frontend/layout
目錄下進(jìn)行
安裝single-spa
npm i single-spa -S
改造基座項(xiàng)目
入口文件
// src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { registerApplication, start } from 'single-spa'
Vue.config.productionTip = false
// 遠(yuǎn)程加載子應(yīng)用
function createScript(url) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = url
script.onload = resolve
script.onerror = reject
const firstScript = document.getElementsByTagName('script')[0]
firstScript.parentNode.insertBefore(script, firstScript)
})
}
// 記載函數(shù),返回一個(gè) promise
function loadApp(url, globalVar) {
// 支持遠(yuǎn)程加載子應(yīng)用
return async () => {
await createScript(url + '/js/chunk-vendors.js')
await createScript(url + '/js/app.js')
// 這里的return很重要,需要從這個(gè)全局對(duì)象中拿到子應(yīng)用暴露出來(lái)的生命周期函數(shù)
return window[globalVar]
}
}
// 子應(yīng)用列表
const apps = [
{
// 子應(yīng)用名稱
name: 'app1',
// 子應(yīng)用加載函數(shù),是一個(gè)promise
app: loadApp('http://localhost:8081', 'app1'),
// 當(dāng)路由滿足條件時(shí)(返回true),激活(掛載)子應(yīng)用
activeWhen: location => location.pathname.startsWith('/app1'),
// 傳遞給子應(yīng)用的對(duì)象
customProps: {}
},
{
name: 'app2',
app: loadApp('http://localhost:8082', 'app2'),
activeWhen: location => location.pathname.startsWith('/app2'),
customProps: {}
},
{
// 子應(yīng)用名稱
name: 'app3',
// 子應(yīng)用加載函數(shù),是一個(gè)promise
app: loadApp('http://localhost:3000', 'app3'),
// 當(dāng)路由滿足條件時(shí)(返回true),激活(掛載)子應(yīng)用
activeWhen: location => location.pathname.startsWith('/app3'),
// 傳遞給子應(yīng)用的對(duì)象,這個(gè)很重要,該配置告訴react子應(yīng)用自己的容器元素是什么,這塊兒和vue子應(yīng)用的集成不一樣,官網(wǎng)并沒(méi)有說(shuō)這部分,或者我沒(méi)找到,是通過(guò)看single-spa-react源碼知道的
customProps: {
domElement: document.getElementById('microApp'),
// 添加 name 屬性是為了兼容自己寫(xiě)的lyn-single-spa,原生的不需要,當(dāng)然加了也不影響
name: 'app3'
}
}
]
// 注冊(cè)子應(yīng)用
for (let i = apps.length - 1; i >= 0; i--) {
registerApplication(apps[i])
}
new Vue({
router,
mounted() {
// 啟動(dòng)
start()
},
render: h => h(App)
}).$mount('#app')
App.vue
<template>
<div id="app">
<div id="nav">
<router-link to="/app1">app1</router-link> |
<router-link to="/app2">app2</router-link>
</div>
<!-- 子應(yīng)用容器 -->
<div id = "microApp">
<router-view/>
</div>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
</style>
路由
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = []
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
啟動(dòng)基座應(yīng)用
npm run serve
瀏覽器訪問(wèn)基座應(yīng)用
<div style = "display: flex;">
<img src = "https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c861159484c64ea8bf71d254f3df28d6~tplv-k3u1fbpfcp-zoom-1.image" style = "width: 49%; margin-right: 1%;" />
<img src = "https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ce7cf883fbb54eef84dfbc00299ff8a3~tplv-k3u1fbpfcp-zoom-1.image" style = "width: 49%; margin-left: 1%;" />
</div>
終于看到了結(jié)果。
小技巧
有時(shí)候
single-spa
可能會(huì)報(bào)一些我們現(xiàn)在無(wú)法理解的錯(cuò)誤,我們可能需要去做代碼調(diào)試,閱讀源碼時(shí)碰到不理解的地方也需要編寫(xiě)示例
+ 單步調(diào)試
,但是默認(rèn)的是已經(jīng)打包壓縮后的代碼,不太方便做這些,大家可以在node_modules
目錄找到single-spa
目錄,把目錄下的package.json
中的module
字段的值改為lib/single-spa.dev.js
,這是一個(gè)未壓縮的bundle
,利于代碼的閱讀的調(diào)試,當(dāng)然需要重啟應(yīng)用。
子應(yīng)用也是一樣類似的技巧,因?yàn)?code>single-spa-vue就一個(gè)文件,可以直接拷貝出來(lái)放到項(xiàng)目的
/src
目錄下,將main.js
中的引入的single-spa-vue
改成當(dāng)前目錄即可。
打包部署
打包
在各個(gè)項(xiàng)目的根目錄下分別執(zhí)行
npm run build
部署
可以將打包后的bundle
發(fā)布到nginx
服務(wù)器上,這個(gè)nginx
服務(wù)器可以是單獨(dú)的服務(wù)器、或者虛擬機(jī)、亦或是docker
容器都行,這里采用serve
在本地模擬部署
如果你有條件部署到nginx
上,需要注意nginx
的代理配置
- 對(duì)于子應(yīng)用靜態(tài)資源的加載只需要攔截相應(yīng)的前綴將請(qǐng)求轉(zhuǎn)發(fā)到對(duì)應(yīng)子應(yīng)用的目錄下即可
- 頁(yè)面刷新只需要攔截到主應(yīng)用即可,主應(yīng)用內(nèi)部自己根據(jù)
activeWhen
去掛載對(duì)應(yīng)的子應(yīng)用
全局安裝 serve
npm i serve -g
在各個(gè)項(xiàng)目的根目錄下啟動(dòng) serve
serve ./dist -p port
在瀏覽器訪問(wèn)基座應(yīng)用的地址,發(fā)現(xiàn)得到和剛才一樣的結(jié)果
single-spa 源碼分析
整個(gè)閱讀過(guò)程以示例項(xiàng)目為例,閱讀源碼時(shí)一定要多動(dòng)手寫(xiě)注釋、做筆記,遇到不理解的地方編寫(xiě)示例代碼 +
console.log
+ 單步調(diào)試,切記不要只看不動(dòng)手。
這是我在閱讀時(shí)整理的一個(gè)思維導(dǎo)圖,源碼中也寫(xiě)了大量的注釋,大家可以參照著進(jìn)行閱讀。Ok !!這就開(kāi)始吧
從源碼目錄中可以看到,single-spa
是使用rollup
來(lái)打包的,從rollup.config.js
中可以發(fā)現(xiàn)入口是single-spa.js
,
打開(kāi)會(huì)發(fā)現(xiàn)里面導(dǎo)出了一大堆東西,有我們非常熟悉的各個(gè)方法,我們就從registerApplication
方法開(kāi)始
registerApplication 注冊(cè)子應(yīng)用
single-spa/src/applications/apps.js
/**
* 注冊(cè)應(yīng)用,兩種方式
* registerApplication('app1', loadApp(url), activeWhen('/app1'), customProps)
* registerApplication({
* name: 'app1',
* app: loadApp(url),
* activeWhen: activeWhen('/app1'),
* customProps: {}
* })
* @param {*} appNameOrConfig 應(yīng)用名稱或者應(yīng)用配置對(duì)象
* @param {*} appOrLoadApp 應(yīng)用的加載方法,是一個(gè) promise
* @param {*} activeWhen 判斷應(yīng)用是否激活的一個(gè)方法,方法返回 true or false
* @param {*} customProps 傳遞給子應(yīng)用的 props 對(duì)象
*/
export function registerApplication(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
) {
/**
* 格式化用戶傳遞的應(yīng)用配置參數(shù)
* registration = {
* name: 'app1',
* loadApp: 返回promise的函數(shù),
* activeWhen: 返回boolean值的函數(shù),
* customProps: {},
* }
*/
const registration = sanitizeArguments(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
);
// 判斷應(yīng)用是否重名
if (getAppNames().indexOf(registration.name) !== -1)
throw Error(
formatErrorMessage(
21,
__DEV__ &&
`There is already an app registered with name ${registration.name}`,
registration.name
)
);
// 將各個(gè)應(yīng)用的配置信息都存放到 apps 數(shù)組中
apps.push(
// 給每個(gè)應(yīng)用增加一個(gè)內(nèi)置屬性
assign(
{
loadErrorTime: null,
// 最重要的,應(yīng)用的狀態(tài)
status: NOT_LOADED,
parcels: {},
devtools: {
overlays: {
options: {},
selectors: [],
},
},
},
registration
)
);
// 瀏覽器環(huán)境運(yùn)行
if (isInBrowser) {
// https://zh-hans.single-spa.js.org/docs/api#ensurejquerysupport
// 如果頁(yè)面中使用了jQuery,則給jQuery打patch
ensureJQuerySupport();
reroute();
}
}
sanitizeArguments 格式化用戶傳遞的子應(yīng)用配置參數(shù)
single-spa/src/applications/apps.js
// 返回處理后的應(yīng)用配置對(duì)象
function sanitizeArguments(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
) {
// 判斷第一個(gè)參數(shù)是否為對(duì)象
const usingObjectAPI = typeof appNameOrConfig === "object";
// 初始化應(yīng)用配置對(duì)象
const registration = {
name: null,
loadApp: null,
activeWhen: null,
customProps: null,
};
if (usingObjectAPI) {
// 注冊(cè)應(yīng)用的時(shí)候傳遞的參數(shù)是對(duì)象
validateRegisterWithConfig(appNameOrConfig);
registration.name = appNameOrConfig.name;
registration.loadApp = appNameOrConfig.app;
registration.activeWhen = appNameOrConfig.activeWhen;
registration.customProps = appNameOrConfig.customProps;
} else {
// 參數(shù)列表
validateRegisterWithArguments(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
);
registration.name = appNameOrConfig;
registration.loadApp = appOrLoadApp;
registration.activeWhen = activeWhen;
registration.customProps = customProps;
}
// 如果第二個(gè)參數(shù)不是一個(gè)函數(shù),比如是一個(gè)包含已經(jīng)生命周期的對(duì)象,則包裝成一個(gè)返回 promise 的函數(shù)
registration.loadApp = sanitizeLoadApp(registration.loadApp);
// 如果用戶沒(méi)有提供 props 對(duì)象,則給一個(gè)默認(rèn)的空對(duì)象
registration.customProps = sanitizeCustomProps(registration.customProps);
// 保證activeWhen是一個(gè)返回boolean值的函數(shù)
registration.activeWhen = sanitizeActiveWhen(registration.activeWhen);
// 返回處理后的應(yīng)用配置對(duì)象
return registration;
}
validateRegisterWithConfig
single-spa/src/applications/apps.js
/**
* 驗(yàn)證應(yīng)用配置對(duì)象的各個(gè)屬性是否存在不合法的情況,存在則拋出錯(cuò)誤
* @param {*} config = { name: 'app1', app: function, activeWhen: function, customProps: {} }
*/
export function validateRegisterWithConfig(config) {
// 異常判斷,應(yīng)用的配置對(duì)象不能是數(shù)組或者null
if (Array.isArray(config) || config === null)
throw Error(
formatErrorMessage(
39,
__DEV__ && "Configuration object can't be an Array or null!"
)
);
// 配置對(duì)象只能包括這四個(gè)key
const validKeys = ["name", "app", "activeWhen", "customProps"];
// 找到配置對(duì)象存在的無(wú)效的key
const invalidKeys = Object.keys(config).reduce(
(invalidKeys, prop) =>
validKeys.indexOf(prop) >= 0 ? invalidKeys : invalidKeys.concat(prop),
[]
);
// 如果存在無(wú)效的key,則拋出一個(gè)錯(cuò)誤
if (invalidKeys.length !== 0)
throw Error(
formatErrorMessage(
38,
__DEV__ &&
`The configuration object accepts only: ${validKeys.join(
", "
)}. Invalid keys: ${invalidKeys.join(", ")}.`,
validKeys.join(", "),
invalidKeys.join(", ")
)
);
// 驗(yàn)證應(yīng)用名稱,只能是字符串,且不能為空
if (typeof config.name !== "string" || config.name.length === 0)
throw Error(
formatErrorMessage(
20,
__DEV__ &&
"The config.name on registerApplication must be a non-empty string"
)
);
// app 屬性只能是一個(gè)對(duì)象或者函數(shù)
// 對(duì)象是一個(gè)已被解析過(guò)的對(duì)象,是一個(gè)包含各個(gè)生命周期的對(duì)象;
// 加載函數(shù)必須返回一個(gè) promise
// 以上信息在官方文檔中有提到:https://zh-hans.single-spa.js.org/docs/configuration
if (typeof config.app !== "object" && typeof config.app !== "function")
throw Error(
formatErrorMessage(
20,
__DEV__ &&
"The config.app on registerApplication must be an application or a loading function"
)
);
// 第三個(gè)參數(shù),可以是一個(gè)字符串,也可以是一個(gè)函數(shù),也可以是兩者組成的一個(gè)數(shù)組,表示當(dāng)前應(yīng)該被激活的應(yīng)用的baseURL
const allowsStringAndFunction = (activeWhen) =>
typeof activeWhen === "string" || typeof activeWhen === "function";
if (
!allowsStringAndFunction(config.activeWhen) &&
!(
Array.isArray(config.activeWhen) &&
config.activeWhen.every(allowsStringAndFunction)
)
)
throw Error(
formatErrorMessage(
24,
__DEV__ &&
"The config.activeWhen on registerApplication must be a string, function or an array with both"
)
);
// 傳遞給子應(yīng)用的props對(duì)象必須是一個(gè)對(duì)象
if (!validCustomProps(config.customProps))
throw Error(
formatErrorMessage(
22,
__DEV__ && "The optional config.customProps must be an object"
)
);
}
validateRegisterWithArguments
single-spa/src/applications/apps.js
// 同樣是驗(yàn)證四個(gè)參數(shù)是否合法
function validateRegisterWithArguments(
name,
appOrLoadApp,
activeWhen,
customProps
) {
if (typeof name !== "string" || name.length === 0)
throw Error(
formatErrorMessage(
20,
__DEV__ &&
`The 1st argument to registerApplication must be a non-empty string 'appName'`
)
);
if (!appOrLoadApp)
throw Error(
formatErrorMessage(
23,
__DEV__ &&
"The 2nd argument to registerApplication must be an application or loading application function"
)
);
if (typeof activeWhen !== "function")
throw Error(
formatErrorMessage(
24,
__DEV__ &&
"The 3rd argument to registerApplication must be an activeWhen function"
)
);
if (!validCustomProps(customProps))
throw Error(
formatErrorMessage(
22,
__DEV__ &&
"The optional 4th argument is a customProps and must be an object"
)
);
}
sanitizeLoadApp
single-spa/src/applications/apps.js
// 保證第二個(gè)參數(shù)一定是一個(gè)返回 promise 的函數(shù)
function sanitizeLoadApp(loadApp) {
if (typeof loadApp !== "function") {
return () => Promise.resolve(loadApp);
}
return loadApp;
}
sanitizeCustomProps
single-spa/src/applications/apps.js
// 保證 props 不為 undefined
function sanitizeCustomProps(customProps) {
return customProps ? customProps : {};
}
sanitizeActiveWhen
single-spa/src/applications/apps.js
// 得到一個(gè)函數(shù),函數(shù)負(fù)責(zé)判斷瀏覽器當(dāng)前地址是否和用戶給定的baseURL相匹配,匹配返回true,否則返回false
function sanitizeActiveWhen(activeWhen) {
// []
let activeWhenArray = Array.isArray(activeWhen) ? activeWhen : [activeWhen];
// 保證數(shù)組中每個(gè)元素都是一個(gè)函數(shù)
activeWhenArray = activeWhenArray.map((activeWhenOrPath) =>
typeof activeWhenOrPath === "function"
? activeWhenOrPath
// activeWhen如果是一個(gè)路徑,則保證成一個(gè)函數(shù)
: pathToActiveWhen(activeWhenOrPath)
);
// 返回一個(gè)函數(shù),函數(shù)返回一個(gè) boolean 值
return (location) =>
activeWhenArray.some((activeWhen) => activeWhen(location));
}
pathToActiveWhen
single-spa/src/applications/apps.js
export function pathToActiveWhen(path) {
// 根據(jù)用戶提供的baseURL,生成正則表達(dá)式
const regex = toDynamicPathValidatorRegex(path);
// 函數(shù)返回boolean值,判斷當(dāng)前路由是否匹配用戶給定的路徑
return (location) => {
const route = location.href
.replace(location.origin, "")
.replace(location.search, "")
.split("?")[0];
return regex.test(route);
};
}
reroute 更改app.status和執(zhí)行生命周期函數(shù)
single-spa/src/navigation/reroute.js
/**
* 每次切換路由前,將應(yīng)用分為4大類,
* 首次加載時(shí)執(zhí)行l(wèi)oadApp
* 后續(xù)的路由切換執(zhí)行performAppChange
* 為四大類的應(yīng)用分別執(zhí)行相應(yīng)的操作,比如更改app.status,執(zhí)行生命周期函數(shù)
* 所以,從這里也可以看出來(lái),single-spa就是一個(gè)維護(hù)應(yīng)用的狀態(tài)機(jī)
* @param {*} pendingPromises
* @param {*} eventArguments
*/
export function reroute(pendingPromises = [], eventArguments) {
// 應(yīng)用正在切換,這個(gè)狀態(tài)會(huì)在執(zhí)行performAppChanges之前置為true,執(zhí)行結(jié)束之后再置為false
// 如果在中間用戶重新切換路由了,即走這個(gè)if分支,暫時(shí)看起來(lái)就在數(shù)組中存儲(chǔ)了一些信息,沒(méi)看到有什么用
// 字面意思理解就是用戶等待app切換
if (appChangeUnderway) {
return new Promise((resolve, reject) => {
peopleWaitingOnAppChange.push({
resolve,
reject,
eventArguments,
});
});
}
// 將應(yīng)用分為4大類
const {
// 需要被移除的
appsToUnload,
// 需要被卸載的
appsToUnmount,
// 需要被加載的
appsToLoad,
// 需要被掛載的
appsToMount,
} = getAppChanges();
let appsThatChanged;
// 是否已經(jīng)執(zhí)行 start 方法
if (isStarted()) {
// 已執(zhí)行
appChangeUnderway = true;
// 所有需要被改變的的應(yīng)用
appsThatChanged = appsToUnload.concat(
appsToLoad,
appsToUnmount,
appsToMount
);
// 執(zhí)行改變
return performAppChanges();
} else {
// 未執(zhí)行
appsThatChanged = appsToLoad;
// 加載Apps
return loadApps();
}
// 整體返回一個(gè)立即resolved的promise,通過(guò)微任務(wù)來(lái)加載apps
function loadApps() {
return Promise.resolve().then(() => {
// 加載每個(gè)子應(yīng)用,并做一系列的狀態(tài)變更和驗(yàn)證(比如結(jié)果為promise、子應(yīng)用要導(dǎo)出生命周期函數(shù))
const loadPromises = appsToLoad.map(toLoadPromise);
return (
// 保證所有加載子應(yīng)用的微任務(wù)執(zhí)行完成
Promise.all(loadPromises)
.then(callAllEventListeners)
// there are no mounted apps, before start() is called, so we always return []
.then(() => [])
.catch((err) => {
callAllEventListeners();
throw err;
})
);
});
}
function performAppChanges() {
return Promise.resolve().then(() => {
// https://github.com/single-spa/single-spa/issues/545
// 自定義事件,在應(yīng)用狀態(tài)發(fā)生改變之前可觸發(fā),給用戶提供搞事情的機(jī)會(huì)
window.dispatchEvent(
new CustomEvent(
appsThatChanged.length === 0
? "single-spa:before-no-app-change"
: "single-spa:before-app-change",
getCustomEventDetail(true)
)
);
window.dispatchEvent(
new CustomEvent(
"single-spa:before-routing-event",
getCustomEventDetail(true)
)
);
// 移除應(yīng)用 => 更改應(yīng)用狀態(tài),執(zhí)行unload生命周期函數(shù),執(zhí)行一些清理動(dòng)作
// 其實(shí)一般情況下這里沒(méi)有真的移除應(yīng)用
const unloadPromises = appsToUnload.map(toUnloadPromise);
// 卸載應(yīng)用,更改狀態(tài),執(zhí)行unmount生命周期函數(shù)
const unmountUnloadPromises = appsToUnmount
.map(toUnmountPromise)
// 卸載完然后移除,通過(guò)注冊(cè)微任務(wù)的方式實(shí)現(xiàn)
.map((unmountPromise) => unmountPromise.then(toUnloadPromise));
const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);
const unmountAllPromise = Promise.all(allUnmountPromises);
// 卸載全部完成后觸發(fā)一個(gè)事件
unmountAllPromise.then(() => {
window.dispatchEvent(
new CustomEvent(
"single-spa:before-mount-routing-event",
getCustomEventDetail(true)
)
);
});
/* We load and bootstrap apps while other apps are unmounting, but we
* wait to mount the app until all apps are finishing unmounting
* 這個(gè)原因其實(shí)是因?yàn)檫@些操作都是通過(guò)注冊(cè)不同的微任務(wù)實(shí)現(xiàn)的,而JS是單線程執(zhí)行,
* 所以自然后續(xù)的只能等待前面的執(zhí)行完了才能執(zhí)行
* 這里一般情況下其實(shí)不會(huì)執(zhí)行,只有手動(dòng)執(zhí)行了unloadApplication方法才會(huì)二次加載
*/
const loadThenMountPromises = appsToLoad.map((app) => {
return toLoadPromise(app).then((app) =>
tryToBootstrapAndMount(app, unmountAllPromise)
);
});
/* These are the apps that are already bootstrapped and just need
* to be mounted. They each wait for all unmounting apps to finish up
* before they mount.
* 初始化和掛載app,其實(shí)做的事情很簡(jiǎn)單,就是改變app.status,執(zhí)行生命周期函數(shù)
* 當(dāng)然這里的初始化和掛載其實(shí)是前后腳一起完成的(只要中間用戶沒(méi)有切換路由)
*/
const mountPromises = appsToMount
.filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
.map((appToMount) => {
return tryToBootstrapAndMount(appToMount, unmountAllPromise);
});
// 后面就沒(méi)啥了,可以理解為收尾工作
return unmountAllPromise
.catch((err) => {
callAllEventListeners();
throw err;
})
.then(() => {
/* Now that the apps that needed to be unmounted are unmounted, their DOM navigation
* events (like hashchange or popstate) should have been cleaned up. So it's safe
* to let the remaining captured event listeners to handle about the DOM event.
*/
callAllEventListeners();
return Promise.all(loadThenMountPromises.concat(mountPromises))
.catch((err) => {
pendingPromises.forEach((promise) => promise.reject(err));
throw err;
})
.then(finishUpAndReturn);
});
});
}
}
getAppChanges
single-spa/src/applications/apps.js
// 將應(yīng)用分為四大類
export function getAppChanges() {
// 需要被移除的應(yīng)用
const appsToUnload = [],
// 需要被卸載的應(yīng)用
appsToUnmount = [],
// 需要被加載的應(yīng)用
appsToLoad = [],
// 需要被掛載的應(yīng)用
appsToMount = [];
// We re-attempt to download applications in LOAD_ERROR after a timeout of 200 milliseconds
const currentTime = new Date().getTime();
apps.forEach((app) => {
// boolean,應(yīng)用是否應(yīng)該被激活
const appShouldBeActive =
app.status !== SKIP_BECAUSE_BROKEN && shouldBeActive(app);
switch (app.status) {
// 需要被加載的應(yīng)用
case LOAD_ERROR:
if (currentTime - app.loadErrorTime >= 200) {
appsToLoad.push(app);
}
break;
// 需要被加載的應(yīng)用
case NOT_LOADED:
case LOADING_SOURCE_CODE:
if (appShouldBeActive) {
appsToLoad.push(app);
}
break;
// 狀態(tài)為xx的應(yīng)用
case NOT_BOOTSTRAPPED:
case NOT_MOUNTED:
if (!appShouldBeActive && getAppUnloadInfo(toName(app))) {
// 需要被移除的應(yīng)用
appsToUnload.push(app);
} else if (appShouldBeActive) {
// 需要被掛載的應(yīng)用
appsToMount.push(app);
}
break;
// 需要被卸載的應(yīng)用,已經(jīng)處于掛載狀態(tài),但現(xiàn)在路由已經(jīng)變了的應(yīng)用需要被卸載
case MOUNTED:
if (!appShouldBeActive) {
appsToUnmount.push(app);
}
break;
// all other statuses are ignored
}
});
return { appsToUnload, appsToUnmount, appsToLoad, appsToMount };
}
shouldBeActive
single-spa/src/applications/app.helpers.js
// 返回boolean值,應(yīng)用是否應(yīng)該被激活
export function shouldBeActive(app) {
try {
return app.activeWhen(window.location);
} catch (err) {
handleAppError(err, app, SKIP_BECAUSE_BROKEN);
return false;
}
}
toLoadPromise
single-spa/src/lifecycles/load.js
/**
* 通過(guò)微任務(wù)加載子應(yīng)用,其實(shí)singleSpa中很多地方都用了微任務(wù)
* 這里最終是return了一個(gè)promise出行,在注冊(cè)了加載子應(yīng)用的微任務(wù)
* 概括起來(lái)就是:
* 更改app.status為L(zhǎng)OAD_SOURCE_CODE => NOT_BOOTSTRAP,當(dāng)然還有可能是LOAD_ERROR
* 執(zhí)行加載函數(shù),并將props傳遞給加載函數(shù),給用戶處理props的一個(gè)機(jī)會(huì),因?yàn)檫@個(gè)props是一個(gè)完備的props
* 驗(yàn)證加載函數(shù)的執(zhí)行結(jié)果,必須為promise,且加載函數(shù)內(nèi)部必須return一個(gè)對(duì)象
* 這個(gè)對(duì)象是子應(yīng)用的,對(duì)象中必須包括各個(gè)必須的生命周期函數(shù)
* 然后將生命周期方法通過(guò)一個(gè)函數(shù)包裹并掛載到app對(duì)象上
* app加載完成,刪除app.loadPromise
* @param {*} app
*/
export function toLoadPromise(app) {
return Promise.resolve().then(() => {
if (app.loadPromise) {
// 說(shuō)明app已經(jīng)在被加載
return app.loadPromise;
}
// 只有狀態(tài)為NOT_LOADED和LOAD_ERROR的app才可以被加載
if (app.status !== NOT_LOADED && app.status !== LOAD_ERROR) {
return app;
}
// 設(shè)置App的狀態(tài)
app.status = LOADING_SOURCE_CODE;
let appOpts, isUserErr;
return (app.loadPromise = Promise.resolve()
.then(() => {
// 執(zhí)行app的加載函數(shù),并給子應(yīng)用傳遞props => 用戶自定義的customProps和內(nèi)置的比如應(yīng)用的名稱、singleSpa實(shí)例
// 其實(shí)這里有個(gè)疑問(wèn),這個(gè)props是怎么傳遞給子應(yīng)用的,感覺(jué)跟后面的生命周期函數(shù)有關(guān)
const loadPromise = app.loadApp(getProps(app));
// 加載函數(shù)需要返回一個(gè)promise
if (!smellsLikeAPromise(loadPromise)) {
// The name of the app will be prepended to this error message inside of the handleAppError function
isUserErr = true;
throw Error(
formatErrorMessage(
33,
__DEV__ &&
`single-spa loading function did not return a promise. Check the second argument to registerApplication('${toName(
app
)}', loadingFunction, activityFunction)`,
toName(app)
)
);
}
// 這里很重要,這個(gè)val就是示例項(xiàng)目中加載函數(shù)中return出來(lái)的window.singleSpa,這個(gè)屬性是子應(yīng)用打包時(shí)設(shè)置的
return loadPromise.then((val) => {
app.loadErrorTime = null;
// window.singleSpa
appOpts = val;
let validationErrMessage, validationErrCode;
// 以下進(jìn)行一系列的驗(yàn)證,已window.singleSpa為例說(shuō)明,簡(jiǎn)稱g.s
// g.s必須為對(duì)象
if (typeof appOpts !== "object") {
validationErrCode = 34;
if (__DEV__) {
validationErrMessage = `does not export anything`;
}
}
// g.s必須導(dǎo)出bootstrap生命周期函數(shù)
if (!validLifecycleFn(appOpts.bootstrap)) {
validationErrCode = 35;
if (__DEV__) {
validationErrMessage = `does not export a bootstrap function or array of functions`;
}
}
// g.s必須導(dǎo)出mount生命周期函數(shù)
if (!validLifecycleFn(appOpts.mount)) {
validationErrCode = 36;
if (__DEV__) {
validationErrMessage = `does not export a bootstrap function or array of functions`;
}
}
// g.s必須導(dǎo)出unmount生命周期函數(shù)
if (!validLifecycleFn(appOpts.unmount)) {
validationErrCode = 37;
if (__DEV__) {
validationErrMessage = `does not export a bootstrap function or array of functions`;
}
}
const type = objectType(appOpts);
// 說(shuō)明上述驗(yàn)證失敗,拋出錯(cuò)誤提示信息
if (validationErrCode) {
let appOptsStr;
try {
appOptsStr = JSON.stringify(appOpts);
} catch {}
console.error(
formatErrorMessage(
validationErrCode,
__DEV__ &&
`The loading function for single-spa ${type} '${toName(
app
)}' resolved with the following, which does not have bootstrap, mount, and unmount functions`,
type,
toName(app),
appOptsStr
),
appOpts
);
handleAppError(validationErrMessage, app, SKIP_BECAUSE_BROKEN);
return app;
}
if (appOpts.devtools && appOpts.devtools.overlays) {
// app.devtoolsoverlays添加子應(yīng)用的devtools.overlays的屬性,不知道是干嘛用的
app.devtools.overlays = assign(
{},
app.devtools.overlays,
appOpts.devtools.overlays
);
}
// 設(shè)置app狀態(tài)為未初始化,表示加載完了
app.status = NOT_BOOTSTRAPPED;
// 在app對(duì)象上掛載生命周期方法,每個(gè)方法都接收一個(gè)props作為參數(shù),方法內(nèi)部執(zhí)行子應(yīng)用導(dǎo)出的生命周期函數(shù),并確保生命周期函數(shù)返回一個(gè)promise
app.bootstrap = flattenFnArray(appOpts, "bootstrap");
app.mount = flattenFnArray(appOpts, "mount");
app.unmount = flattenFnArray(appOpts, "unmount");
app.unload = flattenFnArray(appOpts, "unload");
app.timeouts = ensureValidAppTimeouts(appOpts.timeouts);
// 執(zhí)行到這里說(shuō)明子應(yīng)用已成功加載,刪除app.loadPromise屬性
delete app.loadPromise;
return app;
});
})
.catch((err) => {
// 加載失敗,稍后重新加載
delete app.loadPromise;
let newStatus;
if (isUserErr) {
newStatus = SKIP_BECAUSE_BROKEN;
} else {
newStatus = LOAD_ERROR;
app.loadErrorTime = new Date().getTime();
}
handleAppError(err, app, newStatus);
return app;
}));
});
}
getProps
single-spa/src/lifecycles/prop.helpers.js
/**
* 得到傳遞給子應(yīng)用的props
* @param {} appOrParcel => app
* 以下返回內(nèi)容其實(shí)在官網(wǎng)也都有提到,比如singleSpa實(shí)例,目的是為了子應(yīng)用不需要重復(fù)引入single-spa
* return {
* ...customProps,
* name,
* mountParcel: mountParcel.bind(appOrParcel),
* singleSpa,
* }
*/
export function getProps(appOrParcel) {
// app.name
const name = toName(appOrParcel);
// app.customProps,以下對(duì)customProps對(duì)象的判斷邏輯有點(diǎn)多余
// 因?yàn)榍懊娴膮?shù)格式化已經(jīng)保證customProps肯定是一個(gè)對(duì)象
let customProps =
typeof appOrParcel.customProps === "function"
? appOrParcel.customProps(name, window.location)
: appOrParcel.customProps;
if (
typeof customProps !== "object" ||
customProps === null ||
Array.isArray(customProps)
) {
customProps = {};
console.warn(
formatErrorMessage(
40,
__DEV__ &&
`single-spa: ${name}'s customProps function must return an object. Received ${customProps}`
),
name,
customProps
);
}
const result = assign({}, customProps, {
name,
mountParcel: mountParcel.bind(appOrParcel),
singleSpa,
});
if (isParcel(appOrParcel)) {
result.unmountSelf = appOrParcel.unmountThisParcel;
}
return result;
}
smellsLikeAPromise
single-spa/src/lifecycles/lifecycle.helpers.js
// 判斷一個(gè)變量是否為promise
export function smellsLikeAPromise(promise) {
return (
promise &&
typeof promise.then === "function" &&
typeof promise.catch === "function"
);
}
flattenFnArray
single-spa/src/lifecycles/lifecycle.helpers.js
/**
* 返回一個(gè)接受props作為參數(shù)的函數(shù),這個(gè)函數(shù)負(fù)責(zé)執(zhí)行子應(yīng)用中的生命周期函數(shù),
* 并確保生命周期函數(shù)返回的結(jié)果為promise
* @param {*} appOrParcel => window.singleSpa,子應(yīng)用打包后的對(duì)象
* @param {*} lifecycle => 字符串,生命周期名稱
*/
export function flattenFnArray(appOrParcel, lifecycle) {
// fns = fn or []
let fns = appOrParcel[lifecycle] || [];
// fns = [] or [fn]
fns = Array.isArray(fns) ? fns : [fns];
// 有些生命周期函數(shù)子應(yīng)用可能不會(huì)設(shè)置,比如unload
if (fns.length === 0) {
fns = [() => Promise.resolve()];
}
const type = objectType(appOrParcel);
const name = toName(appOrParcel);
return function (props) {
// 這里最后返回了一個(gè)promise鏈,這個(gè)操作似乎沒(méi)啥必要,因?yàn)椴豢赡艹霈F(xiàn)同名的生命周期函數(shù),所以,這里將生命周期函數(shù)放數(shù)組,沒(méi)太理解目的是啥
return fns.reduce((resultPromise, fn, index) => {
return resultPromise.then(() => {
// 執(zhí)行生命周期函數(shù),傳遞props給函數(shù),并驗(yàn)證函數(shù)的返回結(jié)果,必須為promise
const thisPromise = fn(props);
return smellsLikeAPromise(thisPromise)
? thisPromise
: Promise.reject(
formatErrorMessage(
15,
__DEV__ &&
`Within ${type} ${name}, the lifecycle function ${lifecycle} at array index ${index} did not return a promise`,
type,
name,
lifecycle,
index
)
);
});
}, Promise.resolve());
};
}
toUnloadPromise
single-spa/src/lifecycles/unload.js
const appsToUnload = {};
/**
* 移除應(yīng)用,就更改一下應(yīng)用的狀態(tài),執(zhí)行unload生命周期函數(shù),執(zhí)行清理操作
*
* 其實(shí)一般情況是不會(huì)執(zhí)行移除操作的,除非你手動(dòng)調(diào)用unloadApplication方法
* 單步調(diào)試會(huì)發(fā)現(xiàn)appsToUnload對(duì)象是個(gè)空對(duì)象,所以第一個(gè)if就return了,這里啥也沒(méi)做
* https://zh-hans.single-spa.js.org/docs/api#unloadapplication
* */
export function toUnloadPromise(app) {
return Promise.resolve().then(() => {
// 應(yīng)用信息
const unloadInfo = appsToUnload[toName(app)];
if (!unloadInfo) {
/* No one has called unloadApplication for this app,
* 不需要移除
* 一般情況下都不需要移除,只有在調(diào)用unloadApplication方法手動(dòng)執(zhí)行移除時(shí)才會(huì)
* 執(zhí)行后面的內(nèi)容
*/
return app;
}
// 已經(jīng)卸載了,執(zhí)行一些清理操作
if (app.status === NOT_LOADED) {
/* This app is already unloaded. We just need to clean up
* anything that still thinks we need to unload the app.
*/
finishUnloadingApp(app, unloadInfo);
return app;
}
// 如果應(yīng)用正在執(zhí)行掛載,路由突然發(fā)生改變,那么也需要應(yīng)用掛載完成才可以執(zhí)行移除
if (app.status === UNLOADING) {
/* Both unloadApplication and reroute want to unload this app.
* It only needs to be done once, though.
*/
return unloadInfo.promise.then(() => app);
}
if (app.status !== NOT_MOUNTED) {
/* The app cannot be unloaded until it is unmounted.
*/
return app;
}
// 更改狀態(tài)為 UNLOADING
app.status = UNLOADING;
// 在合理的時(shí)間范圍內(nèi)執(zhí)行生命周期函數(shù)
return reasonableTime(app, "unload")
.then(() => {
// 一些清理操作
finishUnloadingApp(app, unloadInfo);
return app;
})
.catch((err) => {
errorUnloadingApp(app, unloadInfo, err);
return app;
});
});
}
finishUnloadingApp
single-spa/src/lifecycles/unload.js
// 移除完成,執(zhí)行一些清理動(dòng)作,其實(shí)就是從appsToUnload數(shù)組中移除該app,移除生命周期函數(shù),更改app.status
// 但應(yīng)用不是真的被移除,后面再激活時(shí)不需要重新去下載資源,,只是做一些狀態(tài)上的變更,當(dāng)然load的那個(gè)過(guò)程還是需要的,這點(diǎn)可能需要再確認(rèn)一下
function finishUnloadingApp(app, unloadInfo) {
delete appsToUnload[toName(app)];
// Unloaded apps don't have lifecycles
delete app.bootstrap;
delete app.mount;
delete app.unmount;
delete app.unload;
app.status = NOT_LOADED;
/* resolve the promise of whoever called unloadApplication.
* This should be done after all other cleanup/bookkeeping
*/
unloadInfo.resolve();
}
reasonableTime
single-spa/src/applications/timeouts.js
/**
* 合理的時(shí)間,即生命周期函數(shù)合理的執(zhí)行時(shí)間
* 在合理的時(shí)間內(nèi)執(zhí)行生命周期函數(shù),并將函數(shù)的執(zhí)行結(jié)果resolve出去
* @param {*} appOrParcel => app
* @param {*} lifecycle => 生命周期函數(shù)名
*/
export function reasonableTime(appOrParcel, lifecycle) {
// 應(yīng)用的超時(shí)配置
const timeoutConfig = appOrParcel.timeouts[lifecycle];
// 超時(shí)警告
const warningPeriod = timeoutConfig.warningMillis;
const type = objectType(appOrParcel);
return new Promise((resolve, reject) => {
let finished = false;
let errored = false;
// 這里很關(guān)鍵,之前一直奇怪props是怎么傳遞給子應(yīng)用的,這里就是了,果然和之前的猜想是一樣的
// 是在執(zhí)行生命周期函數(shù)時(shí)像子應(yīng)用傳遞的props,所以之前執(zhí)行l(wèi)oadApp傳遞props不會(huì)到子應(yīng)用,
// 那么設(shè)計(jì)估計(jì)是給用戶自己處理props的一個(gè)機(jī)會(huì)吧,因?yàn)槟莻€(gè)時(shí)候處理的props已經(jīng)是{ ...customProps, ...內(nèi)置props }
appOrParcel[lifecycle](getProps(appOrParcel))
.then((val) => {
finished = true;
resolve(val);
})
.catch((val) => {
finished = true;
reject(val);
});
// 下面就沒(méi)啥了,就是超時(shí)的一些提示信息
setTimeout(() => maybeTimingOut(1), warningPeriod);
setTimeout(() => maybeTimingOut(true), timeoutConfig.millis);
const errMsg = formatErrorMessage(
31,
__DEV__ &&
`Lifecycle function ${lifecycle} for ${type} ${toName(
appOrParcel
)} lifecycle did not resolve or reject for ${timeoutConfig.millis} ms.`,
lifecycle,
type,
toName(appOrParcel),
timeoutConfig.millis
);
function maybeTimingOut(shouldError) {
if (!finished) {
if (shouldError === true) {
errored = true;
if (timeoutConfig.dieOnTimeout) {
reject(Error(errMsg));
} else {
console.error(errMsg);
//don't resolve or reject, we're waiting this one out
}
} else if (!errored) {
const numWarnings = shouldError;
const numMillis = numWarnings * warningPeriod;
console.warn(errMsg);
if (numMillis + warningPeriod < timeoutConfig.millis) {
setTimeout(() => maybeTimingOut(numWarnings + 1), warningPeriod);
}
}
}
}
});
}
toUnmountPromise
single-spa/src/lifecycles/unmount.js
/**
* 執(zhí)行了狀態(tài)上的更改
* 執(zhí)行unmount生命周期函數(shù)
* @param {*} appOrParcel => app
* @param {*} hardFail => 索引
*/
export function toUnmountPromise(appOrParcel, hardFail) {
return Promise.resolve().then(() => {
// 只卸載已掛載的應(yīng)用
if (appOrParcel.status !== MOUNTED) {
return appOrParcel;
}
// 更改狀態(tài)
appOrParcel.status = UNMOUNTING;
// 有關(guān)parcels的一些處理,沒(méi)使用過(guò)parcels,所以u(píng)nmountChildrenParcels = []
const unmountChildrenParcels = Object.keys(
appOrParcel.parcels
).map((parcelId) => appOrParcel.parcels[parcelId].unmountThisParcel());
let parcelError;
return Promise.all(unmountChildrenParcels)
// 在合理的時(shí)間范圍內(nèi)執(zhí)行unmount生命周期函數(shù)
.then(unmountAppOrParcel, (parcelError) => {
// There is a parcel unmount error
return unmountAppOrParcel().then(() => {
// Unmounting the app/parcel succeeded, but unmounting its children parcels did not
const parentError = Error(parcelError.message);
if (hardFail) {
throw transformErr(parentError, appOrParcel, SKIP_BECAUSE_BROKEN);
} else {
handleAppError(parentError, appOrParcel, SKIP_BECAUSE_BROKEN);
}
});
})
.then(() => appOrParcel);
function unmountAppOrParcel() {
// We always try to unmount the appOrParcel, even if the children parcels failed to unmount.
return reasonableTime(appOrParcel, "unmount")
.then(() => {
// The appOrParcel needs to stay in a broken status if its children parcels fail to unmount
if (!parcelError) {
appOrParcel.status = NOT_MOUNTED;
}
})
.catch((err) => {
if (hardFail) {
throw transformErr(err, appOrParcel, SKIP_BECAUSE_BROKEN);
} else {
handleAppError(err, appOrParcel, SKIP_BECAUSE_BROKEN);
}
});
}
});
}
tryToBootstrapAndMount
single-spa/src/navigation/reroute.js
/**
* Let's imagine that some kind of delay occurred during application loading.
* The user without waiting for the application to load switched to another route,
* this means that we shouldn't bootstrap and mount that application, thus we check
* twice if that application should be active before bootstrapping and mounting.
* https://github.com/single-spa/single-spa/issues/524
* 這里這個(gè)兩次判斷還是很重要的
*/
function tryToBootstrapAndMount(app, unmountAllPromise) {
if (shouldBeActive(app)) {
// 一次判斷為true,才會(huì)執(zhí)行初始化
return toBootstrapPromise(app).then((app) =>
unmountAllPromise.then(() =>
// 第二次, 兩次都為true才會(huì)去掛載
shouldBeActive(app) ? toMountPromise(app) : app
)
);
} else {
// 卸載
return unmountAllPromise.then(() => app);
}
}
toBootstrapPromise
single-spa/src/lifecycles/bootstrap.js
// 初始化app,更改app.status,在合理的時(shí)間內(nèi)執(zhí)行bootstrap生命周期函數(shù)
export function toBootstrapPromise(appOrParcel, hardFail) {
return Promise.resolve().then(() => {
if (appOrParcel.status !== NOT_BOOTSTRAPPED) {
return appOrParcel;
}
appOrParcel.status = BOOTSTRAPPING;
return reasonableTime(appOrParcel, "bootstrap")
.then(() => {
appOrParcel.status = NOT_MOUNTED;
return appOrParcel;
})
.catch((err) => {
if (hardFail) {
throw transformErr(err, appOrParcel, SKIP_BECAUSE_BROKEN);
} else {
handleAppError(err, appOrParcel, SKIP_BECAUSE_BROKEN);
return appOrParcel;
}
});
});
}
toMountPromise
single-spa/src/lifecycles/mount.js
// 掛載app,執(zhí)行mount生命周期函數(shù),并更改app.status
export function toMountPromise(appOrParcel, hardFail) {
return Promise.resolve().then(() => {
if (appOrParcel.status !== NOT_MOUNTED) {
return appOrParcel;
}
if (!beforeFirstMountFired) {
window.dispatchEvent(new CustomEvent("single-spa:before-first-mount"));
beforeFirstMountFired = true;
}
return reasonableTime(appOrParcel, "mount")
.then(() => {
appOrParcel.status = MOUNTED;
if (!firstMountFired) {
// single-spa其實(shí)在不同的階段提供了相應(yīng)的自定義事件,讓用戶可以做一些事情
window.dispatchEvent(new CustomEvent("single-spa:first-mount"));
firstMountFired = true;
}
return appOrParcel;
})
.catch((err) => {
// If we fail to mount the appOrParcel, we should attempt to unmount it before putting in SKIP_BECAUSE_BROKEN
// We temporarily put the appOrParcel into MOUNTED status so that toUnmountPromise actually attempts to unmount it
// instead of just doing a no-op.
appOrParcel.status = MOUNTED;
return toUnmountPromise(appOrParcel, true).then(
setSkipBecauseBroken,
setSkipBecauseBroken
);
function setSkipBecauseBroken() {
if (!hardFail) {
handleAppError(err, appOrParcel, SKIP_BECAUSE_BROKEN);
return appOrParcel;
} else {
throw transformErr(err, appOrParcel, SKIP_BECAUSE_BROKEN);
}
}
});
});
}
start(opts)
single-spa/src/start.js
let started = false
/**
* https://zh-hans.single-spa.js.org/docs/api#start
* 調(diào)用start之前,應(yīng)用會(huì)被加載,但不會(huì)初始化、掛載和卸載,有了start可以更好的控制應(yīng)用的性能
* @param {*} opts
*/
export function start(opts) {
started = true;
if (opts && opts.urlRerouteOnly) {
setUrlRerouteOnly(opts.urlRerouteOnly);
}
if (isInBrowser) {
reroute();
}
}
export function isStarted() {
return started;
}
if (isInBrowser) {
// registerApplication之后如果一直沒(méi)有調(diào)用start,則在5000ms后給出警告提示
setTimeout(() => {
if (!started) {
console.warn(
formatErrorMessage(
1,
__DEV__ &&
`singleSpa.start() has not been called, 5000ms after single-spa was loaded. Before start() is called, apps can be declared and loaded, but not bootstrapped or mounted.`
)
);
}
}, 5000);
}
監(jiān)聽(tīng)路由變化
single-spa/src/navigation/navigation-events.js
以下代碼會(huì)被打包進(jìn)bundle
的全局作用域內(nèi),bundle
被加載以后就會(huì)自動(dòng)執(zhí)行。這句提示不需要的話可自動(dòng)忽略
/**
* 監(jiān)聽(tīng)路由變化
*/
if (isInBrowser) {
// We will trigger an app change for any routing events,監(jiān)聽(tīng)hashchange和popstate事件
window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
// Monkeypatch addEventListener so that we can ensure correct timing
/**
* 擴(kuò)展原生的addEventListener和removeEventListener方法
* 每次注冊(cè)事件和事件處理函數(shù)都會(huì)將事件和處理函數(shù)保存下來(lái),當(dāng)然移除時(shí)也會(huì)做刪除
* */
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = function (eventName, fn) {
if (typeof fn === "function") {
if (
// eventName只能是hashchange或popstate && 對(duì)應(yīng)事件的fn注冊(cè)函數(shù)沒(méi)有注冊(cè)
routingEventsListeningTo.indexOf(eventName) >= 0 &&
!find(capturedEventListeners[eventName], (listener) => listener === fn)
) {
// 注冊(cè)(保存)eventName 事件的處理函數(shù)
capturedEventListeners[eventName].push(fn);
return;
}
}
// 原生方法
return originalAddEventListener.apply(this, arguments);
};
window.removeEventListener = function (eventName, listenerFn) {
if (typeof listenerFn === "function") {
// 從captureEventListeners數(shù)組中移除eventName事件指定的事件處理函數(shù)
if (routingEventsListeningTo.indexOf(eventName) >= 0) {
capturedEventListeners[eventName] = capturedEventListeners[
eventName
].filter((fn) => fn !== listenerFn);
return;
}
}
return originalRemoveEventListener.apply(this, arguments);
};
// 增強(qiáng)pushstate和replacestate
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
window.history.replaceState = patchedUpdateState(
window.history.replaceState,
"replaceState"
);
if (window.singleSpaNavigate) {
console.warn(
formatErrorMessage(
41,
__DEV__ &&
"single-spa has been loaded twice on the page. This can result in unexpected behavior."
)
);
} else {
/* For convenience in `onclick` attributes, we expose a global function for navigating to
* whatever an <a> tag's href is.
* singleSpa暴露出來(lái)的一個(gè)全局方法,用戶也可以基于它去判斷子應(yīng)用是運(yùn)行在基座應(yīng)用上還是獨(dú)立運(yùn)行
*/
window.singleSpaNavigate = navigateToUrl;
}
}
patchedUpdateState
single-spa/src/navigation/navigation-events.js
/**
* 通過(guò)裝飾器模式,增強(qiáng)pushstate和replacestate方法,除了原生的操作歷史記錄,還會(huì)調(diào)用reroute
* @param {*} updateState window.history.pushstate/replacestate
* @param {*} methodName 'pushstate' or 'replacestate'
*/
function patchedUpdateState(updateState, methodName) {
return function () {
// 當(dāng)前url
const urlBefore = window.location.href;
// pushstate或者replacestate的執(zhí)行結(jié)果
const result = updateState.apply(this, arguments);
// pushstate或replacestate執(zhí)行后的url地址
const urlAfter = window.location.href;
// 如果調(diào)用start傳遞了參數(shù)urlRerouteOnly為true,則這里不會(huì)觸發(fā)reroute
// https://zh-hans.single-spa.js.org/docs/api#start
if (!urlRerouteOnly || urlBefore !== urlAfter) {
urlReroute(createPopStateEvent(window.history.state, methodName));
}
return result;
};
}
createPopStateEvent
single-spa/src/navigation/navigation-events.js
function createPopStateEvent(state, originalMethodName) {
// https://github.com/single-spa/single-spa/issues/224 and https://github.com/single-spa/single-spa-angular/issues/49
// We need a popstate event even though the browser doesn't do one by default when you call replaceState, so that
// all the applications can reroute. We explicitly identify this extraneous event by setting singleSpa=true and
// singleSpaTrigger=<pushState|replaceState> on the event instance.
let evt;
try {
evt = new PopStateEvent("popstate", { state });
} catch (err) {
// IE 11 compatibility https://github.com/single-spa/single-spa/issues/299
// https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-html5e/bd560f47-b349-4d2c-baa8-f1560fb489dd
evt = document.createEvent("PopStateEvent");
evt.initPopStateEvent("popstate", false, false, state);
}
evt.singleSpa = true;
evt.singleSpaTrigger = originalMethodName;
return evt;
}
urlReroute
single-spa/src/navigation/navigation-events.js
export function setUrlRerouteOnly(val) {
urlRerouteOnly = val;
}
function urlReroute() {
reroute([], arguments);
}
小結(jié)
以上就是對(duì)整個(gè)single-spa
框架源碼的解讀,相信讀到這里你會(huì)有不一樣的理解吧,當(dāng)然第一遍讀完你有可能有點(diǎn)懵,我當(dāng)時(shí)就是這樣,這時(shí)候就需要那句古話了,書(shū)讀百遍,其義自現(xiàn)(干了這碗雞湯)
整個(gè)框架的源碼讀完以后,你會(huì)發(fā)現(xiàn):single-spa
的原理其實(shí)很簡(jiǎn)單,它就是一個(gè)子應(yīng)用加載器 + 狀態(tài)機(jī)的結(jié)合體,而且具體怎么加載子應(yīng)用還是基座應(yīng)用提供的;框架里面維護(hù)了各個(gè)子應(yīng)用的狀態(tài),以及在適當(dāng)?shù)臅r(shí)候負(fù)責(zé)更改子應(yīng)用的狀態(tài)、執(zhí)行相應(yīng)的生命周期函數(shù)
想想框架好像也不復(fù)雜,對(duì)吧??那接下來(lái)就來(lái)實(shí)現(xiàn)一個(gè)自己的single-spa
框架吧
手寫(xiě) single-spa 框架
經(jīng)過(guò)上面的閱讀,相信對(duì)single-spa
已經(jīng)有一定的理解了,接下來(lái)就來(lái)實(shí)現(xiàn)一個(gè)自己的single-spa
,就叫lyn-single-spa
吧。
我們好像只需要實(shí)現(xiàn)registerApplication
和start
兩個(gè)方法并導(dǎo)出即可。
寫(xiě)代碼之前,必須理清框架內(nèi)子應(yīng)用的各個(gè)狀態(tài)以及狀態(tài)的變更過(guò)程,為了便于理解,代碼寫(xiě)詳細(xì)的注釋,希望大家看完以后都可以實(shí)現(xiàn)一個(gè)自己的single-spa
// 實(shí)現(xiàn)子應(yīng)用的注冊(cè)、掛載、切換、卸載功能
/**
* 子應(yīng)用狀態(tài)
*/
// 子應(yīng)用注冊(cè)以后的初始狀態(tài)
const NOT_LOADED = 'NOT_LOADED'
// 表示正在加載子應(yīng)用源代碼
const LOADING_SOURCE_CODE = 'LOADING_SOURCE_CODE'
// 執(zhí)行完 app.loadApp,即子應(yīng)用加載完以后的狀態(tài)
const NOT_BOOTSTRAPPED = 'NOT_BOOTSTRAPPED'
// 正在初始化
const BOOTSTRAPPING = 'BOOTSTRAPPING'
// 執(zhí)行 app.bootstrap 之后的狀態(tài),表是初始化完成,處于未掛載的狀態(tài)
const NOT_MOUNTED = 'NOT_MOUNTED'
// 正在掛載
const MOUNTING = 'MOUNTING'
// 掛載完成,app.mount 執(zhí)行完畢
const MOUNTED = 'MOUNTED'
const UPDATING = 'UPDATING'
// 正在卸載
const UNMOUNTING = 'UNMOUNTING'
// 以下三種狀態(tài)這里沒(méi)有涉及
const UNLOADING = 'UNLOADING'
const LOAD_ERROR = 'LOAD_ERROR'
const SKIP_BECAUSE_BROKEN = 'SKIP_BECAUSE_BROKEN'
// 存放所有的子應(yīng)用
const apps = []
/**
* 注冊(cè)子應(yīng)用
* @param {*} appConfig = {
* name: '',
* app: promise function,
* activeWhen: location => location.pathname.startsWith(path),
* customProps: {}
* }
*/
export function registerApplication (appConfig) {
apps.push(Object.assign({}, appConfig, { status: NOT_LOADED }))
reroute()
}
// 啟動(dòng)
let isStarted = false
export function start () {
isStarted = true
}
function reroute () {
// 三類 app
const { appsToLoad, appsToMount, appsToUnmount } = getAppChanges()
if (isStarted) {
performAppChanges()
} else {
loadApps()
}
function loadApps () {
appsToLoad.map(toLoad)
}
function performAppChanges () {
// 卸載
appsToUnmount.map(toUnmount)
// 初始化 + 掛載
appsToMount.map(tryToBoostrapAndMount)
}
}
/**
* 掛載應(yīng)用
* @param {*} app
*/
async function tryToBoostrapAndMount(app) {
if (shouldBeActive(app)) {
// 正在初始化
app.status = BOOTSTRAPPING
// 初始化
await app.bootstrap(app.customProps)
// 初始化完成
app.status = NOT_MOUNTED
// 第二次判斷是為了防止中途用戶切換路由
if (shouldBeActive(app)) {
// 正在掛載
app.status = MOUNTING
// 掛載
await app.mount(app.customProps)
// 掛載完成
app.status = MOUNTED
}
}
}
/**
* 卸載應(yīng)用
* @param {*} app
*/
async function toUnmount (app) {
if (app.status !== 'MOUNTED') return app
// 更新?tīng)顟B(tài)為正在卸載
app.status = MOUNTING
// 執(zhí)行卸載
await app.unmount(app.customProps)
// 卸載完成
app.status = NOT_MOUNTED
return app
}
/**
* 加載子應(yīng)用
* @param {*} app
*/
async function toLoad (app) {
if (app.status !== NOT_LOADED) return app
// 更改狀態(tài)為正在加載
app.status = LOADING_SOURCE_CODE
// 加載 app
const res = await app.app()
// 加載完成
app.status = NOT_BOOTSTRAPPED
// 將子應(yīng)用導(dǎo)出的生命周期函數(shù)掛載到 app 對(duì)象上
app.bootstrap = res.bootstrap
app.mount = res.mount
app.unmount = res.unmount
app.unload = res.unload
// 加載完以后執(zhí)行 reroute 嘗試掛載
reroute()
return app
}
/**
* 將所有的子應(yīng)用分為三大類,待加載、待掛載、待卸載
*/
function getAppChanges () {
const appsToLoad = [],
appsToMount = [],
appsToUnmount = []
apps.forEach(app => {
switch (app.status) {
// 待加載
case NOT_LOADED:
appsToLoad.push(app)
break
// 初始化 + 掛載
case NOT_BOOTSTRAPPED:
case NOT_MOUNTED:
if (shouldBeActive(app)) {
appsToMount.push(app)
}
break
// 待卸載
case MOUNTED:
if (!shouldBeActive(app)) {
appsToUnmount.push(app)
}
break
}
})
return { appsToLoad, appsToMount, appsToUnmount }
}
/**
* 應(yīng)用需要激活嗎 ?
* @param {*} app
* return true or false
*/
function shouldBeActive (app) {
try {
return app.activeWhen(window.location)
} catch (err) {
console.error('shouldBeActive function error', err);
return false
}
}
// 讓子應(yīng)用判斷自己是否運(yùn)行在基座應(yīng)用中
window.singleSpaNavigate = true
// 監(jiān)聽(tīng)路由
window.addEventListener('hashchange', reroute)
window.history.pushState = patchedUpdateState(window.history.pushState)
window.history.replaceState = patchedUpdateState(window.history.replaceState)
/**
* 裝飾器,增強(qiáng) pushState 和 replaceState 方法
* @param {*} updateState
*/
function patchedUpdateState (updateState) {
return function (...args) {
// 當(dāng)前url
const urlBefore = window.location.href;
// pushState or replaceState 的執(zhí)行結(jié)果
const result = Reflect.apply(updateState, this, args)
// 執(zhí)行updateState之后的url
const urlAfter = window.location.href
if (urlBefore !== urlAfter) {
reroute()
}
return result
}
}
看著是不是很簡(jiǎn)單,加注釋也才200
行而已,當(dāng)然,這只是一個(gè)簡(jiǎn)版的single-spa
框架,沒(méi)什么健壯性可言,但也正因?yàn)楹?jiǎn)單,所以更能說(shuō)明single-spa
框架的本質(zhì)。
single-spa-vue 源碼分析
single-spa-vue
負(fù)責(zé)為vue
應(yīng)用生成通用的生命周期鉤子,這些鉤子函數(shù)負(fù)責(zé)子應(yīng)用的初始化、掛載、更新(數(shù)據(jù))、卸載。
import "css.escape";
const defaultOpts = {
// required opts
Vue: null,
appOptions: null,
template: null
};
/**
* 判斷參數(shù)的合法性
* 返回生命周期函數(shù),其中的mount方法負(fù)責(zé)實(shí)例化子應(yīng)用,update方法提供了基座應(yīng)用和子應(yīng)用通信的機(jī)會(huì),unmount卸載子應(yīng)用,bootstrap感覺(jué)沒(méi)啥用
* @param {*} userOpts = {
* Vue,
* appOptions: {
* el: '#id',
* store,
* router,
* render: h => h(App)
* }
* }
* return 四個(gè)生命周期函數(shù)組成的對(duì)象
*/
export default function singleSpaVue(userOpts) {
// object
if (typeof userOpts !== "object") {
throw new Error(`single-spa-vue requires a configuration object`);
}
// 合并用戶選項(xiàng)和默認(rèn)選項(xiàng)
const opts = {
...defaultOpts,
...userOpts
};
// Vue構(gòu)造函數(shù)
if (!opts.Vue) {
throw Error("single-spa-vue must be passed opts.Vue");
}
// appOptions
if (!opts.appOptions) {
throw Error("single-spa-vue must be passed opts.appOptions");
}
// el選擇器
if (
opts.appOptions.el &&
typeof opts.appOptions.el !== "string" &&
!(opts.appOptions.el instanceof HTMLElement)
) {
throw Error(
`single-spa-vue: appOptions.el must be a string CSS selector, an HTMLElement, or not provided at all. Was given ${typeof opts
.appOptions.el}`
);
}
// Just a shared object to store the mounted object state
// key - name of single-spa app, since it is unique
let mountedInstances = {};
/**
* 返回一個(gè)對(duì)象,每個(gè)屬性都是一個(gè)生命周期函數(shù)
*/
return {
bootstrap: bootstrap.bind(null, opts, mountedInstances),
mount: mount.bind(null, opts, mountedInstances),
unmount: unmount.bind(null, opts, mountedInstances),
update: update.bind(null, opts, mountedInstances)
};
}
function bootstrap(opts) {
if (opts.loadRootComponent) {
return opts.loadRootComponent().then(root => (opts.rootComponent = root));
} else {
return Promise.resolve();
}
}
/**
* 做了三件事情:
* 大篇幅的處理el元素
* 然后是render函數(shù)
* 實(shí)例化子應(yīng)用
*/
function mount(opts, mountedInstances, props) {
const instance = {};
return Promise.resolve().then(() => {
const appOptions = { ...opts.appOptions };
// 可以通過(guò)props.domElement屬性單獨(dú)設(shè)置自應(yīng)用的渲染DOM容器,當(dāng)然appOptions.el必須為空
if (props.domElement && !appOptions.el) {
appOptions.el = props.domElement;
}
let domEl;
if (appOptions.el) {
if (typeof appOptions.el === "string") {
// 子應(yīng)用的DOM容器
domEl = document.querySelector(appOptions.el);
if (!domEl) {
throw Error(
`If appOptions.el is provided to single-spa-vue, the dom element must exist in the dom. Was provided as ${appOptions.el}`
);
}
} else {
// 處理DOM容器是元素的情況
domEl = appOptions.el;
if (!domEl.id) {
// 設(shè)置元素ID
domEl.id = `single-spa-application:${props.name}`;
}
appOptions.el = `#${CSS.escape(domEl.id)}`;
}
} else {
// 當(dāng)然如果沒(méi)有id,這里會(huì)自動(dòng)生成一個(gè)id
const htmlId = `single-spa-application:${props.name}`;
appOptions.el = `#${CSS.escape(htmlId)}`;
domEl = document.getElementById(htmlId);
if (!domEl) {
domEl = document.createElement("div");
domEl.id = htmlId;
document.body.appendChild(domEl);
}
}
appOptions.el = appOptions.el + " .single-spa-container";
// single-spa-vue@>=2 always REPLACES the `el` instead of appending to it.
// We want domEl to stick around and not be replaced. So we tell Vue to mount
// into a container div inside of the main domEl
if (!domEl.querySelector(".single-spa-container")) {
const singleSpaContainer = document.createElement("div");
singleSpaContainer.className = "single-spa-container";
domEl.appendChild(singleSpaContainer);
}
instance.domEl = domEl;
// render
if (!appOptions.render && !appOptions.template && opts.rootComponent) {
appOptions.render = h => h(opts.rootComponent);
}
// data
if (!appOptions.data) {
appOptions.data = {};
}
appOptions.data = { ...appOptions.data, ...props };
// 實(shí)例化子應(yīng)用
instance.vueInstance = new opts.Vue(appOptions);
if (instance.vueInstance.bind) {
instance.vueInstance = instance.vueInstance.bind(instance.vueInstance);
}
mountedInstances[props.name] = instance;
return instance.vueInstance;
});
}
// 基座應(yīng)用通過(guò)update生命周期函數(shù)可以更新子應(yīng)用的屬性
function update(opts, mountedInstances, props) {
return Promise.resolve().then(() => {
// 應(yīng)用實(shí)例
const instance = mountedInstances[props.name];
// 所有的屬性
const data = {
...(opts.appOptions.data || {}),
...props
};
// 更新實(shí)例對(duì)象上的屬性值,vm.test = 'xxx'
for (let prop in data) {
instance.vueInstance[prop] = data[prop];
}
});
}
// 調(diào)用$destroy鉤子函數(shù),銷毀子應(yīng)用
function unmount(opts, mountedInstances, props) {
return Promise.resolve().then(() => {
const instance = mountedInstances[props.name];
instance.vueInstance.$destroy();
instance.vueInstance.$el.innerHTML = "";
delete instance.vueInstance;
if (instance.domEl) {
instance.domEl.innerHTML = "";
delete instance.domEl;
}
});
}
結(jié)語(yǔ)
到這里就結(jié)束了,文章比較長(zhǎng),寫(xiě)這篇文章也花費(fèi)了好幾天的時(shí)間,但是感覺(jué)真的很好,收獲滿滿
,特別是最后手寫(xiě)框架部分。
也給各位同學(xué)一個(gè)建議,一定要勤動(dòng)手,不動(dòng)筆墨不讀書(shū)
,當(dāng)你真的把框架寫(xiě)出來(lái)時(shí),那個(gè)感覺(jué)是只看源碼完全所不能比擬的,檢驗(yàn)?zāi)闶欠裾娴亩蚣茉淼淖詈棉k法,就是看你能否寫(xiě)一個(gè)框架出來(lái)
。
愿同學(xué)們也能收獲滿滿??!
感謝各位的:點(diǎn)贊、收藏和評(píng)論,我們下期見(jiàn)。
當(dāng)學(xué)習(xí)成為了習(xí)慣,知識(shí)也就變成了常識(shí)。 感謝各位的 點(diǎn)贊、收藏和評(píng)論。
新視頻和文章會(huì)第一時(shí)間在微信公眾號(hào)發(fā)送,歡迎關(guān)注:李永寧lyn
文章已收錄到 github 倉(cāng)庫(kù) liyongning/blog,歡迎 Watch 和 Star。