前言
在上一篇文章了解了qiankun的整體運(yùn)行。下面繼續(xù)看:
1.qiankun如何根據(jù)entry字段去加載子應(yīng)用的資源。
2.qiankun提供的沙箱隔離。
正文
(1) loadApp
在上一篇中說(shuō)到single-spa的app配置需要開(kāi)發(fā)者自己處理加載子應(yīng)用的邏輯,在qiankun的registerMicroApps
中,封裝了loadApp
方法。
export function registerMicroApps<T extends ObjectType>(
apps: Array<RegistrableApp<T>>,
lifeCycles?: FrameworkLifeCycles<T>,
) {
// Each app only needs to be registered once
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,
app: async () => {
loader(true);
await frameworkStartedDefer.promise;
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,
});
});
}
然后進(jìn)入到src/loader.ts
中查看loadApp
都做了什么:
首先使用importEntry
將entry傳入,獲取到 template, execScripts, assetPublicPath。importEntry這個(gè)包的作用就是,給它一個(gè)站點(diǎn)鏈接,它請(qǐng)求到站點(diǎn)的整個(gè)html,然后解析出html的各個(gè)內(nèi)容:dom、script、css等,然后根據(jù)需要去加載。下面是其全部返回的輸出:
這里也許會(huì)有疑問(wèn)是:為什么像上一篇中直接使用動(dòng)態(tài)加載script技術(shù)一樣,直接將整個(gè)html append到domcument加載出來(lái),不是很省事情嗎?
這是因?yàn)閝iankun要做沙箱隔離,所以先自己解析出資源,處理后再加載到頁(yè)面。
對(duì)于dom和style,qiankun會(huì)對(duì)其進(jìn)行一些包裝,然后使用getRender
下的render
方法,將內(nèi)容append到容器中:
if (element) {
rawAppendChild.call(containerElement, element);
}
最終容器內(nèi)容如下:
(2) execScripts 與沙箱隔離
上圖可以看到script標(biāo)簽都被注釋掉了,下面要使用execScripts去執(zhí)行JS。在importEntry中查看execScripts:
其可以傳入一個(gè)沙箱,讓JS都在這個(gè)沙箱中執(zhí)行。回到qiankun的loadApp方法中:
export async function loadApp<T extends ObjectType>(
app: LoadableApp<T>,
configuration: FrameworkConfiguration = {},
lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObjectGetter> {
const { entry, name: appName } = app;
const {
singular = false,
sandbox = true,
excludeAssetFilter,
globalContext = window,
...importEntryOpts
} = configuration;
... ...
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
let global = globalContext;
const useLooseSandbox = typeof sandbox === 'object' && !!sandbox.loose;
let sandboxContainer;
if (sandbox) {
sandboxContainer = createSandboxContainer(
appInstanceId,
// FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518
initialAppWrapperGetter,
scopedCSS,
useLooseSandbox,
excludeAssetFilter,
global,
);
// 用沙箱的代理對(duì)象作為接下來(lái)使用的全局對(duì)象
global = sandboxContainer.instance.proxy as typeof window;
mountSandbox = sandboxContainer.mount;
unmountSandbox = sandboxContainer.unmount;
}
... ...
// get the lifecycle hooks from module exports
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox);
從上面可以看到,global一開(kāi)始默認(rèn)為window環(huán)境,后判斷如果sandbox
為true,使用createSandboxContainer
的sandboxContainer.instance.proxy
來(lái)替換。在src/sandbox/index.ts
下查看createSandboxContainer
:
import LegacySandbox from './legacy/sandbox';
import ProxySandbox from './proxySandbox';
import SnapshotSandbox from './snapshotSandbox';
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);
}
... ...
}
可以看到qiankun有三種JS隔離機(jī)制,分別是 SnapshotSandbox、LegacySandbox和ProxySandbox。
三個(gè)沙箱的原理,都比較簡(jiǎn)單:
SnapshotSandbox:快照沙箱。就是在加載子應(yīng)用時(shí)淺拷貝一份window,名為windowSnapshot。在卸載子應(yīng)用時(shí),再使用windowSnapshot將window復(fù)原。下面是自己的簡(jiǎn)單實(shí)現(xiàn):
LegacySandbox:
LegacySandbox和快照沙箱差不多,不同的是,其使用Proxy劫持set操作,記錄那些被更改的window屬性。這樣在后續(xù)的狀態(tài)還原時(shí)候就不再需要遍歷window的所有屬性來(lái)進(jìn)行對(duì)比,提升了程序運(yùn)行的性能。但是它最終還是去修改了window上的屬性,所以這種機(jī)制仍然污染了window的狀態(tài)。
ProxySandbox:看了LegacySandbox會(huì)有疑問(wèn),既然都用了代理了,修改代理對(duì)象就好了,為什么經(jīng)過(guò)代理后還去修改window啊:
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)) {
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;
}
... ...
}
},
});
上面代碼中fakeWindow
初始為一個(gè)對(duì)象,通過(guò)劫持set操作,將value保存到fakeWindow
即可,不用去修改window,所以也不用復(fù)原操作。
(3) execScripts原理
終于弄明白傳入execScripts的沙箱是個(gè)啥玩意了,下面回到import-html-entry
的execScripts中:
主要看到
getExecutableScript
和evalCode
,我們有了沙箱后,如何讓JS代碼在沙箱環(huán)境執(zhí)行呢?
看到getExecutableScript
,它將傳入的scriptText
進(jìn)行了一層自執(zhí)行函數(shù)包裹,自執(zhí)行函數(shù)接收代理對(duì)象,然后函數(shù)參數(shù)名為window。這樣子,scriptText
中對(duì)window的訪(fǎng)問(wèn),實(shí)際都是訪(fǎng)問(wèn)到代理對(duì)象!
得到最終的code后,調(diào)用evalCode
:
可以看到,
evalCode
就是簡(jiǎn)單的調(diào)用eval,執(zhí)行我們的JS代碼,由此實(shí)現(xiàn)應(yīng)用的JS Bundle加載!
總結(jié)
這一篇主要講的是qiankun的loadApps,如何根據(jù)entry字段去加載子應(yīng)用的資源、以及提供的沙箱來(lái)執(zhí)行JS。大概流程就是這樣。
另外有個(gè)疑問(wèn)是,無(wú)論是快照沙箱還是代理沙箱,只能監(jiān)聽(tīng)到window上第一層的key值,對(duì)于更深層的對(duì)象,如果被修改了那還是會(huì)被污染的。