<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          微前端框架 之 single-spa 從入門到精通

          共 51625字,需瀏覽 104分鐘

           ·

          2020-08-24 10:25


          ??

          來源:李永寧

          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(()?=>?{})
          }


          更改視圖文件








          環(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_ENVdevelopment,而不是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)registerApplicationstart兩個方法并導(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

          》》面試官都在用的題庫,快來看看《《

          瀏覽 79
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产在线观看国产精品产拍 | 亚洲网站在线看 | www.操逼网 | 成人无码www在线看免费 | 豆花视频在线观看国产豆花 |