Open WanderHuang opened 2 years ago
20211013
进度更新react
源码阅读到了Fiber
这一层。有几个需要搞明白的点
Fiber
原理,以及为什么Fiber
好,英文有一些文献解释地很好,因为闭包和函数调用,是同步执行的,而React
的一个组件就是一个函数,渲染也是函数调用链,因此会生成函数堆栈,而使用Fiber
结构,就很好地模拟了函数调用栈,然后基于每个Fiber
的状态和队列,可以做中段、恢复等操作,就可以配合requestAnimationFrame
和requestIdleCallback
两个API做分片更新,相当于构造了一个异步调用函数的运行时。这一点上,Fiber
是很厉害的架构设计。FIber
有一些核心点:协调器、任务队列、Fiber的current和workInProgress概念等需要继续吃透。
React
分了几个模块,其中React
负责组件本身,ReactDOM
负责渲染层,reconciler
协程调度,Fiber
函数调用栈模拟。这样就串起来了:jsx -> react 对象 -> react渲染器 -> render -> 任务队列、调度等实现 -> fiber -> 真实更新
webpack
源码,核心就是搞懂插件和加载器,另外就是热更新等方案。tappable
,这个库也是必须研究的,webpack
源码里面大量是这个库相关的代码,来实现同步和异步的订阅发布,插件其实就是在各个生命周期暴露回调提供给你用,加载器则是在解析对应文件的时候调用你的处理方法,然后返回js
可以认识的格式,这个并不需要在每个生命周期调用,而是在文件解析前使用。
所以,这里面没有魔术。涉及到的概念有
/// 模拟Component函数
function Component() {}
/// 模拟函数渲染
Component.prototype.render = function () {
console.log('>', this.name)
}
/// 模拟任意组件 示例并行讲解JavaScript继承方式
function A(name) {
// 1. 继承所有值
// 返回数据的原型为Component __proto__ = Component
// let o = {};
// o.__proto__ = new Component();
// Object.assign(o, {name});
// o.name = name
// return o
// 2. 继承对象属性
// 拿不到Component原型链上的方法和属性
// Component.call(this);
// this.name = name;
// 3. 继承对象属性和原型方法,需要配合prototype = {}
// Component.call(this);
// this.name = name;
// 4. 调用一次父类构造函数
Component.call(this);
this.name = name;
}
// 3. 需要修正constructor 调用两次Exec
// A.prototype = new Component();
// A.prototype.constructor = A;
A.prototype = Component.prototype; // 或使用Object.create(Component.prototype)
A.prototype.constructor = A;
/// 模拟协调器
var reconciler = (function() {
// 实际使用优先队列,可以对任务进行排列;实际情况也可以把任务中止
let queue = [];
// 实际会更快(requestAnimationFrame),这里为了演示更慢的效果,用1s
let cycle = setInterval(() => {
if (queue.length) {
let task = queue.shift(); // 一次只执行一个任务,慢一点可以看清楚怎么执行的
// 实际任务执行,这里又可以侵入,做很多事情
task.run();
}
}, 1000);
// 实际的任务还涉及到现场记录、对比、还原等,这里只模拟执行
return (task) => {
queue.push(task);
}
})()
/// 模拟ReactDOM
function update(Ctor, ...args) {
console.log('--- before ---')
let o = new Ctor(...args);
/// render函数被隔离出来的意义:
/// 1. 可以异步渲染。
/// 2. 可以任意侵入、代理o对象
let realName = o.name;
reconciler({ // 这个会在第一个时钟执行
run: () => {
o.name = 'CHANGED';
o.render();
}
});
reconciler({ // 这个会在第二个时钟执行
run: () => {
o.name = realName;
o.render();
}
});
console.log('--- after ---', o)
}
/// 这里相当于是两个独立的树在render,如果把父子关系加入,再加上上面的协调器,就是一个比较完整的react原理的demo了
update(A, 'A');
update(A, 'B');
/// 输出
/// CHANGED 1s
/// A 2s
/// CHANGED 3s
/// B 4s
这份代码没有增加hooks相关的内容。其实一样的,hooks是组件的状态,我们只需要把this
上的状态替换为函数式组件的状态就行了。有点复杂,有时间了再写
前端微服务好处:独立部署、迁移、试验性、弹性应用
应用需要划分为root
和application
,我们可以理解为微服务基座和微服务子应用。
基座负责渲染最初的html + css + js
结构,然后通过注册(register
)操作注册子应用(application
),每一个注册的子应用都应该包含:
active
还是inactive
的函数子应用需要注册一些声明周期来处理自身的dom
,比如mount
、unmount
、bootstrap
等
------------------------------------------------------------------------
start ----->----- root ---->---- registerApplication ---->---- application
------------------------------------------------------------------------
------------------------------------------------------------------------
application bootstrap -->-- mount -->-- unmount
------------------------------------------------------------------------
------------------------------------------------------------------------
registerApplication
注册配置app
需要配置route
:含有匹配的url
app
需要配置loadApp
:一个将子应用加载进来的函数,可以返回promise
(loadAppPromise)核心文件
navigation/
路由相关lifecycles/
生命周期钩子applications/
应用注册相关仅负责路由处理,存一个全局变量started
,核心逻辑交给reroute
应用首先会有一个全局的变量存储当前的所有应用,后续的处理都基于这个变量,变量为const apps = []
获取app变更,这个函数做了一个事:遍历所有的app并分类,便于后续处理。
export function getAppChanges() {
const appsToUnload = [], // 需要去除的应用,和unmount的区别
appsToUnmount = [], // 需要卸载的应用
appsToLoad = [], // 需要加载的应用
appsToMount = []; // 需要挂载的应用
// 下载失败时间检测
const currentTime = new Date().getTime();
apps.forEach((app) => {
const appShouldBeActive =
app.status !== SKIP_BECAUSE_BROKEN && shouldBeActive(app);
switch (app.status) { // 检测状态
case LOAD_ERROR: // 下载失败
if (appShouldBeActive && currentTime - app.loadErrorTime >= 200) {
appsToLoad.push(app);
}
break;
case NOT_LOADED: // 加载中
case LOADING_SOURCE_CODE:
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 };
}
注册子应用registerApplication
export function registerApplication(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
) {
const registration = sanitizeArguments( // 参数归一化,这一步统一处理参数为registration对象
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
)
);
// 可以看到注册应用的核心就是把app相关的数据放到apps数组里面
apps.push(
assign(
{
loadErrorTime: null,
status: NOT_LOADED,
parcels: {},
devtools: {
overlays: {
options: {},
selectors: [],
},
},
},
registration
)
);
// 核心操作,reroute,相当于模拟浏览器的跳转,内部会触发app状态变更(加载、装载、卸载)
if (isInBrowser) {
ensureJQuerySupport();
reroute();
}
}
对应的,unregisterApplication/unloadApplication
方法就是从数组中去掉对应的app
,通过appName
这个变量。当然,函数返回了一个function
作为回调。
其他的validate*
和sanitize*
方法可以先不用管,都是处理参数的。
应用加载和卸载的核心都在navigation
里面了,因为前端微服务始终有个限制,多个应用的路由需要共享同一个窗口的location
,因此任意app
都可以监听location
的变化并做出反应,基于这个原理,去触发应用的加载和卸载。
export function reroute(pendingPromises = [], eventArguments) {
if (appChangeUnderway) {
return new Promise((resolve, reject) => {
peopleWaitingOnAppChange.push({
resolve,
reject,
eventArguments,
});
});
}
const {
appsToUnload,
appsToUnmount,
appsToLoad,
appsToMount,
} = getAppChanges(); // 拿到各种promise,即每个app的下一个状态
let appsThatChanged,
navigationIsCanceled = false,
oldUrl = currentUrl,
newUrl = (currentUrl = window.location.href);
// 处理app状态变更
if (isStarted()) {
appChangeUnderway = true;
appsThatChanged = appsToUnload.concat(
appsToLoad,
appsToUnmount,
appsToMount
);
return performAppChanges(); // 真实处理
} else {
appsThatChanged = appsToLoad;
return loadApps(); // 加载app
}
function cancelNavigation() {
navigationIsCanceled = true;
}
function loadApps() { // 通过app的路径加载文件
return Promise.resolve().then(() => {
const loadPromises = 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();
throw err;
})
);
});
}
function performAppChanges() { // 处理app状态变更
return Promise.resolve().then(() => {
// https://github.com/single-spa/single-spa/issues/545
// 通过自定义事件来处理
window.dispatchEvent(
new CustomEvent(
appsThatChanged.length === 0
? "single-spa:before-no-app-change"
: "single-spa:before-app-change",
getCustomEventDetail(true)
)
);
window.dispatchEvent(
new CustomEvent(
"single-spa:before-routing-event",
getCustomEventDetail(true, { cancelNavigation })
)
);
if (navigationIsCanceled) {
window.dispatchEvent(
new CustomEvent(
"single-spa:before-mount-routing-event",
getCustomEventDetail(true)
)
);
finishUpAndReturn();
navigateToUrl(oldUrl);
return;
}
const unloadPromises = appsToUnload.map(toUnloadPromise);
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)
)
);
});
/* We load and bootstrap apps while other apps are unmounting, but we
* wait to mount the app until all apps are finishing unmounting
*/
const loadThenMountPromises = appsToLoad.map((app) => {
return toLoadPromise(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.
*/
const mountPromises = appsToMount
.filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
.map((appToMount) => {
return tryToBootstrapAndMount(appToMount, unmountAllPromise);
});
return unmountAllPromise // 卸载出错
.catch((err) => {
callAllEventListeners();
throw err;
})
.then(() => { // unmount成功
/* 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();
return Promise.all(loadThenMountPromises.concat(mountPromises)) // 加载应用
.catch((err) => {
pendingPromises.forEach((promise) => promise.reject(err));
throw err;
})
.then(finishUpAndReturn); // 刷新
});
});
}
function finishUpAndReturn() {
const returnValue = getMountedApps();
pendingPromises.forEach((promise) => promise.resolve(returnValue));
try {
const appChangeEventName =
appsThatChanged.length === 0
? "single-spa:no-app-change"
: "single-spa:app-change";
window.dispatchEvent(
new CustomEvent(appChangeEventName, getCustomEventDetail())
);
window.dispatchEvent(
new CustomEvent("single-spa:routing-event", getCustomEventDetail())
);
} catch (err) {
/* We use a setTimeout because if someone else's event handler throws an error, single-spa
* needs to carry on. If a listener to the event throws an error, it's their own fault, not
* single-spa's.
*/
setTimeout(() => {
throw err;
});
}
/* Setting this allows for subsequent calls to reroute() to actually perform
* a reroute instead of just getting queued behind the current reroute call.
* We want to do this after the mounting/unmounting is done but before we
* resolve the promise for the `reroute` function.
*/
appChangeUnderway = false;
if (peopleWaitingOnAppChange.length > 0) { // 处理路由加载卸载过程中又触发了reroute
/* While we were rerouting, someone else triggered another reroute that got queued.
* So we need reroute again.
*/
const nextPendingPromises = peopleWaitingOnAppChange;
peopleWaitingOnAppChange = [];
reroute(nextPendingPromises);
}
return returnValue;
}
/* We need to call all event listeners that have been delayed because they were
* waiting on single-spa. This includes haschange and popstate events for both
* the current run of performAppChanges(), but also all of the queued event listeners.
* We want to call the listeners in the same order as if they had not been delayed by
* single-spa, which means queued ones first and then the most recent one.
*/
function callAllEventListeners() {
pendingPromises.forEach((pendingPromise) => {
callCapturedEventListeners(pendingPromise.eventArguments);
});
callCapturedEventListeners(eventArguments);
}
function getCustomEventDetail(isBeforeChanges = false, extraProperties) {
const newAppStatuses = {};
const appsByNewStatus = {
// for apps that were mounted
[MOUNTED]: [],
// for apps that were unmounted
[NOT_MOUNTED]: [],
// apps that were forcibly unloaded
[NOT_LOADED]: [],
// apps that attempted to do something but are broken now
[SKIP_BECAUSE_BROKEN]: [],
};
if (isBeforeChanges) {
appsToLoad.concat(appsToMount).forEach((app, index) => {
addApp(app, MOUNTED);
});
appsToUnload.forEach((app) => {
addApp(app, NOT_LOADED);
});
appsToUnmount.forEach((app) => {
addApp(app, NOT_MOUNTED);
});
} else {
appsThatChanged.forEach((app) => {
addApp(app);
});
}
const result = {
detail: {
newAppStatuses,
appsByNewStatus,
totalAppChanges: appsThatChanged.length,
originalEvent: eventArguments?.[0],
oldUrl,
newUrl,
navigationIsCanceled,
},
};
if (extraProperties) {
assign(result.detail, extraProperties);
}
return result;
function addApp(app, status) { // 添加应用
const appName = toName(app);
status = status || getAppStatus(appName);
newAppStatuses[appName] = status;
const statusArr = (appsByNewStatus[status] =
appsByNewStatus[status] || []);
statusArr.push(appName);
}
}
}
先看一下load
怎么操作的
export function toLoadPromise(app) {
return Promise.resolve().then(() => {
if (app.loadPromise) {
return app.loadPromise; // 配置了loadPromise 优先使用
}
if (app.status !== NOT_LOADED && app.status !== LOAD_ERROR) {
return app;
}
app.status = LOADING_SOURCE_CODE;
let appOpts, isUserErr;
return (app.loadPromise = Promise.resolve()
.then(() => {
const loadPromise = app.loadApp(getProps(app)); // 配置了loadApp,转为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 loadPromise.then((val) => {
app.loadErrorTime = null;
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
Object.prototype.hasOwnProperty.call(appOpts, "bootstrap") &&
!validLifecycleFn(appOpts.bootstrap)
) {
validationErrCode = 35;
if (__DEV__) {
validationErrMessage = `does not export a valid bootstrap function or array of functions`;
}
}
if (!validLifecycleFn(appOpts.mount)) {
validationErrCode = 36;
if (__DEV__) {
validationErrMessage = `does not export a mount function or array of functions`;
}
}
if (!validLifecycleFn(appOpts.unmount)) {
validationErrCode = 37;
if (__DEV__) {
validationErrMessage = `does not export a unmount function or array of functions`;
}
}
const type = objectType(appOpts);
if (validationErrCode) {
let appOptsStr;
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);
return app;
}
if (appOpts.devtools && appOpts.devtools.overlays) {
app.devtools.overlays = assign(
{},
app.devtools.overlays,
appOpts.devtools.overlays
);
}
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);
delete app.loadPromise;
return app;
});
})
.catch((err) => {
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;
}));
});
}
其他的生命周期类似,就是处理mount
、unmount
函数
qiankun
底层基于single-spa
来做的,我们看源码主要看一下怎么去实现它的功能的。
官方首先列了为什么不用iframe
的原因
url
不同步,但是iframe
可能会操作到top
的history
导致页面跳转dom
不共享,iframe
无法实现全局居中的弹窗postMessage
,cookie
传递要受到安全策略影响,变量无法共享iframe
本身会构建整个浏览上下文,启动速度慢qiankun
的特性:
single-spa
封装,自然需要生命周期、loadApp
等概念HTML Entry
的接入方式(前面我们提到过single-spa
的入口需要导出一个函数,本质上是js
入口)JS
沙箱umi
插件其中我们主要关心这几个点怎么实现的
single-spa
html entry
JS
沙箱注册应用的方法:registerMicroApps
,可以看到注册很简单,直接调用registerApplication
就行了,而且因为single-spa
的loadApp
支持Promise
,因此可以在加载的地方包装一下,使用自己的加载逻辑qiankun/loadApp
export function registerMicroApps<T extends ObjectType>(
apps: Array<RegistrableApp<T>>,
lifeCycles?: FrameworkLifeCycles<T>,
) {
// Each app only needs to be registered once
// microApps是一个全局变量,和single-spa里面的apps一个道理,缓存当前已经加载的应用
const unregisteredApps = apps.filter((app) => !microApps.some((registeredApp) => registeredApp.name === app.name));
microApps = [...microApps, ...unregisteredApps];
unregisteredApps.forEach((app) => {
const { name, activeRule, loader = noop, props, ...appConfig } = app;
registerApplication({
name,
// singal-spa app -> single-spa loadApp -> Promise<app>
app: async () => { // single-spa的loadApp函数,返回一个带有生命周期的对象
loader(true);
await frameworkStartedDefer.promise;
// qiankun自己的加载app原理
const { mount, ...otherMicroAppConfigs } = (
await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
)();
return {
mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
...otherMicroAppConfigs,
};
},
activeWhen: activeRule,
customProps: props,
});
});
}
手动加载微应用的方法:loadMicroApp
export function loadMicroApp<T extends ObjectType>(
app: LoadableApp<T>,
configuration?: FrameworkConfiguration,
lifeCycles?: FrameworkLifeCycles<T>,
): MicroApp {
const { props, name } = app;
// container是挂载点
const container = 'container' in app ? app.container : undefined;
// Must compute the container xpath at beginning to keep it consist around app running
// If we compute it every time, the container dom structure most probably been changed and result in a different xpath value
const containerXPath = getContainerXPath(container);
const appContainerXPathKey = `${name}-${containerXPath}`; // 应用路径和container元素关联,在挂载的时候好卸载同一个container上挂载的其他应用
let microApp: MicroApp;
const wrapParcelConfigForRemount = (config: ParcelConfigObject): ParcelConfigObject => {
let microAppConfig = config;
if (container) {
if (containerXPath) {
const containerMicroApps = containerMicroAppsMap.get(appContainerXPathKey);
if (containerMicroApps?.length) {
const mount = [
async () => {
// While there are multiple micro apps mounted on the same container, we must wait until the prev instances all had unmounted
// Otherwise it will lead some concurrent issues
// 卸载其他应用
const prevLoadMicroApps = containerMicroApps.slice(0, containerMicroApps.indexOf(microApp));
const prevLoadMicroAppsWhichNotBroken = prevLoadMicroApps.filter(
(v) => v.getStatus() !== 'LOAD_ERROR' && v.getStatus() !== 'SKIP_BECAUSE_BROKEN',
);
await Promise.all(prevLoadMicroAppsWhichNotBroken.map((v) => v.unmountPromise));
},
...toArray(microAppConfig.mount),
];
microAppConfig = {
...config,
mount,
};
}
}
}
return {
...microAppConfig,
// empty bootstrap hook which should not run twice while it calling from cached micro app
bootstrap: () => Promise.resolve(),
};
};
/**
* using name + container xpath as the micro app instance id,
* it means if you rendering a micro app to a dom which have been rendered before,
* the micro app would not load and evaluate its lifecycles again
*/
const memorizedLoadingFn = async (): Promise<ParcelConfigObject> => { // 内存缓存
const userConfiguration = autoDowngradeForLowVersionBrowser(
configuration ?? { ...frameworkConfiguration, singular: false },
);
const { $$cacheLifecycleByAppName } = userConfiguration;
if (container) {
// using appName as cache for internal experimental scenario
if ($$cacheLifecycleByAppName) {
const parcelConfigGetterPromise = appConfigPromiseGetterMap.get(name);
if (parcelConfigGetterPromise) return wrapParcelConfigForRemount((await parcelConfigGetterPromise)(container));
}
if (containerXPath) {
const parcelConfigGetterPromise = appConfigPromiseGetterMap.get(appContainerXPathKey);
if (parcelConfigGetterPromise) return wrapParcelConfigForRemount((await parcelConfigGetterPromise)(container));
}
}
const parcelConfigObjectGetterPromise = loadApp(app, userConfiguration, lifeCycles);
if (container) {
if ($$cacheLifecycleByAppName) {
appConfigPromiseGetterMap.set(name, parcelConfigObjectGetterPromise);
} else if (containerXPath) appConfigPromiseGetterMap.set(appContainerXPathKey, parcelConfigObjectGetterPromise);
}
return (await parcelConfigObjectGetterPromise)(container);
};
if (!started) {
// We need to invoke start method of single-spa as the popstate event should be dispatched while the main app calling pushState/replaceState automatically,
// but in single-spa it will check the start status before it dispatch popstate
// see https://github.com/single-spa/single-spa/blob/f28b5963be1484583a072c8145ac0b5a28d91235/src/navigation/navigation-events.js#L101
// ref https://github.com/umijs/qiankun/pull/1071
startSingleSpa({ urlRerouteOnly: frameworkConfiguration.urlRerouteOnly ?? defaultUrlRerouteOnly }); // 启动single-spa
}
// 基座挂载
microApp = mountRootParcel(memorizedLoadingFn, { domElement: document.createElement('div'), ...props });
if (container) {
if (containerXPath) {
// Store the microApps which they mounted on the same container
const microAppsRef = containerMicroAppsMap.get(appContainerXPathKey) || [];
microAppsRef.push(microApp);
containerMicroAppsMap.set(appContainerXPathKey, microAppsRef);
const cleanup = () => {
const index = microAppsRef.indexOf(microApp);
microAppsRef.splice(index, 1);
// @ts-ignore
microApp = null;
};
// gc after unmount
microApp.unmountPromise.then(cleanup).catch(cleanup); // unmount生命周期处理
}
}
return microApp;
}
加载应用的方法:loadApp
export async function loadApp<T extends ObjectType>(
app: LoadableApp<T>,
configuration: FrameworkConfiguration = {},
lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObjectGetter> {
const { entry, name: appName } = app; // name为区别各子应用必须的。entry分为jsentry和html的entry
const appInstanceId = `${appName}_${+new Date()}_${Math.floor(Math.random() * 1000)}`;
const markName = `[qiankun] App ${appInstanceId} Loading`;
if (process.env.NODE_ENV === 'development') {
performanceMark(markName);
}
const {
singular = false,
sandbox = true,
excludeAssetFilter,
globalContext = window,
...importEntryOpts
} = configuration;
// get the entry html content and script executor
// html-entry加载的核心原理:加载html -> 遍历加载css和js -> css和js做隔离,这些需要单独看importEntry这个库。
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
// as single-spa load and bootstrap new app parallel with other apps unmounting
// (see https://github.com/CanopyTax/single-spa/blob/master/src/navigation/reroute.js#L74)
// we need wait to load the app until all apps are finishing unmount in singular mode
if (await validateSingularMode(singular, app)) { // 校验是否单应用,单应用/多应用主要关联到路由的分配问题
await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise);
}
const appContent = getDefaultTplWrapper(appInstanceId, appName)(template);
const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation;
if (process.env.NODE_ENV === 'development' && strictStyleIsolation) {
console.warn(
"[qiankun] strictStyleIsolation configuration will be removed in 3.0, pls don't depend on it or use experimentalStyleIsolation instead!",
);
}
const scopedCSS = isEnableScopedCSS(sandbox);
let initialAppWrapperElement: HTMLElement | null = createElement(
appContent,
strictStyleIsolation,
scopedCSS,
appName,
);
const initialContainer = 'container' in app ? app.container : undefined;
const legacyRender = 'render' in app ? app.render : undefined;
const render = getRender(appName, appContent, legacyRender); // render的作用就是调用appendChild添加元素到DOM,添加的自然是container描述的元素
// 第一次加载设置应用可见区域 dom 结构
// 确保每次应用加载前容器 dom 结构已经设置完毕
render({ element: initialAppWrapperElement, loading: true, container: initialContainer }, 'loading');
const initialAppWrapperGetter = getAppWrapperGetter(
appName,
appInstanceId,
!!legacyRender,
strictStyleIsolation,
scopedCSS,
() => initialAppWrapperElement,
);
let global = globalContext; // 要实现隔离,就要用到eval | with特性,这里暂存当前的上下文为global,其实就是window
let mountSandbox = () => Promise.resolve();
let unmountSandbox = () => Promise.resolve();
const useLooseSandbox = typeof sandbox === 'object' && !!sandbox.loose;
let sandboxContainer;
if (sandbox) {
sandboxContainer = createSandboxContainer( // js沙箱环境构建
appName,
// FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518
initialAppWrapperGetter,
scopedCSS,
useLooseSandbox,
excludeAssetFilter,
global,
);
// 用沙箱的代理对象作为接下来使用的全局对象
global = sandboxContainer.instance.proxy as typeof window;
mountSandbox = sandboxContainer.mount;
unmountSandbox = sandboxContainer.unmount;
}
const {
beforeUnmount = [],
afterUnmount = [],
afterMount = [],
beforeMount = [],
beforeLoad = [],
} = mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, (v1, v2) => concat(v1 ?? [], v2 ?? []));
await execHooksChain(toArray(beforeLoad), app, global); // 执行生命周期
// get the lifecycle hooks from module exports
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox); // 得到app入口的脚本文件
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports( // 得到生命周期
scriptExports,
appName,
global,
sandboxContainer?.instance?.latestSetProp,
);
const { onGlobalStateChange, setGlobalState, offGlobalStateChange }: Record<string, CallableFunction> =
getMicroAppStateActions(appInstanceId); // qiankun的方法,给基座注入的全局状态,可以在父子组件、兄弟组件之间传递信息
// FIXME temporary way
const syncAppWrapperElement2Sandbox = (element: HTMLElement | null) => (initialAppWrapperElement = element);
const parcelConfigGetter: ParcelConfigObjectGetter = (remountContainer = initialContainer) => {
let appWrapperElement: HTMLElement | null;
let appWrapperGetter: ReturnType<typeof getAppWrapperGetter>;
const parcelConfig: ParcelConfigObject = {
name: appInstanceId,
bootstrap,
mount: [ // 相当于流水线
async () => {
if (process.env.NODE_ENV === 'development') {
const marks = performanceGetEntriesByName(markName, 'mark');
// mark length is zero means the app is remounting
if (marks && !marks.length) {
performanceMark(markName);
}
}
},
async () => {
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
return prevAppUnmountedDeferred.promise;
}
return undefined;
},
// initial wrapper element before app mount/remount
async () => {
appWrapperElement = initialAppWrapperElement;
appWrapperGetter = getAppWrapperGetter(
appName,
appInstanceId,
!!legacyRender,
strictStyleIsolation,
scopedCSS,
() => appWrapperElement,
);
},
// 添加 mount hook, 确保每次应用加载前容器 dom 结构已经设置完毕
async () => {
const useNewContainer = remountContainer !== initialContainer;
if (useNewContainer || !appWrapperElement) {
// element will be destroyed after unmounted, we need to recreate it if it not exist
// or we try to remount into a new container
appWrapperElement = createElement(appContent, strictStyleIsolation, scopedCSS, appName);
syncAppWrapperElement2Sandbox(appWrapperElement);
}
render({ element: appWrapperElement, loading: true, container: remountContainer }, 'mounting');
},
mountSandbox,
// exec the chain after rendering to keep the behavior with beforeLoad
async () => execHooksChain(toArray(beforeMount), app, global),
async (props) => mount({ ...props, container: appWrapperGetter(), setGlobalState, onGlobalStateChange }),
// finish loading after app mounted
async () => render({ element: appWrapperElement, loading: false, container: remountContainer }, 'mounted'),
async () => execHooksChain(toArray(afterMount), app, global),
// initialize the unmount defer after app mounted and resolve the defer after it unmounted
async () => {
if (await validateSingularMode(singular, app)) {
prevAppUnmountedDeferred = new Deferred<void>();
}
},
async () => {
if (process.env.NODE_ENV === 'development') {
const measureName = `[qiankun] App ${appInstanceId} Loading Consuming`;
performanceMeasure(measureName, markName);
}
},
],
unmount: [ // 同理流水线
async () => execHooksChain(toArray(beforeUnmount), app, global),
async (props) => unmount({ ...props, container: appWrapperGetter() }),
unmountSandbox,
async () => execHooksChain(toArray(afterUnmount), app, global),
async () => {
render({ element: null, loading: false, container: remountContainer }, 'unmounted');
offGlobalStateChange(appInstanceId);
// for gc
appWrapperElement = null;
syncAppWrapperElement2Sandbox(appWrapperElement);
},
async () => {
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
prevAppUnmountedDeferred.resolve();
}
},
],
};
if (typeof update === 'function') {
parcelConfig.update = update;
}
return parcelConfig;
};
return parcelConfigGetter;
}
给基座创建沙箱环境:createSandboxContainer
/**
* 生成应用运行时沙箱
*
* 沙箱分两个类型:
* 1. app 环境沙箱
* app 环境沙箱是指应用初始化过之后,应用会在什么样的上下文环境运行。每个应用的环境沙箱只会初始化一次,因为子应用只会触发一次 bootstrap 。
* 子应用在切换时,实际上切换的是 app 环境沙箱。
* 2. render 沙箱
* 子应用在 app mount 开始前生成好的的沙箱。每次子应用切换过后,render 沙箱都会重现初始化。
*
* 这么设计的目的是为了保证每个子应用切换回来之后,还能运行在应用 bootstrap 之后的环境下。
*
* @param appName
* @param elementGetter
* @param scopedCSS
* @param useLooseSandbox
* @param excludeAssetFilter
* @param globalContext
*/
export function createSandboxContainer(
appName: string,
elementGetter: () => HTMLElement | ShadowRoot,
scopedCSS: boolean,
useLooseSandbox?: boolean,
excludeAssetFilter?: (url: string) => boolean,
globalContext?: typeof window,
) {
let sandbox: SandBox;
if (window.Proxy) {
sandbox = useLooseSandbox ? new LegacySandbox(appName, globalContext) : new ProxySandbox(appName, globalContext);
} else {
sandbox = new SnapshotSandbox(appName);
}
// some side effect could be be invoked while bootstrapping, such as dynamic stylesheet injection with style-loader, especially during the development phase
const bootstrappingFreers = patchAtBootstrapping(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter);
// mounting freers are one-off and should be re-init at every mounting time
let mountingFreers: Freer[] = [];
let sideEffectsRebuilders: Rebuilder[] = [];
return {
instance: sandbox,
/**
* 沙箱被 mount
* 可能是从 bootstrap 状态进入的 mount
* 也可能是从 unmount 之后再次唤醒进入 mount
*/
async mount() {
/* ------------------------------------------ 因为有上下文依赖(window),以下代码执行顺序不能变 ------------------------------------------ */
/* ------------------------------------------ 1. 启动/恢复 沙箱------------------------------------------ */
sandbox.active();
const sideEffectsRebuildersAtBootstrapping = sideEffectsRebuilders.slice(0, bootstrappingFreers.length);
const sideEffectsRebuildersAtMounting = sideEffectsRebuilders.slice(bootstrappingFreers.length);
// must rebuild the side effects which added at bootstrapping firstly to recovery to nature state
if (sideEffectsRebuildersAtBootstrapping.length) {
sideEffectsRebuildersAtBootstrapping.forEach((rebuild) => rebuild());
}
/* ------------------------------------------ 2. 开启全局变量补丁 ------------------------------------------*/
// render 沙箱启动时开始劫持各类全局监听,尽量不要在应用初始化阶段有 事件监听/定时器 等副作用
mountingFreers = patchAtMounting(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter);
/* ------------------------------------------ 3. 重置一些初始化时的副作用 ------------------------------------------*/
// 存在 rebuilder 则表明有些副作用需要重建
if (sideEffectsRebuildersAtMounting.length) {
sideEffectsRebuildersAtMounting.forEach((rebuild) => rebuild());
}
// clean up rebuilders
sideEffectsRebuilders = [];
},
/**
* 恢复 global 状态,使其能回到应用加载之前的状态
*/
async unmount() {
// record the rebuilders of window side effects (event listeners or timers)
// note that the frees of mounting phase are one-off as it will be re-init at next mounting
sideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map((free) => free());
sandbox.inactive();
},
};
}
我们再看一个实现沙箱环境的方法:ProxySandbox
/**
* 基于 Proxy 实现的沙箱
*/
export default class ProxySandbox implements SandBox {
/** window 值变更记录 */
private updatedValueSet = new Set<PropertyKey>();
name: string;
type: SandBoxType;
proxy: WindowProxy;
globalContext: typeof window;
sandboxRunning = true;
latestSetProp: PropertyKey | null = null;
private registerRunningApp(name: string, proxy: Window) {
if (this.sandboxRunning) {
setCurrentRunningApp({ name, window: proxy }); // 设置当前运行中的app
// FIXME if you have any other good ideas
// remove the mark in next tick, thus we can identify whether it in micro app or not
// this approach is just a workaround, it could not cover all complex cases, such as the micro app runs in the same task context with master in some case
nextTask(() => {
setCurrentRunningApp(null);
});
}
}
active() {
if (!this.sandboxRunning) activeSandboxCount++;
this.sandboxRunning = true;
}
inactive() {
if (process.env.NODE_ENV === 'development') {
console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [
...this.updatedValueSet.keys(),
]);
}
if (--activeSandboxCount === 0) { // 子应用都是inactive了就检查清理掉所有属性
variableWhiteList.forEach((p) => {
if (this.proxy.hasOwnProperty(p)) {
// @ts-ignore
delete this.globalContext[p];
}
});
}
this.sandboxRunning = false;
}
constructor(name: string, globalContext = window) {
this.name = name;
this.globalContext = globalContext;
this.type = SandBoxType.Proxy;
const { updatedValueSet } = this;
const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext); // fakeWindow通过便利globanContenxt的属性生成,也就是一个对象
const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>();
const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || globalContext.hasOwnProperty(key);
const proxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
if (this.sandboxRunning) {
this.registerRunningApp(name, proxy);
// We must kept its description while the property existed in globalContext before
if (!target.hasOwnProperty(p) && globalContext.hasOwnProperty(p)) { // 不允许修改window原生的属性
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
const { writable, configurable, enumerable } = descriptor!;
if (writable) {
Object.defineProperty(target, p, {
configurable,
enumerable,
writable,
value,
});
}
} else {
// @ts-ignore
target[p] = value;
}
if (variableWhiteList.indexOf(p) !== -1) {
// @ts-ignore
globalContext[p] = value;
}
updatedValueSet.add(p);
this.latestSetProp = p;
return true;
}
if (process.env.NODE_ENV === 'development') {
console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}!`);
}
// 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
return true;
},
get: (target: FakeWindow, p: PropertyKey): any => {
this.registerRunningApp(name, proxy);
if (p === Symbol.unscopables) return unscopables;
// avoid who using window.window or window.self to escape the sandbox environment to touch the really window
// see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
if (p === 'window' || p === 'self') {
return proxy;
}
// hijack globalWindow accessing with globalThis keyword
if (p === 'globalThis') { // global.globalThis指向parent
return proxy;
}
if (
p === 'top' ||
p === 'parent' ||
(process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
) { // 子应用调用window.top window.parent
// if your master app in an iframe context, allow these props escape the sandbox
if (globalContext === globalContext.parent) { // iframe调用,指向父窗口
return proxy;
}
return (globalContext as any)[p]; // 普通调用,从缓存中取出父窗口
}
// proxy.hasOwnProperty would invoke getter firstly, then its value represented as globalContext.hasOwnProperty
if (p === 'hasOwnProperty') { // hasOwnProperty
return hasOwnProperty;
}
if (p === 'document') { // document
return document;
}
if (p === 'eval') { // eval
return eval;
}
const value = propertiesWithGetter.has(p)
? (globalContext as any)[p]
: p in target
? (target as any)[p]
: (globalContext as any)[p];
/* Some dom api must be bound to native window, otherwise it would cause exception like 'TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation'
See this code:
const proxy = new Proxy(window, {});
const proxyFetch = fetch.bind(proxy);
proxyFetch('https://qiankun.com');
*/
const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : globalContext;
return getTargetValue(boundTarget, value);
},
// trap in operator
// see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12
has(target: FakeWindow, p: string | number | symbol): boolean {
return p in unscopables || p in target || p in globalContext;
},
getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {
/*
as the descriptor of top/self/window/mockTop in raw window are configurable but not in proxy target, we need to get it from target to avoid TypeError
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor
> A property cannot be reported as non-configurable, if it does not exists as an own property of the target object or if it exists as a configurable own property of the target object.
*/
if (target.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(target, p);
descriptorTargetMap.set(p, 'target');
return descriptor;
}
if (globalContext.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
descriptorTargetMap.set(p, 'globalContext');
// A property cannot be reported as non-configurable, if it does not exists as an own property of the target object
if (descriptor && !descriptor.configurable) {
descriptor.configurable = true;
}
return descriptor;
}
return undefined;
},
// trap to support iterator with sandbox
ownKeys(target: FakeWindow): ArrayLike<string | symbol> {
return uniq(Reflect.ownKeys(globalContext).concat(Reflect.ownKeys(target)));
},
defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {
const from = descriptorTargetMap.get(p);
/*
Descriptor must be defined to native window while it comes from native window via Object.getOwnPropertyDescriptor(window, p),
otherwise it would cause a TypeError with illegal invocation.
*/
switch (from) {
case 'globalContext':
return Reflect.defineProperty(globalContext, p, attributes);
default:
return Reflect.defineProperty(target, p, attributes);
}
},
deleteProperty: (target: FakeWindow, p: string | number | symbol): boolean => {
this.registerRunningApp(name, proxy);
if (target.hasOwnProperty(p)) {
// @ts-ignore
delete target[p];
updatedValueSet.delete(p);
return true;
}
return true;
},
// makes sure `window instanceof Window` returns truthy in micro app
getPrototypeOf() {
return Reflect.getPrototypeOf(globalContext);
},
});
this.proxy = proxy;
activeSandboxCount++;
}
}
import-html-entry本质上是以前传统的js加载处理的方式,源码也不多,可以读一读
资源汇总
react
webpack
Rxjs