微前端single-spa原理学习
前⾔
看这篇⽂章之前先要了解微前端概念,single-spa如何使⽤。
这篇⽂章主要分析single-spa原理。然后分析完之后,作者说说⾃⼰对于同时都是微前端框架qiankun和single-spa的关系的⼀些理解,因为在我学习刚开始微前端的时候,我其实不太明⽩都是微前端框架qiankun和single-spa有什么区别,它们是什么关系,可能⼀些读者也会有这样的疑问,同时这篇⽂章作为铺垫,后⾯会发出qiankun的原理学习。
我们关注⼏个问题:
1. 我们怎么通过single-spa去读取⼦应⽤的js?
2. single-spa是怎么访问⼦应⽤的⽣命周期函数,同时对于⽣命周期的调度时机是怎么样的?
3. 主应⽤是怎么传⼊props进⼊⼦应⽤的⽣命周期函数中?
4. 主应⽤是怎么控制路由?
5. 如果有了解过qiankun,那么qiankun和single-spa是什么关系?
本⽂主要从两个函数为切⼊,就是registerApplication和start函数
registerApplication函数
我们⼀般会这样去写registerApplication函数
name: 'singleDemo',
app: async () => {
...
return ...;
},
bootstrappedactiveWhen: () => location.pathname.startsWith('xxx') // 配置微前端模块
});
它的源码如下:
export function registerApplication(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
) {
//sanitizeArguments作⽤就是规范化参数和参数格式校验
const registration = sanitizeArguments(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
);
//这⾥就是校验你注册的应⽤是不是有重复名字的,有的话就抛出异常
if (getAppNames().indexOf(registration.name) !== -1)
throw Error(
formatErrorMessage(
21,
__DEV__ &&
`There is already an app registered with name ${registration.name}`,
registration.name
)
);
//将应⽤信息推⼊apps数组中,assign就是⽤于两个对象的合并
apps.push(
assign(
{
loadErrorTime: null,
status: NOT_LOADED,
parcels: {},
devtools: {
overlays: {
options: {},
selectors: [],
},
},
},
registration
)
);
if (isInBrowser) {
ensureJQuerySupport();
/
/这个reroute做了很多的事情。执⾏了⽤户⾃定义加载函数。存放了⽣命周期等等
reroute();
}
}
第⼀个执⾏的函数是sanitizeArguments,它的作⽤就是把我们传⼊registerApplication函数的参数就是规范化,有写错误的就抛出异常,下⾯是它的源码
function sanitizeArguments(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
) {
const usingObjectAPI = typeof appNameOrConfig === "object";
const registration = {
name: null,
activeWhen: null,
customProps: null,
};
if (usingObjectAPI) {
validateRegisterWithConfig(appNameOrConfig);
registration.name = appNameOrConfig.name;
registration.loadApp = appNameOrConfig.app;
registration.activeWhen = appNameOrConfig.activeWhen;
registration.customProps = appNameOrConfig.customProps;
} else {
//这句话的作⽤就是检查⽤户所传⼊的参数是不是符合规范的,不符合规范的话就抛出异常
validateRegisterWithArguments(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
);
registration.name = appNameOrConfig;
registration.loadApp = appOrLoadApp;
registration.activeWhen = activeWhen;
registration.customProps = customProps;
}
//如果第⼆个参数不是函数的话就转⼊promise,是函数的话就原封不动返回
registration.loadApp = sanitizeLoadApp(registration.loadApp);
//看看CustomProps有没有写内容,没有的话就默认返回对象,有的话就原封返回
registration.customProps = sanitizeCustomProps(registration.customProps);
//如果你的activeWhen写成函数的就原封返回,如果是⼀个字符串就帮你处理为函数再返回
registration.activeWhen = sanitizeActiveWhen(registration.activeWhen);
return registration;
}
阅读它的源码我们发现,⾥⾯就是⼀些规范化操作的细节,同时规划化完成之后会在registration对应的属性上进⾏赋值。最后把这个registration返回出去。
所以这段代码执⾏的作⽤就是:
1. 规范化属性,有错误的就抛出异常。
2. 最后把我们的传⼊的参数整理完了之后的属性值重新赋值给registration,并且返回出去。
规范化操作结束之后,接着继续执⾏registerApplication函数
export function registerApplication(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
) {
...
if (getAppNames().indexOf(registration.name) !== -1)
throw Error(
formatErrorMessage(
21,
__DEV__ &&
`There is already an app registered with name ${registration.name}`,
registration.name
)
);
apps.push(
assign(
{
loadErrorTime: null,
status: NOT_LOADED,
parcels: {},
devtools: {
overlays: {
options: {},
selectors: [],
},
},
},
registration
)
);
...
}
接下来的贴出的这段⽐较好好理解。从错误信息也能够看出,第⼀个if语句⼤概就是检查你注册的⼦应⽤信息是否有重名的现象。有的话就抛出异常,没有的话就把这个registration和⼀个对象进⾏合并,推⼊⼀个apps的数组⾥⾯,对⼦应⽤的信息进⾏缓存。
注意看到对象中有⼀个status的属性。这个属性后⾯会被多次提到,同时还有⼀个 registration.loadApp属性,因为这⾥存放的是我们注册选项的加载函数,决定了我们⽤什么样的⽅式去加载⼦应⽤的代码。
接下来进⼊最重要的环节,继续看回registerApplication代码,最后执⾏了⼀个叫做reroute的⽅法,
export function registerApplication(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
) {
...if (isInBrowser) {
ensureJQuerySupport();
reroute();
}
}
下⾯看看reroute做了什么事情,这个reroute⽅法同时也会在start中进⾏调⽤。
reroute
reroute在整个single-spa的职能是什么,就是负责改变app.status。和执⾏在⼦应⽤中注册的⽣命周期函数。
export function reroute(pendingPromises = [], eventArguments) {
//appChangeUnderway定义在了本⽂件的开头,默认是置为false。所以在registerApplication⽅法使⽤的时候,是不会出发的if的逻辑
//在start之后就会被置为true。意味着在start重新调⽤reroute的时候就会进⼊这段if逻辑
if (appChangeUnderway) {
return new Promise((resolve, reject) => {
peopleWaitingOnAppChange.push({
reject,
eventArguments,
});
});
}
const {
appsToUnload,
appsToUnmount,
appsToLoad,
appsToMount,
} = getAppChanges();
let appsThatChanged,
navigationIsCanceled = false,
//oldUrl在⽂件开头获取
oldUrl = currentUrl,
//新的url在本⽂件中获取
newUrl = (currentUrl = window.location.href);
//isStarted判断是否执⾏start⽅法,start⽅法开头把started置为true,就会⾛⼊这个分⽀
if (isStarted()) {
appChangeUnderway = true;
appsThatChanged = at(
appsToLoad,
appsToUnmount,
appsToMount
);
return performAppChanges();
} else {
appsThatChanged = appsToLoad;
return loadApps();
}
function cancelNavigation() {
navigationIsCanceled = true;
}
function loadApps() {
.
..
}
function performAppChanges() {
...
}
function finishUpAndReturn() {
...
}
}
开头的if语句我已经给出了注释,我们接着来看看getAppChanges⽅法。上⾯通过调⽤getAppChanges解构出了⼏个变量,函数的源码如下:
export function getAppChanges() {
//将应⽤分为4类
//需要被移除的
const appsToUnload = [],
//需要被卸载的
appsToUnmount = [],
//需要被加载的
appsToLoad = [],
//需要被挂载的
appsToMount = [];
// We re-attempt to download applications in LOAD_ERROR after a timeout of 200 milliseconds
const currentTime = new Date().getTime();
//apps是我们在registerApplication⽅法中注册的⼦应⽤的信息的json对象会被缓存在apps数组中,apps装有我们⼦应⽤的配置信息
apps.forEach((app) => {
//shouldBeActive这⾥就是真正执⾏activeWhen中定义的函数如果根据当前的location.href匹配路径成功的话,就说明此时
//应该激活这个应⽤
const appShouldBeActive =
app.status !== SKIP_BECAUSE_BROKEN && shouldBeActive(app);
//我们在执⾏registerApplication前⾯的时候把app.status设置为了NOT_LOADED,看看下⾯的swtich,如果在上⾯的匹配成功的话就把app推⼊appsLoad数组中,表明这个⼦应⽤即将被加载。
switch (app.status) {
case LOAD_ERROR:
if (appShouldBeActive && currentTime - app.loadErrorTime >= 200) {
appsToLoad.push(app);
}
break;
//最开始注册完之后的app状态就是NOT_LOADED
case NOT_LOADED:
case LOADING_SOURCE_CODE:
//如果app需要激活的话就推⼊数组
if (appShouldBeActive) {
appsToLoad.push(app);
}
break;
case NOT_BOOTSTRAPPED:
case NOT_MOUNTED:
if (!appShouldBeActive && getAppUnloadInfo(toName(app))) {
appsToUnload.push(app);
} else if (appShouldBeActive) {
appsToMount.push(app);
}
break;
case MOUNTED:
if (!appShouldBeActive) {
appsToUnmount.push(app);
}
break;
// all other statuses are ignored
}
});
return { appsToUnload, appsToUnmount, appsToLoad, appsToMount };
}
getAppChanges在遍历我们apps数组的时候,留意到这句话
const appShouldBeActive =
app.status !== SKIP_BECAUSE_BROKEN && shouldBeActive(app);
这句话的作⽤就是根据我们当前的url进⾏判断需要激活哪⼀个⼦应⽤,这⾥涉及到了我们的activeWhen参数选项,我们先回顾下这个选项有什么作⽤,下⾯是从官⽅⽂档的截图,可以看到这个参数作⽤就是⽤来激活应⽤的
我们看看shouldBeActive的源码
//函数返回true or false表明你当前的url是否匹配到了⼦应⽤
export function shouldBeActive(app) {
try {
//可以看到这⾥就调⽤了activeWhen选项。并且传⼊window.location作为参数
return app.activeWhen(window.location);
} catch (err) {
handleAppError(err, app, SKIP_BECAUSE_BROKEN);
return false;
}
}
//在⽹上的⼀些例⼦中可能会这么写这个参数选项,那结合上⾯的意思就是说匹配路径开头为/vue的,现在你应该明⽩这个activeWhen到底有什么作⽤。activeWhen: () => location.pathname.startsWith('/vue')
看完了onAppChange函数接下来回头看reroute函数
export function reroute(pendingPromises = [], eventArguments) {
...
let appsThatChanged,
navigationIsCanceled = false,
//oldUrl在⽂件开头获取
oldUrl = currentUrl,
//新的url在本函数中获取
newUrl = (currentUrl = window.location.href);
//isStarted判断是否执⾏start⽅法,start⽅法开头把started置为true,就会⾛⼊这个分⽀
if (isStarted()) {
appChangeUnderway = true;
appsThatChanged = at(
appsToLoad,
appsToUnmount,
appsToMount
);
return performAppChanges();
} else {
//registerApplication会⾛⼊这个分⽀
appsThatChanged = appsToLoad;
return loadApps();
}
function cancelNavigation() {
navigationIsCanceled = true;
}
function loadApps() {
...
}
function performAppChanges() {
...
}
function finishUpAndReturn() {
...
}
}
接下来看看loadApps函数,它的源码如下:
function loadApps() {
//这⾥注册了⼀个微任务,注意是微任务说明并不会马上执⾏then之后的逻辑
solve().then(() => {
//appsToLoad是通过activeWhen规则分析当前⽤户所在url,得到需要加载的⼦应⽤的数组。下⾯就开始通过map对需要激活的⼦应⽤进⾏遍历
//toLoadPromise的作⽤⽐较重要,是我执⾏我们调⽤registerApplication⽅法参数中的加载函数选项执⾏的地⽅,toLoadPromise源代码在下⾯
const loadPromises = appsToLoad.map(toLoadPromise);
return (
Promise.all(loadPromises)
.then(callAllEventListeners)
// there are no mounted apps, before start() is called, so we always return []
//在start之前⽣命周期函数都已经准备就绪,但是不会被触发。直到start才会开始触发,从上⾯的注释就可以知道
.then(() => [])
.
catch((err) => {
callAllEventListeners();
throw err;
})
);
});
}
export function toLoadPromise(app) {
//注意这⾥也是注册⼀个微任务,也不是同步执⾏的
//app就是⼦应⽤对应的配置json对象
solve().then(() => {
/
/在registerApplication返回的对json对象是没有loadPromise属性的
if (app.loadPromise) {
return app.loadPromise;
}
//在registerApplication的时候app.status === NOT_LOADED状态,不会进⼊if语句
if (app.status !== NOT_LOADED && app.status !== LOAD_ERROR) {
return app;
}
//这⾥改变了app的状态
app.status = LOADING_SOURCE_CODE;
let appOpts, isUserErr;
//这⾥注册了⼀个微任务,并把返回结果赋值给了app.loadPromise
return (app.loadPromise = solve()
.then(() => {
//这⾥开始执⾏loadApp,可以回头看看loadApp是什么东西,loadApp我们传⼊registerApplication的加载函数!!
//这⾥就是真正执⾏我们的加载函数。我们的加载函数可能是这么写的(如下),说明这⾥就是把我们为应⽤的script标签注⼊到html上
// app: async () => {
// await runScript('127.0.0.1:8081/static/js/chunk-vendors.js');
// await runScript('127.0.0.1:8081/static/js/app.js');
const loadPromise = app.loadApp(getProps(app));
//这个校验传⼊register的第⼆个参数返回的是不是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)
)
);
}
//这个return⼗分重要,⾸先我们要知道上⾯执⾏app.loadApp(getProps(app))会是什么?
//看下⾯的分析
return loadPromise.then((val) => {
...省略若⼲
});
})
.catch((err) => {
...省略若⼲
}));
});
}
⽤户⾃定义加载函数的执⾏
在上⾯代码的toLoadPromise中,有这么⼀句话app.loadApp(getProps(app))。从分析中我们知道,app.loadApp执⾏的函数的就是⽤户调⽤registerApplication传⼊的加载函数。就是下⾯图的东西
我们要明⽩在加载函数中需要我们写上我们对于⼦应⽤代码的加载过程,上⾯的例⼦是⼀种简单的写法,我们还可能会这么写(如下),不管怎么写最终⽬的都是⼀样的。
const runScript = async (url) => {
return new Promise((resolve, reject) => {
const script = ateElement('script');
script.src = url;
const firstScript = ElementsByTagName('script')[0];
firstScript.parentNode.insertBefore(script, firstScript);
});
};
name: 'singleDemo',
app: async () => {
await runScript('127.0.0.1:8081/static/js/chunk-vendors.js');
await runScript('127.0.0.1:8081/static/js/app.js');
console.log(window)
return window['singleDemo'];
},
activeWhen: () => location.pathname.startsWith('/vue') // 配置微前端模块前
});
最终⽬的是什么:
1.需要对于⼦应⽤的代码进⾏加载,加载的写法不限。你可以通过插⼊<script>标签引⽤你的⼦应⽤代码,或者像qiankun⼀样通过window.fetch去请求⼦应⽤的⽂件资源。
从这⾥加载函数的⾃定义我们可以看出为什么single-spa这个可以⽀持不同的前端框架例如vue,react接⼊,原因在于我们的前端框架最终打包都会变成app.js, vendor-chunk.js 等js⽂件,变回原⽣的操作。我们从微前端的主应⽤去引⼊这些
js⽂件去渲染出我们的⼦应⽤,本质上最终都是转为原⽣dom操作,所以说⽆论你的⼦应⽤⽤框架东西写的,其实都⼀样。所以加载函数就是single-spa对应⼦应⽤资源引⼊的⼊⼝地⽅。
2. 第⼆个⽬的就是需要在加载函数中我们要返回出⼦应⽤中导出的⽣命周期函数提供给主应⽤,那么从哪⾥看出需要返回⼦应⽤的⽣命周期函数。我们回过头来看LoadPromise 的加载代码(如下)。
看看appOpts下⾯的if函数,可以看到传⼊的参数有bootstrap, mount, unmount, unload等等的⽣命周期关键词。读者可以仔细阅读⼀下它的校验函数你⼤概就能够知道,他在校验appOpts即val⾥是否有这些⽣命周期函数。说明single-spa要求我们
在加载函数中需要return出⼦应⽤的⽣命周期函数。
export function toLoadPromise(app) {
solve().then(() => {
...省略
return (app.loadPromise = solve()
.then(() => {
...省略
return loadPromise.then((val) => {
app.loadErrorTime = null;
//val就是装有⼦应⽤的⽣命周期函数,appOpts其中装有的就是⼦应⽤获取的到的⽣命周期函数
appOpts = val;
let validationErrMessage, validationErrCode;
if (typeof appOpts !== "object") {
validationErrCode = 34;
if (__DEV__) {
validationErrMessage = `does not export anything`;
}
}
if (
// ES Modules don't have the Object prototype
//这个if语句就是开始检验你有没有bootstrap这个⽣命周期函数
Object.prototype.hasOwnProperty.call(appOpts, "bootstrap") &&
//这个校验看看你的初始化属性是不是函数或者是⼀个函数数组,从这⾥可以看出⽣命周期函数可以写成数组的形式
!validLifecycleFn(appOpts.bootstrap)
) {
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论