You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
/** * 验证应用配置对象的各个属性是否存在不合法的情况,存在则抛出错误 * @param {*} config = { name: 'app1', app: function, activeWhen: function, customProps: {} } */exportfunctionvalidateRegisterWithConfig(config){// 异常判断,应用的配置对象不能是数组或者nullif(Array.isArray(config)||config===null)throwError(formatErrorMessage(39,__DEV__&&"Configuration object can't be an Array or null!"));// 配置对象只能包括这四个keyconstvalidKeys=["name","app","activeWhen","customProps"];// 找到配置对象存在的无效的keyconstinvalidKeys=Object.keys(config).reduce((invalidKeys,prop)=>validKeys.indexOf(prop)>=0 ? invalidKeys : invalidKeys.concat(prop),[]);// 如果存在无效的key,则抛出一个错误if(invalidKeys.length!==0)throwError(formatErrorMessage(38,__DEV__&&`The configuration object accepts only: ${validKeys.join(", ")}. Invalid keys: ${invalidKeys.join(", ")}.`,validKeys.join(", "),invalidKeys.join(", ")));// 验证应用名称,只能是字符串,且不能为空if(typeofconfig.name!=="string"||config.name.length===0)throwError(formatErrorMessage(20,__DEV__&&"The config.name on registerApplication must be a non-empty string"));// app 属性只能是一个对象或者函数// 对象是一个已被解析过的对象,是一个包含各个生命周期的对象;// 加载函数必须返回一个 promise// 以上信息在官方文档中有提到:https://zh-hans.single-spa.js.org/docs/configurationif(typeofconfig.app!=="object"&&typeofconfig.app!=="function")throwError(formatErrorMessage(20,__DEV__&&"The config.app on registerApplication must be an application or a loading function"));// 第三个参数,可以是一个字符串,也可以是一个函数,也可以是两者组成的一个数组,表示当前应该被激活的应用的baseURLconstallowsStringAndFunction=(activeWhen)=>typeofactiveWhen==="string"||typeofactiveWhen==="function";if(!allowsStringAndFunction(config.activeWhen)&&!(Array.isArray(config.activeWhen)&&config.activeWhen.every(allowsStringAndFunction)))throwError(formatErrorMessage(24,__DEV__&&"The config.activeWhen on registerApplication must be a string, function or an array with both"));// 传递给子应用的props对象必须是一个对象if(!validCustomProps(config.customProps))throwError(formatErrorMessage(22,__DEV__&&"The optional config.customProps must be an object"));}
validateRegisterWithArguments
single-spa/src/applications/apps.js
// 同样是验证四个参数是否合法functionvalidateRegisterWithArguments(name,appOrLoadApp,activeWhen,customProps){if(typeofname!=="string"||name.length===0)throwError(formatErrorMessage(20,__DEV__&&`The 1st argument to registerApplication must be a non-empty string 'appName'`));if(!appOrLoadApp)throwError(formatErrorMessage(23,__DEV__&&"The 2nd argument to registerApplication must be an application or loading application function"));if(typeofactiveWhen!=="function")throwError(formatErrorMessage(24,__DEV__&&"The 3rd argument to registerApplication must be an activeWhen function"));if(!validCustomProps(customProps))throwError(formatErrorMessage(22,__DEV__&&"The optional 4th argument is a customProps and must be an object"));}
/** * 每次切换路由前,将应用分为4大类, * 首次加载时执行loadApp * 后续的路由切换执行performAppChange * 为四大类的应用分别执行相应的操作,比如更改app.status,执行生命周期函数 * 所以,从这里也可以看出来,single-spa就是一个维护应用的状态机 * @param {*} pendingPromises * @param {*} eventArguments */exportfunctionreroute(pendingPromises=[],eventArguments){// 应用正在切换,这个状态会在执行performAppChanges之前置为true,执行结束之后再置为false// 如果在中间用户重新切换路由了,即走这个if分支,暂时看起来就在数组中存储了一些信息,没看到有什么用// 字面意思理解就是用户等待app切换if(appChangeUnderway){returnnewPromise((resolve,reject)=>{peopleWaitingOnAppChange.push({
resolve,
reject,
eventArguments,});});}// 将应用分为4大类const{// 需要被移除的
appsToUnload,// 需要被卸载的
appsToUnmount,// 需要被加载的
appsToLoad,// 需要被挂载的
appsToMount,}=getAppChanges();letappsThatChanged;// 是否已经执行 start 方法if(isStarted()){// 已执行appChangeUnderway=true;// 所有需要被改变的的应用appsThatChanged=appsToUnload.concat(appsToLoad,appsToUnmount,appsToMount);// 执行改变returnperformAppChanges();}else{// 未执行appsThatChanged=appsToLoad;// 加载AppsreturnloadApps();}// 整体返回一个立即resolved的promise,通过微任务来加载appsfunctionloadApps(){returnPromise.resolve().then(()=>{// 加载每个子应用,并做一系列的状态变更和验证(比如结果为promise、子应用要导出生命周期函数)constloadPromises=appsToLoad.map(toLoadPromise);return(// 保证所有加载子应用的微任务执行完成Promise.all(loadPromises).then(callAllEventListeners)// there are no mounted apps, before start() is called, so we always return [].then(()=>[]).catch((err)=>{callAllEventListeners();throwerr;}));});}functionperformAppChanges(){returnPromise.resolve().then(()=>{// https://github.com/single-spa/single-spa/issues/545// 自定义事件,在应用状态发生改变之前可触发,给用户提供搞事情的机会window.dispatchEvent(newCustomEvent(appsThatChanged.length===0
? "single-spa:before-no-app-change"
: "single-spa:before-app-change",getCustomEventDetail(true)));window.dispatchEvent(newCustomEvent("single-spa:before-routing-event",getCustomEventDetail(true)));// 移除应用 => 更改应用状态,执行unload生命周期函数,执行一些清理动作// 其实一般情况下这里没有真的移除应用constunloadPromises=appsToUnload.map(toUnloadPromise);// 卸载应用,更改状态,执行unmount生命周期函数constunmountUnloadPromises=appsToUnmount.map(toUnmountPromise)// 卸载完然后移除,通过注册微任务的方式实现.map((unmountPromise)=>unmountPromise.then(toUnloadPromise));constallUnmountPromises=unmountUnloadPromises.concat(unloadPromises);constunmountAllPromise=Promise.all(allUnmountPromises);// 卸载全部完成后触发一个事件unmountAllPromise.then(()=>{window.dispatchEvent(newCustomEvent("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 * 这个原因其实是因为这些操作都是通过注册不同的微任务实现的,而JS是单线程执行, * 所以自然后续的只能等待前面的执行完了才能执行 * 这里一般情况下其实不会执行,只有手动执行了unloadApplication方法才会二次加载 */constloadThenMountPromises=appsToLoad.map((app)=>{returntoLoadPromise(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,执行生命周期函数 * 当然这里的初始化和挂载其实是前后脚一起完成的(只要中间用户没有切换路由) */constmountPromises=appsToMount.filter((appToMount)=>appsToLoad.indexOf(appToMount)<0).map((appToMount)=>{returntryToBootstrapAndMount(appToMount,unmountAllPromise);});// 后面就没啥了,可以理解为收尾工作returnunmountAllPromise.catch((err)=>{callAllEventListeners();throwerr;}).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();returnPromise.all(loadThenMountPromises.concat(mountPromises)).catch((err)=>{pendingPromises.forEach((promise)=>promise.reject(err));throwerr;}).then(finishUpAndReturn);});});}}
getAppChanges
single-spa/src/applications/apps.js
// 将应用分为四大类exportfunctiongetAppChanges(){// 需要被移除的应用constappsToUnload=[],// 需要被卸载的应用appsToUnmount=[],// 需要被加载的应用appsToLoad=[],// 需要被挂载的应用appsToMount=[];// We re-attempt to download applications in LOAD_ERROR after a timeout of 200 millisecondsconstcurrentTime=newDate().getTime();apps.forEach((app)=>{// boolean,应用是否应该被激活constappShouldBeActive=app.status!==SKIP_BECAUSE_BROKEN&&shouldBeActive(app);switch(app.status){// 需要被加载的应用caseLOAD_ERROR:
if(currentTime-app.loadErrorTime>=200){appsToLoad.push(app);}break;// 需要被加载的应用caseNOT_LOADED:
caseLOADING_SOURCE_CODE:
if(appShouldBeActive){appsToLoad.push(app);}break;// 状态为xx的应用caseNOT_BOOTSTRAPPED:
caseNOT_MOUNTED:
if(!appShouldBeActive&&getAppUnloadInfo(toName(app))){// 需要被移除的应用appsToUnload.push(app);}elseif(appShouldBeActive){// 需要被挂载的应用appsToMount.push(app);}break;// 需要被卸载的应用,已经处于挂载状态,但现在路由已经变了的应用需要被卸载caseMOUNTED:
if(!appShouldBeActive){appsToUnmount.push(app);}break;// all other statuses are ignored}});return{ appsToUnload, appsToUnmount, appsToLoad, appsToMount };}
/** * 通过微任务加载子应用,其实singleSpa中很多地方都用了微任务 * 这里最终是return了一个promise出行,在注册了加载子应用的微任务 * 概括起来就是: * 更改app.status为LOAD_SOURCE_CODE => NOT_BOOTSTRAP,当然还有可能是LOAD_ERROR * 执行加载函数,并将props传递给加载函数,给用户处理props的一个机会,因为这个props是一个完备的props * 验证加载函数的执行结果,必须为promise,且加载函数内部必须return一个对象 * 这个对象是子应用的,对象中必须包括各个必须的生命周期函数 * 然后将生命周期方法通过一个函数包裹并挂载到app对象上 * app加载完成,删除app.loadPromise * @param {*} app */exportfunctiontoLoadPromise(app){returnPromise.resolve().then(()=>{if(app.loadPromise){// 说明app已经在被加载returnapp.loadPromise;}// 只有状态为NOT_LOADED和LOAD_ERROR的app才可以被加载if(app.status!==NOT_LOADED&&app.status!==LOAD_ERROR){returnapp;}// 设置App的状态app.status=LOADING_SOURCE_CODE;letappOpts,isUserErr;return(app.loadPromise=Promise.resolve().then(()=>{// 执行app的加载函数,并给子应用传递props => 用户自定义的customProps和内置的比如应用的名称、singleSpa实例// 其实这里有个疑问,这个props是怎么传递给子应用的,感觉跟后面的生命周期函数有关constloadPromise=app.loadApp(getProps(app));// 加载函数需要返回一个promiseif(!smellsLikeAPromise(loadPromise)){// The name of the app will be prepended to this error message inside of the handleAppError functionisUserErr=true;throwError(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就是示例项目中加载函数中return出来的window.singleSpa,这个属性是子应用打包时设置的returnloadPromise.then((val)=>{app.loadErrorTime=null;// window.singleSpaappOpts=val;letvalidationErrMessage,validationErrCode;// 以下进行一系列的验证,已window.singleSpa为例说明,简称g.s// g.s必须为对象if(typeofappOpts!=="object"){validationErrCode=34;if(__DEV__){validationErrMessage=`does not export anything`;}}// g.s必须导出bootstrap生命周期函数if(!validLifecycleFn(appOpts.bootstrap)){validationErrCode=35;if(__DEV__){validationErrMessage=`does not export a bootstrap function or array of functions`;}}// g.s必须导出mount生命周期函数if(!validLifecycleFn(appOpts.mount)){validationErrCode=36;if(__DEV__){validationErrMessage=`does not export a bootstrap function or array of functions`;}}// g.s必须导出unmount生命周期函数if(!validLifecycleFn(appOpts.unmount)){validationErrCode=37;if(__DEV__){validationErrMessage=`does not export a bootstrap function or array of functions`;}}consttype=objectType(appOpts);// 说明上述验证失败,抛出错误提示信息if(validationErrCode){letappOptsStr;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);returnapp;}if(appOpts.devtools&&appOpts.devtools.overlays){// app.devtoolsoverlays添加子应用的devtools.overlays的属性,不知道是干嘛用的app.devtools.overlays=assign({},app.devtools.overlays,appOpts.devtools.overlays);}// 设置app状态为未初始化,表示加载完了app.status=NOT_BOOTSTRAPPED;// 在app对象上挂载生命周期方法,每个方法都接收一个props作为参数,方法内部执行子应用导出的生命周期函数,并确保生命周期函数返回一个promiseapp.bootstrap=flattenFnArray(appOpts,"bootstrap");app.mount=flattenFnArray(appOpts,"mount");app.unmount=flattenFnArray(appOpts,"unmount");app.unload=flattenFnArray(appOpts,"unload");app.timeouts=ensureValidAppTimeouts(appOpts.timeouts);// 执行到这里说明子应用已成功加载,删除app.loadPromise属性deleteapp.loadPromise;returnapp;});}).catch((err)=>{// 加载失败,稍后重新加载deleteapp.loadPromise;letnewStatus;if(isUserErr){newStatus=SKIP_BECAUSE_BROKEN;}else{newStatus=LOAD_ERROR;app.loadErrorTime=newDate().getTime();}handleAppError(err,app,newStatus);returnapp;}));});}
/** * 返回一个接受props作为参数的函数,这个函数负责执行子应用中的生命周期函数, * 并确保生命周期函数返回的结果为promise * @param {*} appOrParcel => window.singleSpa,子应用打包后的对象 * @param {*} lifecycle => 字符串,生命周期名称 */exportfunctionflattenFnArray(appOrParcel,lifecycle){// fns = fn or []letfns=appOrParcel[lifecycle]||[];// fns = [] or [fn]fns=Array.isArray(fns) ? fns : [fns];// 有些生命周期函数子应用可能不会设置,比如unloadif(fns.length===0){fns=[()=>Promise.resolve()];}consttype=objectType(appOrParcel);constname=toName(appOrParcel);returnfunction(props){// 这里最后返回了一个promise链,这个操作似乎没啥必要,因为不可能出现同名的生命周期函数,所以,这里将生命周期函数放数组,没太理解目的是啥returnfns.reduce((resultPromise,fn,index)=>{returnresultPromise.then(()=>{// 执行生命周期函数,传递props给函数,并验证函数的返回结果,必须为promiseconstthisPromise=fn(props);returnsmellsLikeAPromise(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
constappsToUnload={};/** * 移除应用,就更改一下应用的状态,执行unload生命周期函数,执行清理操作 * * 其实一般情况是不会执行移除操作的,除非你手动调用unloadApplication方法 * 单步调试会发现appsToUnload对象是个空对象,所以第一个if就return了,这里啥也没做 * https://zh-hans.single-spa.js.org/docs/api#unloadapplication * */exportfunctiontoUnloadPromise(app){returnPromise.resolve().then(()=>{// 应用信息constunloadInfo=appsToUnload[toName(app)];if(!unloadInfo){/* No one has called unloadApplication for this app, * 不需要移除 * 一般情况下都不需要移除,只有在调用unloadApplication方法手动执行移除时才会 * 执行后面的内容 */returnapp;}// 已经卸载了,执行一些清理操作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);returnapp;}// 如果应用正在执行挂载,路由突然发生改变,那么也需要应用挂载完成才可以执行移除if(app.status===UNLOADING){/* Both unloadApplication and reroute want to unload this app. * It only needs to be done once, though. */returnunloadInfo.promise.then(()=>app);}if(app.status!==NOT_MOUNTED){/* The app cannot be unloaded until it is unmounted. */returnapp;}// 更改状态为 UNLOADINGapp.status=UNLOADING;// 在合理的时间范围内执行生命周期函数returnreasonableTime(app,"unload").then(()=>{// 一些清理操作finishUnloadingApp(app,unloadInfo);returnapp;}).catch((err)=>{errorUnloadingApp(app,unloadInfo,err);returnapp;});});}
finishUnloadingApp
single-spa/src/lifecycles/unload.js
// 移除完成,执行一些清理动作,其实就是从appsToUnload数组中移除该app,移除生命周期函数,更改app.status// 但应用不是真的被移除,后面再激活时不需要重新去下载资源,,只是做一些状态上的变更,当然load的那个过程还是需要的,这点可能需要再确认一下functionfinishUnloadingApp(app,unloadInfo){deleteappsToUnload[toName(app)];// Unloaded apps don't have lifecyclesdeleteapp.bootstrap;deleteapp.mount;deleteapp.unmount;deleteapp.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
/** * 合理的时间,即生命周期函数合理的执行时间 * 在合理的时间内执行生命周期函数,并将函数的执行结果resolve出去 * @param {*} appOrParcel => app * @param {*} lifecycle => 生命周期函数名 */exportfunctionreasonableTime(appOrParcel,lifecycle){// 应用的超时配置consttimeoutConfig=appOrParcel.timeouts[lifecycle];// 超时警告constwarningPeriod=timeoutConfig.warningMillis;consttype=objectType(appOrParcel);returnnewPromise((resolve,reject)=>{letfinished=false;leterrored=false;// 这里很关键,之前一直奇怪props是怎么传递给子应用的,这里就是了,果然和之前的猜想是一样的// 是在执行生命周期函数时像子应用传递的props,所以之前执行loadApp传递props不会到子应用,// 那么设计估计是给用户自己处理props的一个机会吧,因为那个时候处理的props已经是{ ...customProps, ...内置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);consterrMsg=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);functionmaybeTimingOut(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}}elseif(!errored){constnumWarnings=shouldError;constnumMillis=numWarnings*warningPeriod;console.warn(errMsg);if(numMillis+warningPeriod<timeoutConfig.millis){setTimeout(()=>maybeTimingOut(numWarnings+1),warningPeriod);}}}}});}
toUnmountPromise
single-spa/src/lifecycles/unmount.js
/** * 执行了状态上的更改 * 执行unmount生命周期函数 * @param {*} appOrParcel => app * @param {*} hardFail => 索引 */exportfunctiontoUnmountPromise(appOrParcel,hardFail){returnPromise.resolve().then(()=>{// 只卸载已挂载的应用if(appOrParcel.status!==MOUNTED){returnappOrParcel;}// 更改状态appOrParcel.status=UNMOUNTING;// 有关parcels的一些处理,没使用过parcels,所以unmountChildrenParcels = []constunmountChildrenParcels=Object.keys(appOrParcel.parcels).map((parcelId)=>appOrParcel.parcels[parcelId].unmountThisParcel());letparcelError;returnPromise.all(unmountChildrenParcels)// 在合理的时间范围内执行unmount生命周期函数.then(unmountAppOrParcel,(parcelError)=>{// There is a parcel unmount errorreturnunmountAppOrParcel().then(()=>{// Unmounting the app/parcel succeeded, but unmounting its children parcels did notconstparentError=Error(parcelError.message);if(hardFail){throwtransformErr(parentError,appOrParcel,SKIP_BECAUSE_BROKEN);}else{handleAppError(parentError,appOrParcel,SKIP_BECAUSE_BROKEN);}});}).then(()=>appOrParcel);functionunmountAppOrParcel(){// We always try to unmount the appOrParcel, even if the children parcels failed to unmount.returnreasonableTime(appOrParcel,"unmount").then(()=>{// The appOrParcel needs to stay in a broken status if its children parcels fail to unmountif(!parcelError){appOrParcel.status=NOT_MOUNTED;}}).catch((err)=>{if(hardFail){throwtransformErr(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 * 这里这个两次判断还是很重要的 */functiontryToBootstrapAndMount(app,unmountAllPromise){if(shouldBeActive(app)){// 一次判断为true,才会执行初始化returntoBootstrapPromise(app).then((app)=>unmountAllPromise.then(()=>// 第二次, 两次都为true才会去挂载shouldBeActive(app) ? toMountPromise(app) : app));}else{// 卸载returnunmountAllPromise.then(()=>app);}}
// 挂载app,执行mount生命周期函数,并更改app.statusexportfunctiontoMountPromise(appOrParcel,hardFail){returnPromise.resolve().then(()=>{if(appOrParcel.status!==NOT_MOUNTED){returnappOrParcel;}if(!beforeFirstMountFired){window.dispatchEvent(newCustomEvent("single-spa:before-first-mount"));beforeFirstMountFired=true;}returnreasonableTime(appOrParcel,"mount").then(()=>{appOrParcel.status=MOUNTED;if(!firstMountFired){// single-spa其实在不同的阶段提供了相应的自定义事件,让用户可以做一些事情window.dispatchEvent(newCustomEvent("single-spa:first-mount"));firstMountFired=true;}returnappOrParcel;}).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;returntoUnmountPromise(appOrParcel,true).then(setSkipBecauseBroken,setSkipBecauseBroken);functionsetSkipBecauseBroken(){if(!hardFail){handleAppError(err,appOrParcel,SKIP_BECAUSE_BROKEN);returnappOrParcel;}else{throwtransformErr(err,appOrParcel,SKIP_BECAUSE_BROKEN);}}});});}
start(opts)
single-spa/src/start.js
letstarted=false/** * https://zh-hans.single-spa.js.org/docs/api#start * 调用start之前,应用会被加载,但不会初始化、挂载和卸载,有了start可以更好的控制应用的性能 * @param {*} opts */exportfunctionstart(opts){started=true;if(opts&&opts.urlRerouteOnly){setUrlRerouteOnly(opts.urlRerouteOnly);}if(isInBrowser){reroute();}}exportfunctionisStarted(){returnstarted;}if(isInBrowser){// registerApplication之后如果一直没有调用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);}
/** * 监听路由变化 */if(isInBrowser){// We will trigger an app change for any routing events,监听hashchange和popstate事件window.addEventListener("hashchange",urlReroute);window.addEventListener("popstate",urlReroute);// Monkeypatch addEventListener so that we can ensure correct timing/** * 扩展原生的addEventListener和removeEventListener方法 * 每次注册事件和事件处理函数都会将事件和处理函数保存下来,当然移除时也会做删除 * */constoriginalAddEventListener=window.addEventListener;constoriginalRemoveEventListener=window.removeEventListener;window.addEventListener=function(eventName,fn){if(typeoffn==="function"){if(// eventName只能是hashchange或popstate && 对应事件的fn注册函数没有注册routingEventsListeningTo.indexOf(eventName)>=0&&!find(capturedEventListeners[eventName],(listener)=>listener===fn)){// 注册(保存)eventName 事件的处理函数capturedEventListeners[eventName].push(fn);return;}}// 原生方法returnoriginalAddEventListener.apply(this,arguments);};window.removeEventListener=function(eventName,listenerFn){if(typeoflistenerFn==="function"){// 从captureEventListeners数组中移除eventName事件指定的事件处理函数if(routingEventsListeningTo.indexOf(eventName)>=0){capturedEventListeners[eventName]=capturedEventListeners[eventName].filter((fn)=>fn!==listenerFn);return;}}returnoriginalRemoveEventListener.apply(this,arguments);};// 增强pushstate和replacestatewindow.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 <a> tag's href is. * singleSpa暴露出来的一个全局方法,用户也可以基于它去判断子应用是运行在基座应用上还是独立运行 */window.singleSpaNavigate=navigateToUrl;}}
functioncreatePopStateEvent(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=<pushState|replaceState> on the event instance.letevt;try{evt=newPopStateEvent("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-f1560fb489ddevt=document.createEvent("PopStateEvent");evt.initPopStateEvent("popstate",false,false,state);}evt.singleSpa=true;evt.singleSpaTrigger=originalMethodName;returnevt;}
import"css.escape";constdefaultOpts={// required optsVue: null,appOptions: null,template: null};/** * 判断参数的合法性 * 返回生命周期函数,其中的mount方法负责实例化子应用,update方法提供了基座应用和子应用通信的机会,unmount卸载子应用,bootstrap感觉没啥用 * @param {*} userOpts = { * Vue, * appOptions: { * el: '#id', * store, * router, * render: h => h(App) * } * } * return 四个生命周期函数组成的对象 */exportdefaultfunctionsingleSpaVue(userOpts){// objectif(typeofuserOpts!=="object"){thrownewError(`single-spa-vue requires a configuration object`);}// 合并用户选项和默认选项constopts={
...defaultOpts,
...userOpts};// Vue构造函数if(!opts.Vue){throwError("single-spa-vue must be passed opts.Vue");}// appOptionsif(!opts.appOptions){throwError("single-spa-vue must be passed opts.appOptions");}// el选择器if(opts.appOptions.el&&typeofopts.appOptions.el!=="string"&&!(opts.appOptions.elinstanceofHTMLElement)){throwError(`single-spa-vue: appOptions.el must be a string CSS selector, an HTMLElement, or not provided at all. Was given ${typeofopts.appOptions.el}`);}// Just a shared object to store the mounted object state// key - name of single-spa app, since it is uniqueletmountedInstances={};/** * 返回一个对象,每个属性都是一个生命周期函数 */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)};}functionbootstrap(opts){if(opts.loadRootComponent){returnopts.loadRootComponent().then(root=>(opts.rootComponent=root));}else{returnPromise.resolve();}}/** * 做了三件事情: * 大篇幅的处理el元素 * 然后是render函数 * 实例化子应用 */functionmount(opts,mountedInstances,props){constinstance={};returnPromise.resolve().then(()=>{constappOptions={ ...opts.appOptions};// 可以通过props.domElement属性单独设置自应用的渲染DOM容器,当然appOptions.el必须为空if(props.domElement&&!appOptions.el){appOptions.el=props.domElement;}letdomEl;if(appOptions.el){if(typeofappOptions.el==="string"){// 子应用的DOM容器domEl=document.querySelector(appOptions.el);if(!domEl){throwError(`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){// 设置元素IDdomEl.id=`single-spa-application:${props.name}`;}appOptions.el=`#${CSS.escape(domEl.id)}`;}}else{// 当然如果没有id,这里会自动生成一个idconsthtmlId=`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 domElif(!domEl.querySelector(".single-spa-container")){constsingleSpaContainer=document.createElement("div");singleSpaContainer.className="single-spa-container";domEl.appendChild(singleSpaContainer);}instance.domEl=domEl;// renderif(!appOptions.render&&!appOptions.template&&opts.rootComponent){appOptions.render=h=>h(opts.rootComponent);}// dataif(!appOptions.data){appOptions.data={};}appOptions.data={ ...appOptions.data, ...props};// 实例化子应用instance.vueInstance=newopts.Vue(appOptions);if(instance.vueInstance.bind){instance.vueInstance=instance.vueInstance.bind(instance.vueInstance);}mountedInstances[props.name]=instance;returninstance.vueInstance;});}// 基座应用通过update生命周期函数可以更新子应用的属性functionupdate(opts,mountedInstances,props){returnPromise.resolve().then(()=>{// 应用实例constinstance=mountedInstances[props.name];// 所有的属性constdata={
...(opts.appOptions.data||{}),
...props};// 更新实例对象上的属性值,vm.test = 'xxx'for(letpropindata){instance.vueInstance[prop]=data[prop];}});}// 调用$destroy钩子函数,销毁子应用functionunmount(opts,mountedInstances,props){returnPromise.resolve().then(()=>{constinstance=mountedInstances[props.name];instance.vueInstance.$destroy();instance.vueInstance.$el.innerHTML="";deleteinstance.vueInstance;if(instance.domEl){instance.domEl.innerHTML="";deleteinstance.domEl;}});}
微前端框架 之 single-spa 从入门到精通
当学习成为了习惯,知识也就变成了常识。感谢各位的 点赞、收藏和评论。
新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn
文章已收录到 github,欢迎 Watch 和 Star。
封面
简介
从基本使用 -> 部署 -> 框架源码分析 -> 手写框架,带你全方位刨析 single-spa 框架
前序
目的
会使用
single-spa
开发项目,然后打包部署上线刨析
single-spa
的源码原理手写一个自己的
single-spa
框架过程
编写示例项目
打包部署
框架源码解读
手写框架
关于
微前端
的介绍这里就不再赘述了,网上有很多的文章,本文的重点在于刨析微前端框架single-spa
的实现原理。single-spa
是一个很好的微前端基础框架,qiankun
框架就是基于single-spa
来实现的,在single-spa
的基础上做了一层封装,也解决了single-spa
的一些缺陷。因为
single-spa
是一个基础的微前端框架,了解了它的实现原理,再去看其它的微前端框架,就会非常容易了。提示
先熟悉基本使用,熟悉常用的API,可通过示例项目 + 官网相结合来达成
如果基础比较好,可以先读后面的
手写 single-spa 框架
部分,再回来阅读源码,效果可能会更好文章中涉及到的所有代码都在 github(示例项目 +
single-spa
源码分析 + 手写single-spa
框架 +single-spa-vue
源码分析)示例项目
新建项目目录,接下来的所有代码都会在该目录中完成
示例代码都是通过
vue
来编写的,当然也可以采用其它的,比如react
或者原生JS
等子应用 app1
新建子应用
配置子应用
vue.config.js
在项目根目录下新建
vue.config.js
文件安装
single-spa-vue
single-spa-vue
负责为vue
应用生成通用的生命周期钩子,在子应用注册到single-spa
的基座应用时需要用到改造入口文件
更改视图文件
环境配置文件
.env
应用独立运行时的开发环境配置
.env.micro
作为子应用运行时的开发环境配置
.env.buildMicro
作为子应用构建生产环境
bundle
时的环境配置,但这里的NODE_ENV
为development
,而不是production
,是为了方便,这个方便其实single-spa
带来的弊端(js entry的弊端)修改路由文件
修改
package.json
中的script
启动应用
应用独立运行
当然下面的启动方式也可以,只不过会在
pathname
的开头加了/app1
前缀作为子应用运行
作为独立应用访问
子应用 app2
在
/micro-frontend目录下新建子应用
app2,步骤同
app1,只需把过程中出现的'app1'字样改成'app2'即可,
vue.config.js中的
8081改成
8082`启动应用,作为独立应用访问
子应用 app3(react)
示例项目是基于
react
脚手架cra
创建的,整个集成的过程中难点有两个:webpack
的配置,这部分内容官网有提供子应用入口的配置,单纯看官方文档的示例项目根本跑不起来,或者即使跑起来也有问题,
react
和vue
的集成还不一样,react
需要在主项目的配置中也加一点东西,这部分官网配置没说,是通过single-spa-react
源码看出来的接下来就开始吧,在
/micro-frontend
目录下通过cra
脚手架新建子应用app3
安装 app3
以下所有操作都在
/micro-frontend/app3
目录下进行安装
react-router-dom
、single-spa-react
打散配置
打散项目的配置,方便更改
webpack
的配置内容,当然通过react-app-rewired
覆写默认配置应该也是可以的,官网也有提到,不过我这里没试,采用的是直接打散配置更改 webpack 配置文件
/config/webpack.config.js,官网
删掉
optimization
部分,这部分配置和chunk
有关,有动态生成的异步chunk
存在,会导致主应用无法配置,因为chunk
的名字会变,其实这也是single-spa
的缺陷,或者说采用JS entry
的缺陷,JS entry
建议将所有内容都打成一个bundle
-app.js
更改
entry
和output
部分项目入口文件改造
我这里将无关紧要的内容都删了,只留了
/src/index.js
和/src/index.css
/src/index.js
/src/index.css
启动子应用
浏览器访问
localhost:3000
基座应用 layout
在
/micro-frontend
目录下新建基座应用,为了简洁明了,新建项目时选择的配置项和子应用一样;在本示例中基座应用采用了vue
来实现,用别的方式或者框架实现也可以,比如自己用webpack
构建一个项目。安装
single-spa
改造基座项目
入口文件
App.vue
路由
启动基座应用
浏览器访问基座应用
终于看到了结果。
小技巧
打包部署
打包
在各个项目的根目录下分别执行
部署
可以将打包后的
bundle
发布到nginx
服务器上,这个nginx
服务器可以是单独的服务器、或者虚拟机、亦或是docker
容器都行,这里采用serve
在本地模拟部署如果你有条件部署到
nginx
上,需要注意nginx
的代理配置activeWhen
去挂载对应的子应用全局安装 serve
在各个项目的根目录下启动 serve
在浏览器访问基座应用的地址,发现得到和刚才一样的结果
single-spa 源码分析
single-spa 源码阅读思维导图
从源码目录中可以看到,
single-spa
是使用rollup
来打包的,从rollup.config.js
中可以发现入口是single-spa.js
,打开会发现里面导出了一大堆东西,有我们非常熟悉的各个方法,我们就从
registerApplication
方法开始registerApplication 注册子应用
sanitizeArguments 格式化用户传递的子应用配置参数
validateRegisterWithConfig
validateRegisterWithArguments
sanitizeLoadApp
sanitizeCustomProps
sanitizeActiveWhen
pathToActiveWhen
reroute 更改app.status和执行生命周期函数
getAppChanges
shouldBeActive
toLoadPromise
getProps
smellsLikeAPromise
flattenFnArray
toUnloadPromise
finishUnloadingApp
reasonableTime
toUnmountPromise
tryToBootstrapAndMount
toBootstrapPromise
toMountPromise
start(opts)
监听路由变化
以下代码会被打包进
bundle
的全局作用域内,bundle
被加载以后就会自动执行。这句提示不需要的话可自动忽略patchedUpdateState
createPopStateEvent
urlReroute
小结
以上就是对整个
single-spa
框架源码的解读,相信读到这里你会有不一样的理解吧,当然第一遍读完你有可能有点懵,我当时就是这样,这时候就需要那句古话了,书读百遍,其义自现(干了这碗鸡汤)整个框架的源码读完以后,你会发现:
single-spa
的原理其实很简单,它就是一个子应用加载器 + 状态机的结合体,而且具体怎么加载子应用还是基座应用提供的;框架里面维护了各个子应用的状态,以及在适当的时候负责更改子应用的状态、执行相应的生命周期函数想想框架好像也不复杂,对吧??那接下来就来实现一个自己的
single-spa
框架吧手写 single-spa 框架
经过上面的阅读,相信对
single-spa
已经有一定的理解了,接下来就来实现一个自己的single-spa
,就叫lyn-single-spa
吧。我们好像只需要实现
registerApplication
和start
两个方法并导出即可。写代码之前,必须理清框架内子应用的各个状态以及状态的变更过程,为了便于理解,代码写详细的注释,希望大家看完以后都可以实现一个自己的
single-spa
看着是不是很简单,加注释也才
200
行而已,当然,这只是一个简版的single-spa
框架,没什么健壮性可言,但也正因为简单,所以更能说明single-spa
框架的本质。single-spa-vue 源码分析
single-spa-vue
负责为vue
应用生成通用的生命周期钩子,这些钩子函数负责子应用的初始化、挂载、更新(数据)、卸载。结语
到这里就结束了,文章比较长,写这篇文章也花费了好几天的时间,但是感觉真的很好,
收获满满
,特别是最后手写框架部分。也给各位同学一个建议,一定要勤动手,
不动笔墨不读书
,当你真的把框架写出来时,那个感觉是只看源码完全所不能比拟的,检验你是否真的懂框架原理的最好办法,就是看你能否写一个框架出来
。愿同学们也能收获满满!!
链接
微前端专栏
github
感谢各位的:点赞、收藏和评论,我们下期见。
当学习成为了习惯,知识也就变成了常识。感谢各位的 点赞、收藏和评论。
The text was updated successfully, but these errors were encountered: