前言
在上一篇文章了解了qiankun的整体运行。下面继续看:
1.qiankun如何根据entry字段去加载子应用的资源。
2.qiankun提供的沙箱隔离。
正文
(1) loadApp
在上一篇中说到single-spa的app配置需要开发者自己处理加载子应用的逻辑,在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,
});
});
}
然后进入到src/loader.ts
中查看loadApp
都做了什么:
首先使用importEntry
将entry传入,获取到 template, execScripts, assetPublicPath。importEntry这个包的作用就是,给它一个站点链接,它请求到站点的整个html,然后解析出html的各个内容:dom、script、css等,然后根据需要去加载。下面是其全部返回的输出:
这里也许会有疑问是:为什么像上一篇中直接使用动态加载script技术一样,直接将整个html append到domcument加载出来,不是很省事情吗?
这是因为qiankun要做沙箱隔离,所以先自己解析出资源,处理后再加载到页面。
对于dom和style,qiankun会对其进行一些包装,然后使用getRender
下的render
方法,将内容append到容器中:
if (element) {
rawAppendChild.call(containerElement, element);
}
最终容器内容如下:
(2) execScripts 与沙箱隔离
上图可以看到script标签都被注释掉了,下面要使用execScripts去执行JS。在importEntry中查看execScripts:
其可以传入一个沙箱,让JS都在这个沙箱中执行。回到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,
);
// 用沙箱的代理对象作为接下来使用的全局对象
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一开始默认为window环境,后判断如果sandbox
为true,使用createSandboxContainer
的sandboxContainer.instance.proxy
来替换。在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隔离机制,分别是 SnapshotSandbox、LegacySandbox和ProxySandbox。
三个沙箱的原理,都比较简单:
SnapshotSandbox:快照沙箱。就是在加载子应用时浅拷贝一份window,名为windowSnapshot。在卸载子应用时,再使用windowSnapshot将window复原。下面是自己的简单实现:
LegacySandbox:
LegacySandbox和快照沙箱差不多,不同的是,其使用Proxy劫持set操作,记录那些被更改的window属性。这样在后续的状态还原时候就不再需要遍历window的所有属性来进行对比,提升了程序运行的性能。但是它最终还是去修改了window上的属性,所以这种机制仍然污染了window的状态。
ProxySandbox:看了LegacySandbox会有疑问,既然都用了代理了,修改代理对象就好了,为什么经过代理后还去修改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
初始为一个对象,通过劫持set操作,将value保存到fakeWindow
即可,不用去修改window,所以也不用复原操作。
(3) execScripts原理
终于弄明白传入execScripts的沙箱是个啥玩意了,下面回到import-html-entry
的execScripts中:
主要看到
getExecutableScript
和evalCode
,我们有了沙箱后,如何让JS代码在沙箱环境执行呢?
看到getExecutableScript
,它将传入的scriptText
进行了一层自执行函数包裹,自执行函数接收代理对象,然后函数参数名为window。这样子,scriptText
中对window的访问,实际都是访问到代理对象!
得到最终的code后,调用evalCode
:
可以看到,
evalCode
就是简单的调用eval,执行我们的JS代码,由此实现应用的JS Bundle加载!
总结
这一篇主要讲的是qiankun的loadApps,如何根据entry字段去加载子应用的资源、以及提供的沙箱来执行JS。大概流程就是这样。
另外有个疑问是,无论是快照沙箱还是代理沙箱,只能监听到window上第一层的key值,对于更深层的对象,如果被修改了那还是会被污染的。