|
引言
前一段时间, 正好在做微前端的接入和微前端管理平台的相关事项。 而我们当前使用的微前端框架则是 qiankun, 他是这样介绍自己的:
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
所以本文基于 single-spa 源码, 来介绍 single-spa
当前使用版本 5.9.4
启动
在官方 demo 中, 要运行此框架需要做的是有这四步:
- 准备好子应用的文件, 需要抛出一些生命周期函数
- 一个子应用 app1 的加载函数(可以是 import 异步加载, 也可以是 ajax/fetch 加载)
- 注册子应用
- 启动程序
app1.js:- export function bootstrap(props) {
- //初始化时触发
- }
- export function mount(props) {
- // 应用挂载完毕之后触发
- }
- export function unmount(props) {
- // 应用卸载之后触发
- }
复制代码 main.js:- import * as singleSpa from 'single-spa'
- const name = 'app1';
- const app = () => import('./app1/app1.js'); // 一个加载函数
- const activeWhen = '/app1'; // 当路由为 app1 时, 会触发微应用的加载
- // 注册应用
- singleSpa.registerApplication({name, app, activeWhen});
- // 启动
- singleSpa.start();
复制代码 文件结构
single-spa 的文件结构为:- ├── applications
- │ ├── app-errors.js
- │ ├── app.helpers.js
- │ ├── apps.js
- │ └── timeouts.js
- ├── devtools
- │ └── devtools.js
- ├── jquery-support.js
- ├── lifecycles
- │ ├── bootstrap.js
- │ ├── lifecycle.helpers.js
- │ ├── load.js
- │ ├── mount.js
- │ ├── prop.helpers.js
- │ ├── unload.js
- │ ├── unmount.js
- │ └── update.js
- ├── navigation
- │ ├── navigation-events.js
- │ └── reroute.js
- ├── parcels
- │ └── mount-parcel.js
- ├── single-spa.js
- ├── start.js
- └── utils
- ├── assign.js
- ├── find.js
- └── runtime-environment.js
复制代码 registerApplication
我们先从注册应用开始看起- function registerApplication(
- appNameOrConfig,
- appOrLoadApp,
- activeWhen,
- customProps
- ) {
- // 数据整理, 验证传参的合理性, 最后整理得到数据源:
- // {
- // name: xxx,
- // loadApp: xxx,
- // activeWhen: xxx,
- // customProps: xxx,
- // }
- const registration = sanitizeArguments(
- appNameOrConfig,
- appOrLoadApp,
- activeWhen,
- customProps
- );
-
- // 如果有重名,则抛出错误, 所以 name 应该是要保持唯一值
- if (getAppNames().indexOf(registration.name) !== -1)
- throw Error('xxx'); // 这里省略具体错误
-
- // 往 apps 中添加数据
- // apps 是 single-spa 的一个全局变量, 用来存储当前的应用数据
- apps.push(
- assign(
- {
- // 预留值
- loadErrorTime: null,
- status: NOT_LOADED, // 默认是 NOT_LOADED , 也就是待加载的状态
- parcels: {},
- devtools: {
- overlays: {
- options: {},
- selectors: [],
- },
- },
- },
- registration
- )
- );
-
- // 判断 window 是否为空, 进入条件
- if (isInBrowser) {
- ensureJQuerySupport(); // 确保 jq 可用
- reroute();
- }
- }
复制代码 reroute
reroute 是 single-spa 的核心函数, 在注册应用时调用此函数的作用, 就是将应用的 promise 加载函数, 注入一个待加载的数组中 等后面正式启动时再调用, 类似于 ()=>import('xxx')
主要流程: 判断是否符合加载条件 -> 开始加载代码- export function reroute(pendingPromises = [], eventArguments) {
- if (appChangeUnderway) { // 一开始默认是 false
- // 如果是 true, 则返回一个 promise, 在队列中添加 resolve 参数等等
- return new Promise((resolve, reject) => {
- peopleWaitingOnAppChange.push({
- resolve,
- reject,
- eventArguments,
- });
- });
- }
-
- const {
- appsToUnload,
- appsToUnmount,
- appsToLoad,
- appsToMount,
- } = getAppChanges();
- // 遍历所有应用数组 apps , 根据 app 的状态, 来分类到这四个数组中
- // 会根据 url 和 whenActive 判断是否该 load
- // unload , unmount, to load, to mount
-
- let appsThatChanged,
- navigationIsCanceled = false,
- oldUrl = currentUrl,
- newUrl = (currentUrl = window.location.href);
-
- // 存储着一个闭包变量, 是否已经启动, 在注册步骤中, 是未启动的
- if (isStarted()) {
- // 省略, 当前是未开始的
- } else {
- // 未启动, 直接返回 loadApps, 他的定义在下方
- appsThatChanged = appsToLoad;
- return loadApps();
- }
-
- function cancelNavigation() {
- navigationIsCanceled = true;
- }
-
- // 返回一个 resolve 的 promise
- // 将需要加载的应用, map 成一个新的 promise 数组
- // 并且用 promise.all 来返回
- // 不管成功或者失败, 都会调用 callAllEventListeners 函数, 进行路由通知
- function loadApps() {
- return Promise.resolve().then(() => {
- // toLoadPromise 主要作用在甲方有讲述, 主要来定义资源的加载, 以及对应的回调
- const loadPromises = appsToLoad.map(toLoadPromise);
-
- // 通过 Promise.all 来执行, 返回的是 app.loadPromise
- // 这是资源加载
- return (
- Promise.all(loadPromises)
- .then(callAllEventListeners)
- // there are no mounted apps, before start() is called, so we always return []
- .then(() => [])
- .catch((err) => {
- callAllEventListeners();
- throw err;
- })
- );
- });
- }
- }
复制代码 toLoadPromise
注册流程中 reroute 中的主要执行函数
主要功能是赋值 loadPromise 给 app, 其中 loadPromise 函数中包括了: 执行函数、来加载应用的资源、定义加载完毕的回调函数、状态的修改、还有加载错误的一些处理- export function toLoadPromise(app) {
- return Promise.resolve().then(() => {
- // 是否重复注册 promise 加载了
- if (app.loadPromise) {
- return app.loadPromise;
- }
- // 刚注册的就是 NOT_LOADED 状态
- if (app.status !== NOT_LOADED && app.status !== LOAD_ERROR) {
- return app;
- }
-
- // 修改状态为, 加载源码
- app.status = LOADING_SOURCE_CODE;
-
- let appOpts, isUserErr;
-
- // 返回的是 app.loadPromise
- return (app.loadPromise = Promise.resolve()
- .then(() => {
- // 这里调用的了 app的 loadApp 函数(由外部传入的), 开始加载资源
- // getProps 用来判断 customProps 是否合法, 最后传值给 loadApp 函数
- const loadPromise = app.loadApp(getProps(app));
- // 判断 loadPromise 是否是一个 promise
- if (!smellsLikeAPromise(loadPromise)) {
- // 省略报错
- isUserErr = true;
- throw Error("...");
- }
- return loadPromise.then((val) => {
- // 资源加载成功
- app.loadErrorTime = null;
-
- appOpts = val;
-
- let validationErrMessage, validationErrCode;
-
- // 省略对于资源返回结果的判断
- // 比如appOpts是否是对象, appOpts.mount appOpts.bootstrap 是否是函数, 等等
- // ...
-
- // 修改状态为, 未进入引导
- // 同时将资源结果的函数赋值, 以备后面执行
- app.status = NOT_BOOTSTRAPPED;
- 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);
-
- // 执行完毕之后删除 loadPromise
- delete app.loadPromise;
-
- return app;
- });
- })
- .catch((err) => {
- // 报错也会删除 loadPromise
- 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;
- }));
- });
- }
复制代码 start
注册完应用之后, 最后是 singleSpa.start(); 的执行
start 的代码很简单:- // 一般来说 opts 是不传什么东西的
- function start(opts) {
- // 主要作用还是将标记符 started设置为 true 了
- started = true;
- if (opts && opts.urlRerouteOnly) {
- // 使用此参数可以人为地触发事件 popstate
- setUrlRerouteOnly(opts.urlRerouteOnly);
- }
- if (isInBrowser) {
- reroute();
- }
- }
复制代码 reroute
上述已经讲过注册时 reroute 的一些代码了, 这里会忽略已讲过的一些东西- function reroute(pendingPromises = [], eventArguments) {
- const {
- appsToUnload,
- appsToUnmount,
- appsToLoad,
- appsToMount,
- } = getAppChanges();
- let appsThatChanged,
- navigationIsCanceled = false,
- oldUrl = currentUrl,
- newUrl = (currentUrl = window.location.href);
-
- if (isStarted()) {
- // 这次开始执行此处
- appChangeUnderway = true;
- // 合并状态需要变更的 app
- appsThatChanged = appsToUnload.concat(
- appsToLoad,
- appsToUnmount,
- appsToMount
- );
- // 返回 performAppChanges 函数
- return performAppChanges();
- }
- }
复制代码 performAppChanges
在启动后,就会触发此函数 performAppChanges, 并返回结果
本函数的作用主要是事件的触发, 包括自定义事件和子应用中的一些事件- function performAppChanges() {
- return Promise.resolve().then(() => {
- // 触发自定义事件, 关于 CustomEvent 我们再下方详述
- // 当前事件触发 getCustomEventDetail
- // 主要是 app 的状态, url 的变更, 参数等等
- window.dispatchEvent(
- new CustomEvent(
- appsThatChanged.length === 0
- ? "single-spa:before-no-app-change"
- : "single-spa:before-app-change",
- getCustomEventDetail(true)
- )
- );
-
- // 省略类似事件
-
- // 除非在上一个事件中调用了 cancelNavigation, 才会进入这一步
- if (navigationIsCanceled) {
- window.dispatchEvent(
- new CustomEvent(
- "single-spa:before-mount-routing-event",
- getCustomEventDetail(true)
- )
- );
- // 将 peopleWaitingOnAppChange 的数据重新执行 reroute 函数 reroute(peopleWaitingOnAppChange)
- finishUpAndReturn();
- // 更新 url
- navigateToUrl(oldUrl);
- return;
- }
-
- // 准备卸载的 app
- const unloadPromises = appsToUnload.map(toUnloadPromise);
-
- // 执行子应用中的 unmount 函数, 如果超时也会有报警
- const unmountUnloadPromises = appsToUnmount
- .map(toUnmountPromise)
- .map((unmountPromise) => unmountPromise.then(toUnloadPromise));
-
- const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);
-
- const unmountAllPromise = Promise.all(allUnmountPromises);
-
- // 所有应用的卸载事件
- unmountAllPromise.then(() => {
- window.dispatchEvent(
- new CustomEvent(
- "single-spa:before-mount-routing-event",
- getCustomEventDetail(true)
- )
- );
- });
-
- // 执行 bootstrap 生命周期, tryToBootstrapAndMount 确保先执行 bootstrap
- const loadThenMountPromises = appsToLoad.map((app) => {
- return toLoadPromise(app).then((app) =>
- tryToBootstrapAndMount(app, unmountAllPromise)
- );
- });
-
- // 执行 mount 事件
- const mountPromises = appsToMount
- .filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
- .map((appToMount) => {
- return tryToBootstrapAndMount(appToMount, unmountAllPromise);
- });
- // 其他的部分不太重要, 可省略
- });
- }
复制代码 CustomEvent
CustomEvent 是一个原生 API, 这里稍微介绍下
在某些场景中, 我们会经常做出一些模拟点击的行为, 比如这样:- <button id="submit" onclick="alert('Click!');"><button id="submit" onclick="alert('Click!');">btn</button></button>
复制代码 通过 CustomEvent 也能实现这种事件:- <button id="submit" onclick="alert('Click!');"><button id="submit" onclick="alert('Click!');">btn</button></button>
复制代码 不仅是浏览器原生的事件,如'click','mousedown','change','mouseover','mouseenter'等可以触发,任意的自定义名称的事件也是可以触发的- document.body.addEventListener('测试自定义事件', (ev) => {
- console.log(ev.detail)
- })
- document.body.dispatchEvent(new CustomEvent('测试自定义事件', {
- detail: {
- foo: 1
- }
- }))
复制代码 整体流程
- 在正式环境使用 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
|