翼度科技»论坛 编程开发 JavaScript 查看内容

Single-spa 源码浅析

10

主题

10

帖子

30

积分

新手上路

Rank: 1

积分
30
引言

前一段时间, 正好在做微前端的接入和微前端管理平台的相关事项。 而我们当前使用的微前端框架则是 qiankun, 他是这样介绍自己的:
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
所以本文基于 single-spa 源码, 来介绍 single-spa
当前使用版本 5.9.4
启动

在官方 demo 中, 要运行此框架需要做的是有这四步:

  • 准备好子应用的文件, 需要抛出一些生命周期函数
  • 一个子应用 app1 的加载函数(可以是 import 异步加载, 也可以是 ajax/fetch 加载)
  • 注册子应用
  • 启动程序
app1.js:
  1. export function bootstrap(props) {
  2.     //初始化时触发
  3. }
  4. export function mount(props) {
  5.     // 应用挂载完毕之后触发
  6. }
  7. export function unmount(props) {
  8.     // 应用卸载之后触发
  9. }
复制代码
main.js:
  1. import * as singleSpa from 'single-spa'
  2. const name = 'app1';
  3. const app = () => import('./app1/app1.js'); // 一个加载函数
  4. const activeWhen = '/app1'; // 当路由为 app1 时, 会触发微应用的加载
  5. // 注册应用
  6. singleSpa.registerApplication({name, app, activeWhen});
  7. // 启动
  8. singleSpa.start();
复制代码
文件结构

single-spa 的文件结构为:
  1. ├── applications
  2. │   ├── app-errors.js
  3. │   ├── app.helpers.js
  4. │   ├── apps.js
  5. │   └── timeouts.js
  6. ├── devtools
  7. │   └── devtools.js
  8. ├── jquery-support.js
  9. ├── lifecycles
  10. │   ├── bootstrap.js
  11. │   ├── lifecycle.helpers.js
  12. │   ├── load.js
  13. │   ├── mount.js
  14. │   ├── prop.helpers.js
  15. │   ├── unload.js
  16. │   ├── unmount.js
  17. │   └── update.js
  18. ├── navigation
  19. │   ├── navigation-events.js
  20. │   └── reroute.js
  21. ├── parcels
  22. │   └── mount-parcel.js
  23. ├── single-spa.js
  24. ├── start.js
  25. └── utils
  26.     ├── assign.js
  27.     ├── find.js
  28.     └── runtime-environment.js
复制代码
registerApplication

我们先从注册应用开始看起
  1. function registerApplication(
  2.     appNameOrConfig,
  3.     appOrLoadApp,
  4.     activeWhen,
  5.     customProps
  6. ) {
  7.     // 数据整理, 验证传参的合理性, 最后整理得到数据源:
  8.     // {
  9.     //      name: xxx,
  10.     //      loadApp: xxx,
  11.     //      activeWhen: xxx,
  12.     //      customProps: xxx,
  13.     // }
  14.     const registration = sanitizeArguments(
  15.         appNameOrConfig,
  16.         appOrLoadApp,
  17.         activeWhen,
  18.         customProps
  19.     );
  20.    
  21.     // 如果有重名,则抛出错误, 所以 name 应该是要保持唯一值
  22.     if (getAppNames().indexOf(registration.name) !== -1)
  23.         throw Error('xxx'); // 这里省略具体错误
  24.    
  25.     // 往 apps 中添加数据
  26.     // apps 是 single-spa 的一个全局变量, 用来存储当前的应用数据
  27.     apps.push(
  28.         assign(
  29.             {
  30.                 // 预留值
  31.                 loadErrorTime: null,
  32.                 status: NOT_LOADED, // 默认是 NOT_LOADED , 也就是待加载的状态
  33.                 parcels: {},
  34.                 devtools: {
  35.                     overlays: {
  36.                         options: {},
  37.                         selectors: [],
  38.                     },
  39.                 },
  40.             },
  41.             registration
  42.         )
  43.     );
  44.    
  45.     // 判断 window 是否为空, 进入条件
  46.     if (isInBrowser) {
  47.         ensureJQuerySupport(); // 确保 jq 可用
  48.         reroute();
  49.     }
  50. }
复制代码
reroute

reroute 是 single-spa 的核心函数, 在注册应用时调用此函数的作用, 就是将应用的 promise 加载函数, 注入一个待加载的数组中 等后面正式启动时再调用, 类似于 ()=>import('xxx')
主要流程: 判断是否符合加载条件 -> 开始加载代码
  1. export function reroute(pendingPromises = [], eventArguments) {
  2.     if (appChangeUnderway) { //  一开始默认是 false
  3.         // 如果是 true, 则返回一个 promise, 在队列中添加 resolve 参数等等
  4.         return new Promise((resolve, reject) => {
  5.             peopleWaitingOnAppChange.push({
  6.                 resolve,
  7.                 reject,
  8.                 eventArguments,
  9.             });
  10.         });
  11.     }
  12.    
  13.     const {
  14.         appsToUnload,
  15.         appsToUnmount,
  16.         appsToLoad,
  17.         appsToMount,
  18.     } = getAppChanges();
  19.     // 遍历所有应用数组 apps , 根据 app 的状态, 来分类到这四个数组中
  20.     // 会根据 url 和 whenActive 判断是否该 load
  21.     // unload , unmount, to load, to mount
  22.    
  23.     let appsThatChanged,
  24.         navigationIsCanceled = false,
  25.         oldUrl = currentUrl,
  26.         newUrl = (currentUrl = window.location.href);
  27.    
  28.     // 存储着一个闭包变量, 是否已经启动, 在注册步骤中, 是未启动的
  29.     if (isStarted()) {
  30.         // 省略, 当前是未开始的
  31.     } else {
  32.         // 未启动, 直接返回 loadApps, 他的定义在下方
  33.         appsThatChanged = appsToLoad;
  34.         return loadApps();
  35.     }
  36.    
  37.     function cancelNavigation() {
  38.         navigationIsCanceled = true;
  39.     }
  40.    
  41.     // 返回一个 resolve 的 promise
  42.     // 将需要加载的应用,  map 成一个新的 promise 数组
  43.     // 并且用 promise.all 来返回
  44.     // 不管成功或者失败, 都会调用 callAllEventListeners 函数, 进行路由通知
  45.     function loadApps() {
  46.         return Promise.resolve().then(() => {
  47.             // toLoadPromise 主要作用在甲方有讲述, 主要来定义资源的加载, 以及对应的回调
  48.             const loadPromises = appsToLoad.map(toLoadPromise);
  49.             
  50.             // 通过 Promise.all 来执行, 返回的是 app.loadPromise
  51.             // 这是资源加载
  52.             return (
  53.                 Promise.all(loadPromises)
  54.                 .then(callAllEventListeners)
  55.                 // there are no mounted apps, before start() is called, so we always return []
  56.                 .then(() => [])
  57.                 .catch((err) => {
  58.                     callAllEventListeners();
  59.                     throw err;
  60.                 })
  61.             );
  62.         });
  63.     }
  64. }
复制代码
toLoadPromise

注册流程中 reroute 中的主要执行函数
主要功能是赋值 loadPromise 给 app, 其中 loadPromise 函数中包括了: 执行函数、来加载应用的资源、定义加载完毕的回调函数、状态的修改、还有加载错误的一些处理
  1. export function toLoadPromise(app) {
  2.     return Promise.resolve().then(() => {
  3.         // 是否重复注册 promise 加载了
  4.         if (app.loadPromise) {
  5.             return app.loadPromise;
  6.         }
  7.         // 刚注册的就是 NOT_LOADED 状态
  8.         if (app.status !== NOT_LOADED && app.status !== LOAD_ERROR) {
  9.             return app;
  10.         }
  11.         
  12.         // 修改状态为, 加载源码
  13.         app.status = LOADING_SOURCE_CODE;
  14.         
  15.         let appOpts, isUserErr;
  16.         
  17.         // 返回的是 app.loadPromise
  18.         return (app.loadPromise = Promise.resolve()
  19.         .then(() => {
  20.             // 这里调用的了 app的 loadApp 函数(由外部传入的), 开始加载资源
  21.             // getProps 用来判断 customProps 是否合法, 最后传值给 loadApp 函数
  22.             const loadPromise = app.loadApp(getProps(app));
  23.             // 判断 loadPromise 是否是一个 promise
  24.             if (!smellsLikeAPromise(loadPromise)) {
  25.                 // 省略报错
  26.                 isUserErr = true;
  27.                 throw Error("...");
  28.             }
  29.             return loadPromise.then((val) => {
  30.                 // 资源加载成功
  31.                 app.loadErrorTime = null;
  32.                
  33.                 appOpts = val;
  34.                
  35.                 let validationErrMessage, validationErrCode;
  36.                
  37.                 // 省略对于资源返回结果的判断
  38.                 // 比如appOpts是否是对象, appOpts.mount appOpts.bootstrap 是否是函数, 等等
  39.                 // ...
  40.                
  41.                 // 修改状态为, 未进入引导
  42.                 // 同时将资源结果的函数赋值, 以备后面执行
  43.                 app.status = NOT_BOOTSTRAPPED;
  44.                 app.bootstrap = flattenFnArray(appOpts, "bootstrap");
  45.                 app.mount = flattenFnArray(appOpts, "mount");
  46.                 app.unmount = flattenFnArray(appOpts, "unmount");
  47.                 app.unload = flattenFnArray(appOpts, "unload");
  48.                 app.timeouts = ensureValidAppTimeouts(appOpts.timeouts);
  49.                
  50.                 // 执行完毕之后删除 loadPromise
  51.                 delete app.loadPromise;
  52.                
  53.                 return app;
  54.             });
  55.         })
  56.         .catch((err) => {
  57.             // 报错也会删除 loadPromise
  58.             delete app.loadPromise;
  59.             // 修改状态为 用户的传参报错, 或者是加载出错
  60.             let newStatus;
  61.             if (isUserErr) {
  62.                 newStatus = SKIP_BECAUSE_BROKEN;
  63.             } else {
  64.                 newStatus = LOAD_ERROR;
  65.                 app.loadErrorTime = new Date().getTime();
  66.             }
  67.             handleAppError(err, app, newStatus);
  68.             
  69.             return app;
  70.         }));
  71.     });
  72. }
复制代码
start

注册完应用之后, 最后是 singleSpa.start(); 的执行
start 的代码很简单:
  1. // 一般来说 opts 是不传什么东西的
  2. function start(opts) {
  3.     // 主要作用还是将标记符 started设置为 true 了
  4.     started = true;
  5.     if (opts && opts.urlRerouteOnly) {
  6.         // 使用此参数可以人为地触发事件 popstate
  7.         setUrlRerouteOnly(opts.urlRerouteOnly);
  8.     }
  9.     if (isInBrowser) {
  10.         reroute();
  11.     }
  12. }
复制代码
reroute

上述已经讲过注册时 reroute 的一些代码了, 这里会忽略已讲过的一些东西
  1. function reroute(pendingPromises = [], eventArguments) {
  2.     const {
  3.         appsToUnload,
  4.         appsToUnmount,
  5.         appsToLoad,
  6.         appsToMount,
  7.     } = getAppChanges();
  8.     let appsThatChanged,
  9.         navigationIsCanceled = false,
  10.         oldUrl = currentUrl,
  11.         newUrl = (currentUrl = window.location.href);
  12.    
  13.     if (isStarted()) {
  14.         // 这次开始执行此处
  15.         appChangeUnderway = true;
  16.         // 合并状态需要变更的 app
  17.         appsThatChanged = appsToUnload.concat(
  18.             appsToLoad,
  19.             appsToUnmount,
  20.             appsToMount
  21.         );
  22.         // 返回 performAppChanges 函数
  23.         return performAppChanges();
  24.     }
  25. }
复制代码
performAppChanges

在启动后,就会触发此函数 performAppChanges, 并返回结果
本函数的作用主要是事件的触发, 包括自定义事件和子应用中的一些事件
  1.   function performAppChanges() {
  2.     return Promise.resolve().then(() => {
  3.         // 触发自定义事件, 关于 CustomEvent 我们再下方详述
  4.         // 当前事件触发 getCustomEventDetail
  5.         // 主要是 app 的状态, url 的变更, 参数等等
  6.         window.dispatchEvent(
  7.             new CustomEvent(
  8.                 appsThatChanged.length === 0
  9.                     ? "single-spa:before-no-app-change"
  10.                     : "single-spa:before-app-change",
  11.                 getCustomEventDetail(true)
  12.             )
  13.         );
  14.         
  15.         // 省略类似事件
  16.         
  17.         // 除非在上一个事件中调用了 cancelNavigation, 才会进入这一步
  18.         if (navigationIsCanceled) {
  19.             window.dispatchEvent(
  20.                 new CustomEvent(
  21.                     "single-spa:before-mount-routing-event",
  22.                     getCustomEventDetail(true)
  23.                 )
  24.             );
  25.             // 将 peopleWaitingOnAppChange 的数据重新执行 reroute 函数 reroute(peopleWaitingOnAppChange)  
  26.             finishUpAndReturn();
  27.             // 更新 url
  28.             navigateToUrl(oldUrl);
  29.             return;
  30.         }
  31.         
  32.         // 准备卸载的 app
  33.         const unloadPromises = appsToUnload.map(toUnloadPromise);
  34.         
  35.         // 执行子应用中的 unmount 函数, 如果超时也会有报警
  36.         const unmountUnloadPromises = appsToUnmount
  37.         .map(toUnmountPromise)
  38.         .map((unmountPromise) => unmountPromise.then(toUnloadPromise));
  39.         
  40.         const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);
  41.         
  42.         const unmountAllPromise = Promise.all(allUnmountPromises);
  43.         
  44.         // 所有应用的卸载事件
  45.         unmountAllPromise.then(() => {
  46.             window.dispatchEvent(
  47.                 new CustomEvent(
  48.                     "single-spa:before-mount-routing-event",
  49.                     getCustomEventDetail(true)
  50.                 )
  51.             );
  52.         });
  53.         
  54.         // 执行 bootstrap 生命周期, tryToBootstrapAndMount 确保先执行 bootstrap
  55.         const loadThenMountPromises = appsToLoad.map((app) => {
  56.             return toLoadPromise(app).then((app) =>
  57.                 tryToBootstrapAndMount(app, unmountAllPromise)
  58.             );
  59.         });
  60.         
  61.         // 执行 mount 事件
  62.         const mountPromises = appsToMount
  63.         .filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
  64.         .map((appToMount) => {
  65.             return tryToBootstrapAndMount(appToMount, unmountAllPromise);
  66.         });
  67.         // 其他的部分不太重要, 可省略
  68.     });
  69. }
复制代码
CustomEvent

CustomEvent 是一个原生 API, 这里稍微介绍下
在某些场景中, 我们会经常做出一些模拟点击的行为, 比如这样:
  1. <button id="submit" onclick="alert('Click!');"><button id="submit" onclick="alert('Click!');">btn</button></button>
复制代码
通过 CustomEvent 也能实现这种事件:
  1. <button id="submit" onclick="alert('Click!');"><button id="submit" onclick="alert('Click!');">btn</button></button>
复制代码
不仅是浏览器原生的事件,如'click','mousedown','change','mouseover','mouseenter'等可以触发,任意的自定义名称的事件也是可以触发的
  1. document.body.addEventListener('测试自定义事件', (ev) => {
  2.     console.log(ev.detail)
  3. })
  4. document.body.dispatchEvent(new CustomEvent('测试自定义事件', {
  5.     detail: {
  6.         foo: 1
  7.     }
  8. }))
复制代码
整体流程


  • 在正式环境使用 registerApplication 来注册应用
  • 这时候在 single-spa 内部会将注册的信息, 初始化加载函数
  • 使用 url 进行匹配, 是否要加载, 如果需要加载, 则归类
  • 如果匹配上, 开始加载应用的文件 (即使还没使用 start)
  • 最后使用 start, 开始发送各类事件, 调用应用的各类生命周期方法
这里用一个简单的图来说明下:

总结

single-spa 无疑是微前端的一个重要里程碑,在大型应用场景下, 可支持多类框架, 抹平了框架间的巨大交互成本
他的核心是对子应用进行管理,但还有很多工程化问题没做。比如JavaScript全局对象覆盖、css加载卸载、公共模块管理要求只下载一次等等性能问题
这又促成了其他的框架的诞生, 比较出名的就是 qiankun、Isomorphic Layout Composer。
而这些就是另一个话题了。
引用


来源:https://www.cnblogs.com/Grewer/p/17259732.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具