1:如何配置进行控制
registerMicroApps方法里面调用loadApp会传入frameworkConfiguration, 这个对象是乾坤框架对外提供的api用于控制应用加载,切换,沙箱等行为
loadMicroApp方法参数会传入configuration?: FrameworkConfiguration
FrameworkConfiguration 里面可配置的属性如下:
export let frameworkConfiguration: FrameworkConfiguration = {};
ts: FrameworkConfiguration = QiankunSpecialOpts & ImportEntryOpts & StartOpts;
QiankunSpecialOpts :
{
prefetch
sandbox?:
| boolean
| {
strictStyleIsolation?: boolean;
experimentalStyleIsolation?: boolean;
patchers?: Patcher[];
};
/*
with singular mode, any app will wait to load until other apps are unmouting
it is useful for the scenario that only one sub app shown at one time
*/
singular?: boolean | ((app: LoadableApp) => Promise);
excludeAssetFilter?: (url: string) => boolean; // skip some scripts or links intercept, like JSONP
}
ImportEntryOpts :
{
fetch?: typeof window.fetch;
getPublicPath?: (entry: Entry) => string;
getTemplate?: (tpl: string) => string;
}
StartOpts:
{
urlRerouteOnly?: boolean;
}
2:在什么时候,什么地方调用运行
loader.ts 里面 加载完子应用方法 里面
export async function loadApp(
app: LoadableApp,
configuration: FrameworkConfiguration = {},
lifeCycles?: FrameworkLifeCycles,
): Promise {
.......
const { singular = false, sandbox = true, excludeAssetFilter, ...importEntryOpts } = configuration;
// get the entry html content and script executor
// 核心代码,加载app对应的入口文件,以及将文件处理成模板,和可执行脚本信息对象
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
......
// 根据appContent 返回对应的对象,这里dom对象里面是有沙箱机制的
let element: HTMLElement | null = createElement(appContent, strictStyleIsolation);
.......
// 创建沙箱
if (sandbox) {
const sandboxInstance = createSandbox(
appName,
containerGetter,
Boolean(singular),
enableScopedCSS,
excludeAssetFilter,
);
// 用沙箱的代理对象作为接下来使用的全局对象
global = sandboxInstance.proxy as typeof window;
}
// 执行子应用的脚本
const scriptExports: any = await execScripts(global, !singular);
...........
return parcelConfig;
}
在createElement里面, 利用shadow dom 构造独立的代码片段,类型iframe
// 创建元素,appContent 为里面的内容,strictStyleIsolation与沙箱有关
// 根据appContent 返回对应的对象,这里dom对象里面是有沙箱机制的
function createElement(appContent: string, strictStyleIsolation: boolean): HTMLElement {
.........
// 如果元素支持沙箱,否则创建沙箱
if (appElement.attachShadow) {
shadow = appElement.attachShadow({ mode: 'open' });
} else {
// createShadowRoot was proposed in initial spec, which has then been deprecated
shadow = (appElement as any).createShadowRoot();
}
shadow.innerHTML = innerHTML;
..........
return appElement;
}
shadow dom 会隔离css以及利用document不会找到里面的dom,但是对于js脚本而已并没有做到绝对的隔离,比如window.setInterval等里面的方法以及属性还是会与外界相互影响,此时乾坤框架createSandbox实现了对脚本的隔离。
沙箱分为3种:
1:singular=true,如果是单一应用切换则用LegacySandbox,
2:singular=false, 如果一个页面包含多个子应用则用ProxySandbox
3:如果浏览器不支持window.Proxy,则兼容用SnapshotSandbox
ProxySandbox - 多子应用情况
1: fakewindow + window的组合,每次new ProxySandbox() 会创建fakewindow实例作为proxy
2:set时值放到fakewindow里面,get时先从fakewindow里面取,取不到就到window里面取
2:判断是特殊属性比如不可配置,编辑,修改的属性,就直接从window里面取
export default class ProxySandbox implements SandBox {
/** window 值变更记录 */
private updatedValueSet = new Set();
name: string;
type: SandBoxType;
proxy: WindowProxy;
sandboxRunning = true; // 沙箱状态
active() {
// 记录激活的沙箱数量
if (!this.sandboxRunning) activeSandboxCount++;
this.sandboxRunning = true;
}
inactive() {
.........
this.sandboxRunning = false;
}
constructor(name: string) {
// 变量配置,这里rowWindow = window
.........
// 将不可编辑的特殊属性记录到propertiesWithGetter
const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow);
.........
const proxy = new Proxy(fakeWindow, {
set(target: FakeWindow, p: PropertyKey, value: any): boolean {
// 如果本实例的沙箱正在运行
if (self.sandboxRunning) {
.........
// @ts-ignore
target[p] = value;
// 记录修改的值
updatedValueSet.add(p);
.........
}
.........
// 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
return true;
},
get(target: FakeWindow, p: PropertyKey): any {
// 一些特殊属性,如[window,self,top,hasOwnProperty,document] 的特殊处理以及返回
.........
// eslint-disable-next-line no-bitwise
// 有getter的属性,直接访问window.p, 否则访问fakewindow.p
// 如果没有不可编辑且具有getter的属性,就先从fakewindow里面取,取不到就从window里面取
const value = propertiesWithGetter.has(p) ? (rawWindow as any)[p] : (target as any)[p] || (rawWindow as any)[p];
return getTargetValue(rawWindow, value);
},
// trap in operator
// see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12
has() {},
// 获取 FakeWindow || window 里面的自有属性
getOwnPropertyDescriptor() {}
// trap to support iterator with sandbox
// FakeWindow + window 里面的不重复的属性canvcat
ownKeys() {}
// 首先看这个属性是从哪来的,从window里面来的就在window定义
defineProperty() {},
// 这里只删除 fakeWindow里面的属性
deleteProperty() {},
});
this.proxy = proxy;
}
}
图示如下:
LegacySandbox - 单应用的情况,之后会使用ProxySandbox替代
/**
* 基于 Proxy 实现的沙箱
* TODO: 为了兼容性 singular 模式下依旧使用该沙箱,等新沙箱稳定之后再切换
* 生成一个代替window对象的委托,set,get时实际操作的window对象属性同时记录操作行为,active,inactive时释放操作行为使window对象还原
*/
export default class SingularProxySandbox implements SandBox {
/** 沙箱期间新增的全局变量 */
private addedPropsMapInSandbox = new Map();
/** 沙箱期间更新的全局变量 */
private modifiedPropsOriginalValueMapInSandbox = new Map();
/** 持续记录更新的(新增和修改的)全局变量的 map,用于在任意时刻做 snapshot */
private currentUpdatedPropsValueMap = new Map();
.......
active() {
.......
// 根据之前修改的记录重新修改window的属性,即还原沙箱之前的状态
this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v));
.......
}
inactive() {
.......
// renderSandboxSnapshot = snapshot(currentUpdatedPropsValueMapForSnapshot);
// restore global props to initial snapshot
// 将沙箱期间修改的属性还原为原先的属性
this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v));
// 将沙箱期间新增的全局变量消除
this.addedPropsMapInSandbox.forEach((_, p) => setWindowProp(p, undefined, true));
this.sandboxRunning = false;
}
constructor(name: string) {
.......
const proxy = new Proxy(fakeWindow, {
// 在fakeWindow.p = v 执行前,会将p,v增加/编辑队列记录
set(_: Window, p: PropertyKey, value: any): boolean {
.......
// 新增p属性放入新增队列
addedPropsMapInSandbox.set(p, value);
.......
.......
// 如果当前 window 对象存在该属性,且 record map 中未记录过,则记录该属性初始值
const originalValue = (rawWindow as any)[p];
// 记录原始属性
modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
// 记录修改属性以及修改后的值
currentUpdatedPropsValueMap.set(p, value);
// 设置值
(rawWindow as any)[p] = value;
.......
// 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
return true;
},
get(_: Window, p: PropertyKey): any {
// 特殊属性处理
.......
const value = (rawWindow as any)[p];
return getTargetValue(rawWindow, value);
},
.......
},
});
this.proxy = proxy;
}
}
SnapshotSandbox- 不兼容window.Proxy的情况
基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器
- 代理实质为window,get,set,修改的是window的属性,但是active的时候,会生成window的快照,inactive的时候会根据快照还原
3:有什么用处,出于什么原因要设计沙箱机制
页面上多个子应用会造成 全局变量等Js冲突,Css&DOM冲突:样式文件相互影响,dom可能带有相同的class,id造成选中困难
Css&DOM冲突 可以用shadow dom来解决,但是js目前只能使用ProxySandbox脚本hack
5:流程图+结构图
沙箱补丁
启动阶段补丁:patchAtBootstrapping,生成沙箱createSandbox()的时候执行,在loadApp()加载应用文件,生成shadow dom后执行,之后才是导出并执行应用的启动阶段
挂载阶段补丁:生成沙箱createSandbox()的时候导出patchAtMounting,在应用的mount阶段执行
patchAtBootstrapping 主要是对dom的创建,插入,移除等原生方法进行了一层封装,主要有插入style后的css的样式生效以及scoped的隔离逻辑,插入script后自动执行脚本功能逻辑
const basePatchers = [
() => patchDynamicAppend(false),
]
// 执行并返回资源释放,原生方法还原的接口
return basePatchers .map(patch => patch());
patchAtMounting 除了上述patchAtBootstrapping的功能外,对Interval,addEventListener,historyListener等方法的封装
const basePatchers = [
() => patchInterval(sandbox.proxy),
() => patchWindowListener(sandbox.proxy),
() => patchHistoryListener(),
() => patchDynamicAppend(true),
]
// 执行并返回资源释放,原生方法还原的接口
return basePatchers .map(patch => patch());
调用时机:
loadApp() {
.......
// 创建沙箱
if (sandbox) {
const sandboxInstance = createSandbox(...); // 里面已经执行了 patchAtBootstrapping
// 用沙箱的代理对象作为接下来使用的全局对象
global = sandboxInstance.proxy as typeof window;
mountSandbox = sandboxInstance.mount;
unmountSandbox = sandboxInstance.unmount;
}
.......
// 返回封装好的生命周期钩子
return {
bootstrap: [...],
mount: [..., mountSandbox, ...],
unmount: [..., unmountSandbox, ...]
}
}
补丁在沙箱中如何执行:
createSandbox(...) {
sandbox = [LegacySandbox, ProxySandbox, SnapshotSandbox] // 根据条件选择里面一种
// 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()
return {
proxy: sandbox.proxy,
mount() {
sandbox.active();
// sideEffectsRebuilders 包含启动阶段的rebuild,和mount阶段的rebuld,将其拆分出来
// 启动阶段的rebuild执行
const satb = sideEffectsRebuilders.slice(0, bootstrappingFreers.length);
satb.forEach(rebuild => rebuild());
// 返回挂载阶段的释放还原的接口
mountingFreers = patchAtMounting()
// mount阶段的rebuld
const satm = sideEffectsRebuilders.slice(bootstrappingFreers.length);
satm.forEach(rebuild => rebuild());
},
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
// 启动阶段,挂载阶段的所有释放资源,同时返回rebuild列表并记录下来,下次挂载用
sideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map(free => free());
sandbox.inactive();
}
}
}
dom操作的封装 :patchDynamicAppend
patchDynamicAppend 是 patchAtBootstrapping 和 patchAtMounting的核心方法
这里有一个场景,微应用加载后,执行微应用里面的脚本,我们知道由于沙盒的设置,里面的全局变量访问以proxy的方式进行,但是通过生成`], proxy, { strictGlobal: !singular, success: element.onload, error: element.onerror, }); // 将注释插入到mountDOM中 const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun'); return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode); } default: break; } } // refChild为null,则为appendChild, 否则为insertBefore return rawDOMAppendOrInsertBefore.call(this, element, refChild); }; }