无界沙箱激活
前言
在上一篇文章我们分析了无界沙箱的创建过程,在创建过程中主要是创建了iframe
以及对iframe
的window
、document
、location
等进行了代理,
到目前为止,我们的准备工作已经做完了,有了一个子应用的壳子iframe
,但是子应用还没有真正的运行起来,在无界源码中,有一个子应用的激活过程,我们今天看下激活过程。
子应用静态资源获取
在激活子应用之前,我们需要先获取子应用的静态资源,代码如下:
ts
const { template, getExternalScripts, getExternalStyleSheets } = await importHTML({
url,
html,
opts: {
fetch: fetch || window.fetch,
plugins: newSandbox.plugins,
loadError: newSandbox.lifecycles.loadError,
fiber,
},
});
importHTML
函数用来获取子应用的静态资源,template
为子应用的html
模板,getExternalScripts
为子应用的js
资源,getExternalStyleSheets
为子应用的css
资源, 用官方例子,我们可以看到template
为:
html
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="favicon.ico">
<title>vue2</title>
<!-- prefetch/preload/modulepreload link js/Page1.js replaced by wujie --><!-- prefetch/preload/modulepreload link js/Page2.js replaced by wujie --><!-- prefetch/preload/modulepreload link js/Page3.js replaced by wujie --><!-- prefetch/preload/modulepreload link js/app.js replaced by wujie --><!-- prefetch/preload/modulepreload link js/chunk-vendors.js replaced by wujie --></head>
<body>
<noscript>
<strong>We're sorry but vue2 doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- script http://localhost:7200/js/chunk-vendors.js replaced by wujie --><!-- script http://localhost:7200/js/app.js replaced by wujie --></body>
</html>
其中有一个细节点,是子应用的js
资源被注释了,之所以这样处理,是要让子应用的js
资源不被浏览器加载到webComponent中,而是由无界来加载到iframe
中,被注释掉的js资源通过getExternalScripts
可以获取到,最终通过getExternalScripts
返回的js资源结果如下:
ts
[
{ src: 'http://localhost:7200/js/chunk-vendors.js' },
{ src: 'http://localhost:7200/js/app.js' }
]
这里我们先有个大致的了解,方便后面分析。至于importHTML
函数的实现,大家有兴趣可以自己看下,这里不是我们的重点。
小结: 在激活子应用之前,我们需要先获取子应用的静态资源,包括
html
模板、js
资源、css
资源等,其中js、css外链资源被注释掉,后续可以通过提供的函数获取到。
激活子应用
子应用的资源准备好后,源码中还有一个处理processCssLoader
函数,用来处理css
资源,这里我们先跳过,大家可以把processedHtml
等价于我们刚刚获取的子应用的template
:
ts
const processedHtml = await processCssLoader(newSandbox, template, getExternalStyleSheets);
await newSandbox.active({ url, sync, prefix, template: processedHtml, el, props, alive, fetch, replace });
那么加下来,我们就来分析active
函数,active
函数的实现如下:
ts
export default class Wujie {
public async active(options: {
url: string;
sync?: boolean;
prefix?: { [key: string]: string };
template?: string;
el?: string | HTMLElement;
props?: { [key: string]: any };
alive?: boolean;
fetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
replace?: (code: string) => string;
}): Promise<void> {
const { sync, url, el, template, props, alive, prefix, fetch, replace } = options;
this.url = url;
this.sync = sync;
this.alive = alive;
this.hrefFlag = false;
this.prefix = prefix ?? this.prefix;
this.replace = replace ?? this.replace;
this.provide.props = props ?? this.provide.props;
this.activeFlag = true;
// wait iframe init
await this.iframeReady;
// 处理子应用自定义fetch,暂时不分析
// 处理子应用路由同步
// 先将url同步回iframe,然后再同步回浏览器url
syncUrlToIframe(iframeWindow);
syncUrlToWindow(iframeWindow);
// inject template
this.template = template ?? this.template;
if (this.shadowRoot) {
// ...
} else {
// 预执行无容器,暂时插入iframe内部触发Web Component的connect
const iframeBody = rawDocumentQuerySelector.call(iframeWindow.document, "body") as HTMLElement;
this.el = renderElementToContainer(createWujieWebComponent(this.id), el ?? iframeBody);
}
await renderTemplateToShadowRoot(this.shadowRoot, iframeWindow, this.template);
this.patchCssRules();
// inject shadowRoot to app
this.provide.shadowRoot = this.shadowRoot;
}
}
激活步骤可以分为以下几步:
- 等待
iframe
初始化完成,初始化阶段首先将iframe
的dom结构做了基本的填充,然后对iframe
做了一些patch处理,这里我们不做展开分析,有兴趣的可以去看看initIframeDom
这个函数,我们罗列两个重要的操作,比如- 重写iframe的history的pushState和replaceState方法,同步路由到主应用
- 监听hashchange和popstate事件,同步路由到主应用
- 处理子应用路由同步,首先是将url同步到
iframe
,然后再同步回浏览器url,这里的逻辑,以后有机会再看 - 创建自定义组件,并插入到子应用的容器中,这里我们通过代码来看下:
ts
this.el = renderElementToContainer(createWujieWebComponent(this.id), el ?? iframeBody);
export function createWujieWebComponent(id: string): HTMLElement {
const contentElement = window.document.createElement("wujie-app");
contentElement.setAttribute(WUJIE_APP_ID, id);
contentElement.classList.add(WUJIE_IFRAME_CLASS);
return contentElement;
}
export function renderElementToContainer(
element: Element | ChildNode,
selectorOrElement: string | HTMLElement
): HTMLElement {
// 代码有删减,这里只看核心逻辑
const container = getContainer(selectorOrElement);
rawElementAppendChild.call(container, element);
return container;
}
这段代码的目的是要创建一个webComponent
,并插入到子应用容器中,创建的过程比较简单,插入的时候要找到容器,然后插入到容器中,那么容器是什么呢,还记得我们在主应用中,承载子应用使用的<WujieVue>
吗,这个组件的根节点就是我们的容器,我们在回顾下<WujieVue>
的组件定义:
js
render(c) {
return c("div", {
style: {
width: this.width,
height: this.height,
},
ref: "wujie",
});
}
如上代码所示,这里的div就是我们的容器,那么我们的webComponent
就是插入到这个容器中
- 将子应用的
html
模板渲染到webComponent
中 将子应用的html
模板渲染到webComponent
是通过renderTemplateToShadowRoot
函数实现的:
ts
await renderTemplateToShadowRoot(this.shadowRoot, iframeWindow, this.template);
函数的第一个入参是shadowRoot
,那么shadowRoot是什么呢,什么时候创建的呢? 我们把时间线拨回到上一步创建webComponent
前,我们通过判断if (this.shadowRoot)
来创建并插入webComponent
,当我们执行到renderTemplateToShadowRoot
的时候, shadowRoot
已经存在了,这是因为在创建并插入webComponent
的时候,触发了webComponent
的connectedCallback
生命周期,我们来看下具体的实现:
ts
export function defineWujieWebComponent() {
const customElements = window.customElements;
if (customElements && !customElements?.get("wujie-app")) {
class WujieApp extends HTMLElement {
connectedCallback(): void {
if (this.shadowRoot) return;
const shadowRoot = this.attachShadow({ mode: "open" });
const sandbox = getWujieById(this.getAttribute(WUJIE_APP_ID));
patchElementEffect(shadowRoot, sandbox.iframe.contentWindow);
sandbox.shadowRoot = shadowRoot;
}
disconnectedCallback(): void {
const sandbox = getWujieById(this.getAttribute(WUJIE_APP_ID));
sandbox?.unmount();
}
}
customElements?.define("wujie-app", WujieApp);
}
}
关于webComponent
的生命周期,如果有不了解的可以自行查阅,这里不做展开,我们看关键步骤
ts
sandbox.shadowRoot = shadowRoot;
在connectedCallback
生命周期中,我们将shadowRoot
赋值给了sandbox
,这样我们就可以在renderTemplateToShadowRoot
中使用shadowRoot
了, 这里可能有同学会好奇什么时候定义的自定义组件呢,其实defineWujieWebComponent
函数在wujie-core
的入口文件index.js
中已经被执行过了,换言之我们在主项目中通过import WujieVue from "wujie-vue2";
引入无界时,已经被调用了
ts
// index.js
// 定义webComponent容器
defineWujieWebComponent()
搞清楚这些以后,我们回过头继续看下renderTemplateToShadowRoot
如何将子应用的模板渲染到webComponent
中的:
ts
export async function renderTemplateToShadowRoot(
shadowRoot: ShadowRoot,
iframeWindow: Window,
template: string
): Promise<void> {
// 将子应用的html模板渲染为dom元素
const html = renderTemplateToHtml(iframeWindow, template);
// other code
// change ownerDocumen
// 将子应用的html插入到shadowRoot中
shadowRoot.appendChild(processedHtml);
const shade = document.createElement("div");
shade.setAttribute("style", WUJIE_SHADE_STYLE);
processedHtml.insertBefore(shade, processedHtml.firstChild);
shadowRoot.head = shadowRoot.querySelector("head");
shadowRoot.body = shadowRoot.querySelector("body");
// 修复 html parentNode
Object.defineProperty(shadowRoot.firstChild, "parentNode", {
enumerable: true,
configurable: true,
get: () => iframeWindow.document,
});
// 修复工作
patchRenderEffect(shadowRoot, iframeWindow.__WUJIE.id, false);
}
renderTemplateToShadowRoot
主要做了以下三件事:
- 将子应用的模板字符串
template
渲染为dom
元素 - 将子应用的dom元素插入到
shadowRoot
中,shadowRoot
就是我们上一步创建的webComponent
的shadowRoot
- 其他工作,这里我们不做展开
小结:激活子应用的过程,主要是将子应用的模板渲染到
webComponent
中,这里的webComponent
就是我们在主应用中承载子应用的<WujieVue>
组件
总结
子应用的激活过程我们分析完成了,这里我们简单总结下:
- 激活子应用之前,需要先获取子应用的静态资源,包括
html
模板、js
资源、css
资源等,其中js、css外链资源被注释掉,后续可以通过提供的函数获取到。 - 激活子应用的过程,主要是将子应用的模板渲染到
shadowRoot
中,shadowRoot
又插入到容器中,容器就是就是我们在主应用中承载子应用的<WujieVue>
组件
经过以上工作,我们的子应用的激活过程就完成了。
下一期我们将分析wujie-core
的沙箱运行机制,敬请期待!
如果觉得本文有帮助 记得点赞三连哦 十分感谢!