微前端框架 之 single-spa 從入門到精通
?來源:李永寧
https://juejin.im/post/6862661545592111111
前序
目的
會使用
single-spa開發(fā)項目,然后打包部署上線刨析
single-spa的源碼原理手寫一個自己的
single-spa框架
過程
編寫示例項目
打包部署
框架源碼解讀
手寫框架
關(guān)于微前端的介紹這里就不再贅述了,網(wǎng)上有很多的文章,本文的重點在于刨析微前端框架single-spa的實現(xiàn)原理。
single-spa是一個很好的微前端基礎(chǔ)框架,qiankun框架就是基于single-spa來實現(xiàn)的,在single-spa的基礎(chǔ)上做了一層封裝,也解決了single-spa的一些缺陷。
因為single-spa是一個基礎(chǔ)的微前端框架,了解了它的實現(xiàn)原理,再去看其它的微前端框架,就會非常容易了。
提示
先熟悉基本使用,熟悉常用的API,可通過示例項目 + 官網(wǎng)相結(jié)合來達(dá)成
如果基礎(chǔ)比較好,可以先讀后面的
手寫 single-spa 框架部分,再回來閱讀源碼,效果可能會更好文章中涉及到的所有代碼都在 github: https://github.com/liyongning/micro-frontend.git(示例項目 +
single-spa源碼分析 + 手寫single-spa框架 +single-spa-vue源碼分析)
示例項目
新建項目目錄,接下來的所有代碼都會在該目錄中完成
mkdir?micro-frontend?&&?cd?micro-frontend
示例代碼都是通過vue來編寫的,當(dāng)然也可以采用其它的,比如react或者原生JS等
子應(yīng)用 app1
新建子應(yīng)用
vue?create?app1
按圖選擇,去除一切項目不需要的干擾項,后面一路回車,等待應(yīng)用創(chuàng)建完畢


配置子應(yīng)用
以下所有的操作都在項目根目錄
/micro-frontend/app1下完成
vue.config.js
在項目根目錄下新建vue.config.js文件
const?package?=?require('./package.json')
module.exports?=?{
??//?告訴子應(yīng)用在這個地址加載靜態(tài)資源,否則會去基座應(yīng)用的域名下加載
??publicPath:?'//localhost:8081',
??//?開發(fā)服務(wù)器
??devServer:?{
????port:?8081
??},
??configureWebpack:?{
????//?導(dǎo)出umd格式的包,在全局對象上掛載屬性package.name,基座應(yīng)用需要通過這個全局對象獲取一些信息,比如子應(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)用注冊到single-spa的基座應(yīng)用時需要用到
改造入口文件
//?/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)用獨立運行、部署,不依賴于基座應(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(()?=>?{})
}
更改視圖文件
app1 home page
app1 about page
環(huán)境配置文件
.env
應(yīng)用獨立運行時的開發(fā)環(huán)境配置
NODE_ENV=development
VUE_APP_BASE_URL=/
.env.micro
作為子應(yīng)用運行時的開發(fā)環(huán)境配置
NODE_ENV=development
VUE_APP_BASE_URL=/app1
.env.buildMicro
作為子應(yīng)用構(gòu)建生產(chǎn)環(huán)境bundle時的環(huán)境配置,但這里的NODE_ENV為development,而不是production,是為了方便,這個方便其實single-spa帶來的弊端(js entry的弊端)
NODE_ENV=development
VUE_APP_BASE_URL=/app1
修改路由文件
//?/src/router/index.js
//?...
const?router?=?new?VueRouter({
??mode:?'history',
??//?通過環(huán)境變量來配置路由的?base?url
??base:?process.env.VUE_APP_BASE_URL,
??routes
})
//?...
修改package.json中的script
{
??"name":?"app1",
??//?...
??"scripts":?{
????//?獨立運行
????"serve":?"vue-cli-service?serve",
????//?作為子應(yīng)用運行
????"serve:micro":?"vue-cli-service?serve?--mode?micro",
????//?構(gòu)建子應(yīng)用
????"build":?"vue-cli-service?build?--mode?buildMicro"
??},
????//?...
}
啟動應(yīng)用
應(yīng)用獨立運行
npm?run?serve
當(dāng)然下面的啟動方式也可以,只不過會在pathname的開頭加了/app1前綴
npm?run?serve:micro
作為子應(yīng)用運行
npm?run?serve:micro
作為獨立應(yīng)用訪問

子應(yīng)用 app2
在/micro-frontend目錄下新建子應(yīng)用app2,步驟同app1,只需把過程中出現(xiàn)的'app1'字樣改成'app2'即可,vue.config.js中的8081改成8082`
啟動應(yīng)用,作為獨立應(yīng)用訪問

基座應(yīng)用 layout
在/micro-frontend目錄下新建基座應(yīng)用,為了簡潔明了,新建項目時選擇的配置項和子應(yīng)用一樣;在本示例中基座應(yīng)用采用了vue來實現(xiàn),用別的方式或者框架實現(xiàn)也可以,比如自己用webpack構(gòu)建一個項目。
以下操作都在
/micro-frontend/layout目錄下進行
安裝single-spa
npm?i?single-spa?-S
改造基座項目
入口文件
//?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ù),返回一個?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很重要,需要從這個全局對象中拿到子應(yīng)用暴露出來的生命周期函數(shù)
????return?window[globalVar]
??}
}
//?子應(yīng)用列表
const?apps?=?[
??{
????//?子應(yīng)用名稱
????name:?'app1',
????//?子應(yīng)用加載函數(shù),是一個promise
????app:?loadApp('http://localhost:8081',?'app1'),
????//?當(dāng)路由滿足條件時(返回true),激活(掛載)子應(yīng)用
????activeWhen:?location?=>?location.pathname.startsWith('/app1'),
????//?傳遞給子應(yīng)用的對象
????customProps:?{}
??},
??{
????name:?'app2',
????app:?loadApp('http://localhost:8082',?'app2'),
????activeWhen:?location?=>?location.pathname.startsWith('/app2'),
????customProps:?{}
??},
]
//?注冊子應(yīng)用
for?(let?i?=?apps.length?-?1;?i?>=?0;?i--)?{
??registerApplication(apps[i])
}
new?Vue({
??router,
??mounted()?{
????//?啟動
????start()
??},
??render:?h?=>?h(App)
}).$mount('#app')
App.vue
路由
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
啟動基座應(yīng)用
npm?run?serve
瀏覽器訪問基座應(yīng)用

終于看到了結(jié)果。
小技巧
有時候
single-spa可能會報一些我們現(xiàn)在無法理解的錯誤,我們可能需要去做代碼調(diào)試,閱讀源碼時碰到不理解的地方也需要編寫示例+ 單步調(diào)試,但是默認(rèn)的是已經(jīng)打包壓縮后的代碼,不太方便做這些,大家可以在node_modules目錄找到single-spa目錄,把目錄下的package.json中的module字段的值改為lib/single-spa.dev.js,這是一個未壓縮的bundle,利于代碼的閱讀的調(diào)試,當(dāng)然需要重啟應(yīng)用。
子應(yīng)用也是一樣類似的技巧,因為
single-spa-vue就一個文件,可以直接拷貝出來放到項目的/src目錄下,將main.js中的引入的single-spa-vue改成當(dāng)前目錄即可。
打包部署
打包
在各個項目的根目錄下分別執(zhí)行
npm?run?build
部署
可以將打包后的bundle發(fā)布到nginx服務(wù)器上,這個nginx服務(wù)器可以是單獨的服務(wù)器、或者虛擬機、亦或是docker容器都行,這里采用serve在本地模擬部署
如果你有條件部署到nginx上,需要注意nginx的代理配置
對于子應(yīng)用靜態(tài)資源的加載只需要攔截相應(yīng)的前綴將請求轉(zhuǎn)發(fā)到對應(yīng)子應(yīng)用的目錄下即可 頁面刷新只需要攔截到主應(yīng)用即可,主應(yīng)用內(nèi)部自己根據(jù) activeWhen去掛載對應(yīng)的子應(yīng)用
全局安裝 serve
npm?i?serve?-g
在各個項目的根目錄下啟動 serve
serve?./dist?-p?port
在瀏覽器訪問基座應(yīng)用的地址,發(fā)現(xiàn)得到和剛才一樣的結(jié)果
single-spa 源碼分析
整個閱讀過程以示例項目為例,閱讀源碼時一定要多動手寫注釋、做筆記,遇到不理解的地方編寫示例代碼 +
console.log+ 單步調(diào)試,切記不要只看不動手。
single-spa 源碼閱讀思維導(dǎo)圖

這是我在閱讀時整理的一個思維導(dǎo)圖,源碼中也寫了大量的注釋,大家可以參照著進行閱讀。Ok ??!這就開始吧

從源碼目錄中可以看到,single-spa是使用rollup來打包的,從rollup.config.js中可以發(fā)現(xiàn)入口是single-spa.js, 打開會發(fā)現(xiàn)里面導(dǎo)出了一大堆東西,有我們非常熟悉的各個方法,我們就從registerApplication方法開始
registerApplication 注冊子應(yīng)用
single-spa/src/applications/apps.js
/**
?*?注冊應(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)用配置對象
?*?@param?{*}?appOrLoadApp?應(yīng)用的加載方法,是一個?promise
?*?@param?{*}?activeWhen?判斷應(yīng)用是否激活的一個方法,方法返回?true?or?false
?*?@param?{*}?customProps?傳遞給子應(yīng)用的?props?對象
?*/
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
??????)
????);
??//?將各個應(yīng)用的配置信息都存放到?apps?數(shù)組中
??apps.push(
????//?給每個應(yīng)用增加一個內(nèi)置屬性
????assign(
??????{
????????loadErrorTime:?null,
????????//?最重要的,應(yīng)用的狀態(tài)
????????status:?NOT_LOADED,
????????parcels:?{},
????????devtools:?{
??????????overlays:?{
????????????options:?{},
????????????selectors:?[],
??????????},
????????},
??????},
??????registration
????)
??);
??//?瀏覽器環(huán)境運行
??if?(isInBrowser)?{
????//?https://zh-hans.single-spa.js.org/docs/api#ensurejquerysupport
????//?如果頁面中使用了jQuery,則給jQuery打patch
????ensureJQuerySupport();
????reroute();
??}
}
sanitizeArguments 格式化用戶傳遞的子應(yīng)用配置參數(shù)
single-spa/src/applications/apps.js
//?返回處理后的應(yīng)用配置對象
function?sanitizeArguments(
??appNameOrConfig,
??appOrLoadApp,
??activeWhen,
??customProps
)?{
??//?判斷第一個參數(shù)是否為對象
??const?usingObjectAPI?=?typeof?appNameOrConfig?===?"object";
??//?初始化應(yīng)用配置對象
??const?registration?=?{
????name:?null,
????loadApp:?null,
????activeWhen:?null,
????customProps:?null,
??};
??if?(usingObjectAPI)?{
????//?注冊應(yīng)用的時候傳遞的參數(shù)是對象
????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;
??}
??//?如果第二個參數(shù)不是一個函數(shù),比如是一個包含已經(jīng)生命周期的對象,則包裝成一個返回?promise?的函數(shù)
??registration.loadApp?=?sanitizeLoadApp(registration.loadApp);
??//?如果用戶沒有提供?props?對象,則給一個默認(rèn)的空對象
??registration.customProps?=?sanitizeCustomProps(registration.customProps);
??//?保證activeWhen是一個返回boolean值的函數(shù)
??registration.activeWhen?=?sanitizeActiveWhen(registration.activeWhen);
??//?返回處理后的應(yīng)用配置對象
??return?registration;
}
validateRegisterWithConfig
single-spa/src/applications/apps.js
/**
?*?驗證應(yīng)用配置對象的各個屬性是否存在不合法的情況,存在則拋出錯誤
?*?@param?{*}?config?=?{?name:?'app1',?app:?function,?activeWhen:?function,?customProps:?{}?}
?*/
export?function?validateRegisterWithConfig(config)?{
??//?異常判斷,應(yīng)用的配置對象不能是數(shù)組或者null
??if?(Array.isArray(config)?||?config?===?null)
????throw?Error(
??????formatErrorMessage(
????????39,
????????__DEV__?&&?"Configuration?object?can't?be?an?Array?or?null!"
??????)
????);
??//?配置對象只能包括這四個key
??const?validKeys?=?["name",?"app",?"activeWhen",?"customProps"];
??//?找到配置對象存在的無效的key
??const?invalidKeys?=?Object.keys(config).reduce(
????(invalidKeys,?prop)?=>
??????validKeys.indexOf(prop)?>=?0???invalidKeys?:?invalidKeys.concat(prop),
????[]
??);
??//?如果存在無效的key,則拋出一個錯誤
??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ī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?屬性只能是一個對象或者函數(shù)
??//?對象是一個已被解析過的對象,是一個包含各個生命周期的對象;
??//?加載函數(shù)必須返回一個?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"
??????)
????);
??//?第三個參數(shù),可以是一個字符串,也可以是一個函數(shù),也可以是兩者組成的一個數(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對象必須是一個對象
??if?(!validCustomProps(config.customProps))
????throw?Error(
??????formatErrorMessage(
????????22,
????????__DEV__?&&?"The?optional?config.customProps?must?be?an?object"
??????)
????);
}
validateRegisterWithArguments
single-spa/src/applications/apps.js
//?同樣是驗證四個參數(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
//?保證第二個參數(shù)一定是一個返回?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
//?得到一個函數(shù),函數(shù)負(fù)責(zé)判斷瀏覽器當(dāng)前地址是否和用戶給定的baseURL相匹配,匹配返回true,否則返回false
function?sanitizeActiveWhen(activeWhen)?{
??//?[]
??let?activeWhenArray?=?Array.isArray(activeWhen)???activeWhen?:?[activeWhen];
??//?保證數(shù)組中每個元素都是一個函數(shù)
??activeWhenArray?=?activeWhenArray.map((activeWhenOrPath)?=>
????typeof?activeWhenOrPath?===?"function"
????????activeWhenOrPath
??????//?activeWhen如果是一個路徑,則保證成一個函數(shù)
??????:?pathToActiveWhen(activeWhenOrPath)
??);
??//?返回一個函數(shù),函數(shù)返回一個?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大類,
?*?首次加載時執(zhí)行l(wèi)oadApp
?*?后續(xù)的路由切換執(zhí)行performAppChange
?*?為四大類的應(yīng)用分別執(zhí)行相應(yīng)的操作,比如更改app.status,執(zhí)行生命周期函數(shù)
?*?所以,從這里也可以看出來,single-spa就是一個維護應(yīng)用的狀態(tài)機
?*?@param?{*}?pendingPromises?
?*?@param?{*}?eventArguments?
?*/
export?function?reroute(pendingPromises?=?[],?eventArguments)?{
??//?應(yīng)用正在切換,這個狀態(tài)會在執(zhí)行performAppChanges之前置為true,執(zhí)行結(jié)束之后再置為false
??//?如果在中間用戶重新切換路由了,即走這個if分支,暫時看起來就在數(shù)組中存儲了一些信息,沒看到有什么用
??//?字面意思理解就是用戶等待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();
??}
??//?整體返回一個立即resolved的promise,通過微任務(wù)來加載apps
??function?loadApps()?{
????return?Promise.resolve().then(()?=>?{
??????//?加載每個子應(yīng)用,并做一系列的狀態(tài)變更和驗證(比如結(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ā),給用戶提供搞事情的機會
??????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í)行一些清理動作
??????//?其實一般情況下這里沒有真的移除應(yīng)用
??????const?unloadPromises?=?appsToUnload.map(toUnloadPromise);
??????//?卸載應(yīng)用,更改狀態(tài),執(zhí)行unmount生命周期函數(shù)
??????const?unmountUnloadPromises?=?appsToUnmount
????????.map(toUnmountPromise)
????????//?卸載完然后移除,通過注冊微任務(wù)的方式實現(xiàn)
????????.map((unmountPromise)?=>?unmountPromise.then(toUnloadPromise));
??????const?allUnmountPromises?=?unmountUnloadPromises.concat(unloadPromises);
??????const?unmountAllPromise?=?Promise.all(allUnmountPromises);
??????//?卸載全部完成后觸發(fā)一個事件
??????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
???????*?這個原因其實是因為這些操作都是通過注冊不同的微任務(wù)實現(xiàn)的,而JS是單線程執(zhí)行,
???????*?所以自然后續(xù)的只能等待前面的執(zhí)行完了才能執(zhí)行
???????*?這里一般情況下其實不會執(zhí)行,只有手動執(zhí)行了unloadApplication方法才會二次加載
???????*/
??????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,其實做的事情很簡單,就是改變app.status,執(zhí)行生命周期函數(shù)
???????*?當(dāng)然這里的初始化和掛載其實是前后腳一起完成的(只要中間用戶沒有切換路由)
???????*/
??????const?mountPromises?=?appsToMount
????????.filter((appToMount)?=>?appsToLoad.indexOf(appToMount)?0)
????????.map((appToMount)?=>?{
??????????return?tryToBootstrapAndMount(appToMount,?unmountAllPromise);
????????});
??????//?后面就沒啥了,可以理解為收尾工作
??????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
/**
?*?通過微任務(wù)加載子應(yīng)用,其實singleSpa中很多地方都用了微任務(wù)
?*?這里最終是return了一個promise出行,在注冊了加載子應(yīng)用的微任務(wù)
?*?概括起來就是:
?*??更改app.status為LOAD_SOURCE_CODE?=>?NOT_BOOTSTRAP,當(dāng)然還有可能是LOAD_ERROR
?*??執(zhí)行加載函數(shù),并將props傳遞給加載函數(shù),給用戶處理props的一個機會,因為這個props是一個完備的props
?*??驗證加載函數(shù)的執(zhí)行結(jié)果,必須為promise,且加載函數(shù)內(nèi)部必須return一個對象
?*??這個對象是子應(yīng)用的,對象中必須包括各個必須的生命周期函數(shù)
?*??然后將生命周期方法通過一個函數(shù)包裹并掛載到app對象上
?*??app加載完成,刪除app.loadPromise
?*?@param?{*}?app?
?*/
export?function?toLoadPromise(app)?{
??return?Promise.resolve().then(()?=>?{
????if?(app.loadPromise)?{
??????//?說明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實例
????????//?其實這里有個疑問,這個props是怎么傳遞給子應(yīng)用的,感覺跟后面的生命周期函數(shù)有關(guān)
????????const?loadPromise?=?app.loadApp(getProps(app));
????????//?加載函數(shù)需要返回一個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)
????????????)
??????????);
????????}
????????//?這里很重要,這個val就是示例項目中加載函數(shù)中return出來的window.singleSpa,這個屬性是子應(yīng)用打包時設(shè)置的
????????return?loadPromise.then((val)?=>?{
??????????app.loadErrorTime?=?null;
??????????//?window.singleSpa
??????????appOpts?=?val;
??????????let?validationErrMessage,?validationErrCode;
??????????//?以下進行一系列的驗證,已window.singleSpa為例說明,簡稱g.s
??????????//?g.s必須為對象
??????????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);
??????????//?說明上述驗證失敗,拋出錯誤提示信息
??????????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對象上掛載生命周期方法,每個方法都接收一個props作為參數(shù),方法內(nèi)部執(zhí)行子應(yīng)用導(dǎo)出的生命周期函數(shù),并確保生命周期函數(shù)返回一個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í)行到這里說明子應(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)容其實在官網(wǎng)也都有提到,比如singleSpa實例,目的是為了子應(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,以下對customProps對象的判斷邏輯有點多余
??//?因為前面的參數(shù)格式化已經(jīng)保證customProps肯定是一個對象
??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
//?判斷一個變量是否為promise
export?function?smellsLikeAPromise(promise)?{
??return?(
????promise?&&
????typeof?promise.then?===?"function"?&&
????typeof?promise.catch?===?"function"
??);
}
flattenFnArray
single-spa/src/lifecycles/lifecycle.helpers.js
/**
?*?返回一個接受props作為參數(shù)的函數(shù),這個函數(shù)負(fù)責(zé)執(zhí)行子應(yīng)用中的生命周期函數(shù),
?*?并確保生命周期函數(shù)返回的結(jié)果為promise
?*?@param?{*}?appOrParcel?=>?window.singleSpa,子應(yīng)用打包后的對象
?*?@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)用可能不會設(shè)置,比如unload
??if?(fns.length?===?0)?{
????fns?=?[()?=>?Promise.resolve()];
??}
??const?type?=?objectType(appOrParcel);
??const?name?=?toName(appOrParcel);
??return?function?(props)?{
????//?這里最后返回了一個promise鏈,這個操作似乎沒啥必要,因為不可能出現(xiàn)同名的生命周期函數(shù),所以,這里將生命周期函數(shù)放數(shù)組,沒太理解目的是啥
????return?fns.reduce((resultPromise,?fn,?index)?=>?{
??????return?resultPromise.then(()?=>?{
????????//?執(zhí)行生命周期函數(shù),傳遞props給函數(shù),并驗證函數(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í)行清理操作
?*?
?*?其實一般情況是不會執(zhí)行移除操作的,除非你手動調(diào)用unloadApplication方法
?*?單步調(diào)試會發(fā)現(xiàn)appsToUnload對象是個空對象,所以第一個if就return了,這里啥也沒做
?*?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方法手動執(zhí)行移除時才會
???????*?執(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;
????//?在合理的時間范圍內(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í)行一些清理動作,其實就是從appsToUnload數(shù)組中移除該app,移除生命周期函數(shù),更改app.status
//?但應(yīng)用不是真的被移除,后面再激活時不需要重新去下載資源,,只是做一些狀態(tài)上的變更,當(dāng)然load的那個過程還是需要的,這點可能需要再確認(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ù)合理的執(zhí)行時間
?*?在合理的時間內(nèi)執(zhí)行生命周期函數(shù),并將函數(shù)的執(zhí)行結(jié)果resolve出去
?*?@param?{*}?appOrParcel?=>?app
?*?@param?{*}?lifecycle?=>?生命周期函數(shù)名
?*/
export?function?reasonableTime(appOrParcel,?lifecycle)?{
??//?應(yīng)用的超時配置
??const?timeoutConfig?=?appOrParcel.timeouts[lifecycle];
??//?超時警告
??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ù)時像子應(yīng)用傳遞的props,所以之前執(zhí)行l(wèi)oadApp傳遞props不會到子應(yīng)用,
????//?那么設(shè)計估計是給用戶自己處理props的一個機會吧,因為那個時候處理的props已經(jīng)是{?...customProps,?...內(nèi)置props?}
????appOrParcel[lifecycle](getProps(appOrParcel))
??????.then((val)?=>?{
????????finished?=?true;
????????resolve(val);
??????})
??????.catch((val)?=>?{
????????finished?=?true;
????????reject(val);
??????});
????//?下面就沒啥了,就是超時的一些提示信息
????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?????????????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的一些處理,沒使用過parcels,所以unmountChildrenParcels?=?[]
????const?unmountChildrenParcels?=?Object.keys(
??????appOrParcel.parcels
????).map((parcelId)?=>?appOrParcel.parcels[parcelId].unmountThisParcel());
????let?parcelError;
????return?Promise.all(unmountChildrenParcels)
??????//?在合理的時間范圍內(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
?*?這里這個兩次判斷還是很重要的
?*/
function?tryToBootstrapAndMount(app,?unmountAllPromise)?{
??if?(shouldBeActive(app))?{
????//?一次判斷為true,才會執(zhí)行初始化
????return?toBootstrapPromise(app).then((app)?=>
??????unmountAllPromise.then(()?=>
????????//?第二次,?兩次都為true才會去掛載
????????shouldBeActive(app)???toMountPromise(app)?:?app
??????)
????);
??}?else?{
????//?卸載
????return?unmountAllPromise.then(()?=>?app);
??}
}
toBootstrapPromise
single-spa/src/lifecycles/bootstrap.js
//?初始化app,更改app.status,在合理的時間內(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其實在不同的階段提供了相應(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)用會被加載,但不會初始化、掛載和卸載,有了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之后如果一直沒有調(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)聽路由變化
single-spa/src/navigation/navigation-events.js
以下代碼會被打包進bundle的全局作用域內(nèi),bundle被加載以后就會自動執(zhí)行。這句提示不需要的話可自動忽略
/**
?*?監(jiān)聽路由變化
?*/
if?(isInBrowser)?{
??//?We?will?trigger?an?app?change?for?any?routing?events,監(jiān)聽hashchange和popstate事件
??window.addEventListener("hashchange",?urlReroute);
??window.addEventListener("popstate",?urlReroute);
??//?Monkeypatch?addEventListener?so?that?we?can?ensure?correct?timing
??/**
???*?擴展原生的addEventListener和removeEventListener方法
???*?每次注冊事件和事件處理函數(shù)都會將事件和處理函數(shù)保存下來,當(dāng)然移除時也會做刪除
???*?*/?
??const?originalAddEventListener?=?window.addEventListener;
??const?originalRemoveEventListener?=?window.removeEventListener;
??window.addEventListener?=?function?(eventName,?fn)?{
????if?(typeof?fn?===?"function")?{
??????if?(
????????//?eventName只能是hashchange或popstate?&&?對應(yīng)事件的fn注冊函數(shù)沒有注冊
????????routingEventsListeningTo.indexOf(eventName)?>=?0?&&
????????!find(capturedEventListeners[eventName],?(listener)?=>?listener?===?fn)
??????)?{
????????//?注冊(保存)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);
??};
??//?增強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??tag's?href?is.
?????*?singleSpa暴露出來的一個全局方法,用戶也可以基于它去判斷子應(yīng)用是運行在基座應(yīng)用上還是獨立運行
?????*/
????window.singleSpaNavigate?=?navigateToUrl;
??}
}
patchedUpdateState
single-spa/src/navigation/navigation-events.js
/**
?*?通過裝飾器模式,增強pushstate和replacestate方法,除了原生的操作歷史記錄,還會調(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,則這里不會觸發(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=?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é)
以上就是對整個single-spa框架源碼的解讀,相信讀到這里你會有不一樣的理解吧,當(dāng)然第一遍讀完你有可能有點懵,我當(dāng)時就是這樣,這時候就需要那句古話了,書讀百遍,其義自現(xiàn)(干了這碗雞湯)
整個框架的源碼讀完以后,你會發(fā)現(xiàn):single-spa的原理其實很簡單,它就是一個子應(yīng)用加載器 + 狀態(tài)機的結(jié)合體,而且具體怎么加載子應(yīng)用還是基座應(yīng)用提供的;框架里面維護了各個子應(yīng)用的狀態(tài),以及在適當(dāng)?shù)臅r候負(fù)責(zé)更改子應(yīng)用的狀態(tài)、執(zhí)行相應(yīng)的生命周期函數(shù)
想想框架好像也不復(fù)雜,對吧??那接下來就來實現(xiàn)一個自己的single-spa框架吧
手寫 single-spa 框架
經(jīng)過上面的閱讀,相信對single-spa已經(jīng)有一定的理解了,接下來就來實現(xiàn)一個自己的single-spa,就叫lyn-single-spa吧。
我們好像只需要實現(xiàn)registerApplication和start兩個方法并導(dǎo)出即可。
寫代碼之前,必須理清框架內(nèi)子應(yīng)用的各個狀態(tài)以及狀態(tài)的變更過程,為了便于理解,代碼寫詳細(xì)的注釋,希望大家看完以后都可以實現(xiàn)一個自己的single-spa
//?實現(xiàn)子應(yīng)用的注冊、掛載、切換、卸載功能
/**
?*?子應(yīng)用狀態(tài)
?*/
//?子應(yīng)用注冊以后的初始狀態(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)這里沒有涉及
const?UNLOADING?=?'UNLOADING'
const?LOAD_ERROR?=?'LOAD_ERROR'
const?SKIP_BECAUSE_BROKEN?=?'SKIP_BECAUSE_BROKEN'
//?存放所有的子應(yīng)用
const?apps?=?[]
/**
?*?注冊子應(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()
}
//?啟動
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.status?=?NOT_MOUNTED
????//?第二次判斷是為了防止中途用戶切換路由
????if?(shouldBeActive(app))?{
??????//?正在掛載
??????app.status?=?MOUNTING
??????//?掛載
??????await?app.mount()
??????//?掛載完成
??????app.status?=?MOUNTED
????}
??}
}
/**
?*?卸載應(yīng)用
?*?@param?{*}?app?
?*/
async?function?toUnmount?(app)?{
??if?(app.status?!==?'MOUNTED')?return?app
??//?更新狀態(tài)為正在卸載
??app.status?=?MOUNTING
??//?執(zhí)行卸載
??await?app.unmount()
??//?卸載完成
??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?對象上
??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īng)用中
window.singleSpaNavigate?=?true
//?監(jiān)聽路由
window.addEventListener('hashchange',?reroute)
window.history.pushState?=?patchedUpdateState(window.history.pushState)
window.history.replaceState?=?patchedUpdateState(window.history.replaceState)
/**
?*?裝飾器,增強?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
??}
}
看著是不是很簡單,加注釋也才200行而已,當(dāng)然,這只是一個簡版的single-spa框架,沒什么健壯性可言,但也正因為簡單,所以更能說明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é)實例化子應(yīng)用,update方法提供了基座應(yīng)用和子應(yīng)用通信的機會,unmount卸載子應(yīng)用,bootstrap感覺沒啥用
?*?@param?{*}?userOpts?=?{
?*????Vue,
?*????appOptions:?{
?*??????el:?'#id',
?*??????store,
?*??????router,
?*??????render:?h?=>?h(App)
?*????}?
?*?}
?*?return?四個生命周期函數(shù)組成的對象
?*/
export?default?function?singleSpaVue(userOpts)?{
??//?object
??if?(typeof?userOpts?!==?"object")?{
????throw?new?Error(`single-spa-vue?requires?a?configuration?object`);
??}
??//?合并用戶選項和默認(rèn)選項
??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?=?{};
??/**
???*?返回一個對象,每個屬性都是一個生命周期函數(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ù)
?*??實例化子應(yīng)用
?*/
function?mount(opts,?mountedInstances,?props)?{
??const?instance?=?{};
??return?Promise.resolve().then(()?=>?{
????const?appOptions?=?{?...opts.appOptions?};
????//?可以通過props.domElement屬性單獨設(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)然如果沒有id,這里會自動生成一個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?};
????//?實例化子應(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)用通過update生命周期函數(shù)可以更新子應(yīng)用的屬性
function?update(opts,?mountedInstances,?props)?{
??return?Promise.resolve().then(()?=>?{
????//?應(yīng)用實例
????const?instance?=?mountedInstances[props.name];
????//?所有的屬性
????const?data?=?{
??????...(opts.appOptions.data?||?{}),
??????...props
????};
????//?更新實例對象上的屬性值,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é)語
到這里就結(jié)束了,文章比較長,寫這篇文章也花費了好幾天的時間,但是感覺真的很好,收獲滿滿,特別是最后手寫框架部分。
也給各位同學(xué)一個建議,一定要勤動手,不動筆墨不讀書,當(dāng)你真的把框架寫出來時,那個感覺是只看源碼完全所不能比擬的,檢驗?zāi)闶欠裾娴亩蚣茉淼淖詈棉k法,就是看你能否寫一個框架出來。
愿同學(xué)們也能收獲滿滿??!
共同學(xué)習(xí),共同進步~~
github: https://github.com/liyongning/micro-frontend.git
