共计 8139 个字符,预计需要花费 21 分钟才能阅读完成。
引言
前一段时间, 正好在做微前端的接入和微前端治理平台的相干事项。而咱们以后应用的微前端框架则是 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!');">btn</button>
<script>
const btn = document.getElementById('submit');
btn.click()
</script>
通过 CustomEvent
也能实现这种事件:
<button id="submit" onclick="alert('Click!');">btn</button>
<script>
const btn = document.getElementById('submit');
btn.dispatchEvent(new CustomEvent('click'))
// 应用 btn.dispatchEvent(new Event('click')) 也是一样的
// 区别在于 CustomEvent 能够传递自定义参数
</script>
不仅是浏览器原生的事件,如 ’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://zh-hans.single-spa.js.org/docs/getting-started-overview
- https://zhuanlan.zhihu.com/p/344145423
- https://www.zhangxinxu.com/wordpress/2020/08/js-customevent-p…
- https://juejin.cn/post/7054454791803502628