wujie沙箱解析
前言
在上一篇文章中我们分析了wujie-core
的实现,核心是创建沙箱、激活沙箱、运行沙箱三个步骤, 本文将对创建沙箱环节做深入的剖析。
沙箱实现原理
- js沙箱: 先抛结论,
wujie-core
的js沙箱实现原理基于iframe来实现的
将子应用的js注入主应用同域的iframe中运行,iframe是一个原生的window沙箱, 内部有完整的history和location接口,子应用实例instance运行在iframe中, 路由也彻底和主应用解耦,可以直接在业务组件里面启动应用。
- css沙箱:
wujie-core
的css沙箱实现原理基于webcomponent
来实现的
无界采用webcomponent来实现页面的样式隔离,无界会创建一个wujie自定义元素, 然后将子应用的完整结构渲染在内部
- iframe与webcomponent连接机制
无界通过代理 iframe
的document
到webcomponent
,从而实现两者的互联
下面我们通过代码来分析wujie-core
沙箱的实现
沙箱创建
当我们通过new Wujie()
创建一个沙箱时,会执行Wujie
的构造函数
js
export default class Wujie {
constructor(options: {
name: string;
url: string;
attrs: { [key: string]: any };
degradeAttrs: { [key: string]: any };
fiber: boolean;
degrade;
plugins: Array<plugin>;
lifecycles: lifecycles;
}) {
// 传递inject给嵌套子应用
if (window.__POWERED_BY_WUJIE__) this.inject = window.__WUJIE.inject;
else {
this.inject = {
idToSandboxMap: idToSandboxCacheMap,
appEventObjMap,
mainHostPath: window.location.protocol + "//" + window.location.host,
};
}
const { name, url, attrs, fiber, degradeAttrs, degrade, lifecycles, plugins } = options;
this.id = name;
this.fiber = fiber;
this.degrade = degrade || !wujieSupport;
this.bus = new EventBus(this.id);
this.url = url;
this.degradeAttrs = degradeAttrs;
this.provide = { bus: this.bus };
this.styleSheetElements = [];
this.execQueue = [];
this.lifecycles = lifecycles;
this.plugins = getPlugins(plugins);
// 创建目标地址的解析
const { urlElement, appHostPath, appRoutePath } = appRouteParse(url);
const { mainHostPath } = this.inject;
// 创建iframe
this.iframe = iframeGenerator(this, attrs, mainHostPath, appHostPath, appRoutePath);
if (this.degrade) {
// ...somthing code
} else {
const { proxyWindow, proxyDocument, proxyLocation } = proxyGenerator(
this.iframe,
urlElement,
mainHostPath,
appHostPath
);
this.proxy = proxyWindow;
this.proxyDocument = proxyDocument;
this.proxyLocation = proxyLocation;
}
this.provide.location = this.proxyLocation;
addSandboxCacheWithWujie(this.id, this);
}
}
在构造函数中,我们可以看到Wujie
的构造函数主要做了以下4件事情:
- 初始化赋值,例如:
id
、fiber
、degrade
、bus
、url
、degradeAttrs
、provide
、styleSheetElements
、execQueue
、lifecycles
、plugins
等 - 创建iframe,并且将iframe挂到无界实例上
- 创建代理,并且将代理挂到无界实例上
- 将沙箱实例添加到缓存中
我们放一个流程图如下:
在以上四步中,我们重点看下创建iframe和创建代理两个步骤
创建iframe
iframe
是通过iframeGenerator
函数来创建的,函数的实现如下;
js
export function iframeGenerator(
sandbox: WuJie,
attrs: { [key: string]: any },
mainHostPath: string,
appHostPath: string,
appRoutePath: string
): HTMLIFrameElement {
// 创建iframe元素
const iframe = window.document.createElement("iframe");
const attrsMerge = { src: mainHostPath, style: "display: none", ...attrs, name: sandbox.id, [WUJIE_DATA_FLAG]: "" };
// 将属性设置到iframe上
setAttrsToElement(iframe, attrsMerge);
// 将iframe添加到body中,iframe为隐藏状态
window.document.body.appendChild(iframe);
const iframeWindow = iframe.contentWindow;
// 变量需要提前注入,在入口函数通过变量防止死循环
// 将变量注入到iframe中
patchIframeVariable(iframeWindow, sandbox, appHostPath);
sandbox.iframeReady = stopIframeLoading(iframeWindow).then(() => {
if (!iframeWindow.__WUJIE) {
patchIframeVariable(iframeWindow, sandbox, appHostPath);
}
initIframeDom(iframeWindow, sandbox, mainHostPath, appHostPath);
/**
* 如果有同步优先同步,非同步从url读取
*/
if (!isMatchSyncQueryById(iframeWindow.__WUJIE.id)) {
iframeWindow.history.replaceState(null, "", mainHostPath + appRoutePath);
}
});
return iframe;
}
在iframeGenerator
函数中,主要做了以下几件事情:
- 创建iframe元素
- 将属性设置到iframe上,iframe和主应用同域,且为隐藏状态(display:none)
- 将iframe插入到body中
- 将变量注入到iframe中,这里重点关注下,注入了沙箱实例
__WUJIE
、子应用域名__WUJIE_PUBLIC_PATH__
,源码如下:
ts
function patchIframeVariable(iframeWindow: Window, wujie: WuJie, appHostPath: string): void {
iframeWindow.__WUJIE = wujie;
iframeWindow.__WUJIE_PUBLIC_PATH__ = appHostPath + "/";
iframeWindow.$wujie = wujie.provide;
iframeWindow.__WUJIE_RAW_WINDOW__ = iframeWindow;
}
- 创建一个promise,在iframe加载完成后,初始化iframe的dom结构,这块我们后期在沙箱激活的时候再分析
- 最后返回iframe元素
沙箱代理
在创建iframe后,需要将iframe
的window
、document
、location
等对象代理,下面看下整体的代码:
ts
export function proxyGenerator(
iframe: HTMLIFrameElement,
urlElement: HTMLAnchorElement,
mainHostPath: string,
appHostPath: string
): {
proxyWindow: Window;
proxyDocument: Object;
proxyLocation: Object;
} {
const proxyWindow = new Proxy(iframe.contentWindow, {
get: (target: Window, p: PropertyKey): any => {
return getTargetValue(target, p);
},
set: (target: Window, p: PropertyKey, value: any) => {
target[p] = value;
return true;
}
});
// proxy document
const proxyDocument = new Proxy(
{},
{
get: function (_fakeDocument, propKey) {
const document = window.document;
const { shadowRoot, proxyLocation } = iframe.contentWindow.__WUJIE;
const rawCreateElement = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_ELEMENT__;
const rawCreateTextNode = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_TEXT_NODE__;
// 劫持对document的操作,使之作用于shadowRoot上
// other proxy code
if (propKey === "getElementById") {
return new Proxy(shadowRoot.querySelector, {
apply(target, ctx, args) {
// other code
target.call(shadowRoot, `[id="${args[0]}"]`)
// other code
},
});
}
// other proxy code
if (propKey === "forms") return shadowRoot.querySelectorAll("form");
if (propKey === "images") return shadowRoot.querySelectorAll("img");
if (propKey === "links") return shadowRoot.querySelectorAll("a");
// other code
},
}
);
// proxy location
const proxyLocation = new Proxy(
{},
{
get: function (_fakeLocation, propKey) {
const location = iframe.contentWindow.location;
// proxy location的 href host 等属性
// 返回location的属性
return getTargetValue(location, propKey);
},
set: function (_fakeLocation, propKey, value) {
iframe.contentWindow.location[propKey] = value;
return true;
}
}
);
return { proxyWindow, proxyDocument, proxyLocation };
}
从中可以看到,proxyGenerator
函数主要做了以下几件事情:
- 代理
iframe
的window
对象,通过proxyWindow
变量来引用 - 代理
iframe
的document
对象,通过proxyDocument
变量来引用 - 代理
iframe
的location
对象,通过proxyLocation
变量来引用
下面我们展开来看下三个代理的实现原理
proxyWindow
proxyWindow
是用来代理iframe
的window
对象,源码如下:
ts
const proxyWindow = new Proxy(iframe.contentWindow, {
get: (target: Window, p: PropertyKey): any => {
// 劫持location
// 对自身的处理 window self
// 修正this指针指向
return getTargetValue(target, p);
},
set: (target: Window, p: PropertyKey, value: any) => {
target[p] = value;
return true;
}
});
上面的代码中已经精简,通过new Proxy
来创建一个代理对象来代理iframe
的window
,并且添加了get
和set
方法,get
方法主要是用来获取子应用window
的属性,set
方法主要是用来设置window
的属性,源码中还做了一些边界处理,我们暂时不做分析, 只需知道对于子应用window的操作大部分最终还是作用于iframe
的window
上即可。
proxyDocument
proxyDocument
是用来代理iframe
的document
对象,使的对于document的操作大部分都基于子应用(部分方法会从主应用获取),源码如下:
ts
const proxyDocument = new Proxy(
{},
{
get: function (_fakeDocument, propKey) {
const document = window.document;
const { shadowRoot, proxyLocation } = iframe.contentWindow.__WUJIE;
const rawCreateElement = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_ELEMENT__;
const rawCreateTextNode = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_TEXT_NODE__;
// 劫持对document的操作,使之作用于shadowRoot上
// other proxy code
if (propKey === "getElementById") {
return new Proxy(shadowRoot.querySelector, {
apply(target, ctx, args) {
// other code
target.call(shadowRoot, `[id="${args[0]}"]`)
// other code
},
});
}
// other proxy code
if (propKey === "forms") return shadowRoot.querySelectorAll("form");
if (propKey === "images") return shadowRoot.querySelectorAll("img");
if (propKey === "links") return shadowRoot.querySelectorAll("a");
// other code
},
}
);
从上面的代码中可以看出,proxyDocument
主要做了以下几件事情:
- 劫持
document
上的部分方法,例如:getElementById
、getElementsByTagName
、getElementsByClassName
、getElementsByName
、querySelector
、querySelectorAll
等,使之作用于shadowRoot
根节点进行检索, - 劫持
document
上的部分属性,例如:forms
、images
、links
等,使之基于shadowRoot
根节点进行检索 - 以上是从子应用获取的方法和属性,还有一些是从主应用获取的方法和属性,这里不过多赘婿
proxyLocation
proxyLocation
是用来代理iframe
的location
对象,使得我们对于location的操作基本上是基于子应用的,源码如下:
ts
// proxy location
const proxyLocation = new Proxy(
{},
{
get: function (_fakeLocation, propKey) {
const location = iframe.contentWindow.location;
// proxy location的 href host 等属性
// 修正this 返回location的属性
return getTargetValue(location, propKey);
},
set: function (_fakeLocation, propKey, value) {
iframe.contentWindow.location[propKey] = value;
return true;
}
}
);
同理,proxyLocation
主要做了以下几件事情:
- 劫持
location
上的部分方法,例如:host
、hostname
、port
、protocol
、origin
等,这些属性的取值,都是基于urlElement
的,urlElement(baseUrl为父应用:http://localhost:8000)是子应用的url(http://localhost:7200)解析后的对象%25E6%2598%25AF%25E5%25AD%2590%25E5%25BA%2594%25E7%2594%25A8%25E7%259A%2584url(http%3A%2F%2Flocalhost%3A7200)%25E8%25A7%25A3%25E6%259E%2590%25E5%2590%258E%25E7%259A%2584%25E5%25AF%25B9%25E8%25B1%25A1 "http://localhost:8000)%E6%98%AF%E5%AD%90%E5%BA%94%E7%94%A8%E7%9A%84url(http://localhost:7200)%E8%A7%A3%E6%9E%90%E5%90%8E%E7%9A%84%E5%AF%B9%E8%B1%A1") - 其余的方法和属性,都是基于
iframe
的location
对象的,这里不过多赘述 - 对于
location
的修改,也都是基于iframe
的location
对象进行修改
总结
在创建无界实例的时候,第一步是创建沙箱,而创建沙箱有两个关键步骤:
- 创建iframe,并挂到实例上
- 对iframe的
window
、document
、location
等对象进行代理,挂到实例上,除少部分属性和方法需要从主应用获取外,大部分都是从子应用获取的
完成创建iframe和代理后,无界实例就创建完成了。顺带提一下,我们在代码中访问window
、document
、location
等对象时,是如何访问到代理对象上的呢?这里抛出一段无界中的代码,大家可以思考下:
ts
if (!iframeWindow.__WUJIE.degrade && !module) {
code = `(function(window, self, global, location) {
${code}
}).bind(window.__WUJIE.proxy)(
window.__WUJIE.proxy,
window.__WUJIE.proxy,
window.__WUJIE.proxy,
window.__WUJIE.proxyLocation,
);`;
}
下一期我们将分析wujie-core
的沙箱激活机制,敬请期待!
如果觉得本文有帮助 记得点赞三连哦 十分感谢!