无界沙箱激活
前言
在上一篇文章我们分析了无界沙箱的创建过程,在创建过程中主要是创建了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的沙箱运行机制,敬请期待!
如果觉得本文有帮助 记得点赞三连哦 十分感谢!