前言
前阵子看到新的微前端方案 无界,核心是用 iframe+WebComponent
的方式实现。感觉很有意思,用到 iframe
的话就需要解决下面几个问题
- 路由状态丢失,刷新一下,iframe 的 url 状态就丢失了
- dom 割裂严重,弹窗只能在 iframe 内部展示,无法覆盖全局
- web 应用之间通信非常困难
- 每次打开白屏时间太长,对于 SPA 应用来说无法接受
这几个问题都是各大微前端框架不用 iframe
的原因之一,虽然有天然的 js
css
隔离。
当前版本为 wujie@1.0.18
无界是如何渲染子应用
- 注册子应用信息,这个很好理解
- 创建子应用
JS
沙箱,通过主要依赖iframe
实现 - 创建
WebComponent
自定义元素wujie
装载Dom
- 运行
JS
脚本,渲染Dom
创建 JS 沙箱
JS
在 iframe
中运行,那肯定要先创建一个 iframe
容器。
class Wujie {
constructor() {
// 创建目标地址的解析,保证子应用与父应用是同域应用。来解决父子应用通信问题
const { urlElement, appHostPath, appRoutePath } = appRouteParse(url);
// 创建iframe
this.iframe = iframeGenerator(
this,
attrs,
mainHostPath,
appHostPath,
appRoutePath
);
}
}
function iframeGenerator(
sandbox: WuJie,
attrs: { [key: string]: any },
mainHostPath: string,
appHostPath: string,
appRoutePath: string
): HTMLIFrameElement {
const iframe = window.document.createElement("iframe");
const attrsMerge = {
src: mainHostPath,
style: "display: none",
...attrs,
name: sandbox.id,
[WUJIE_DATA_FLAG]: "",
};
setAttrsToElement(iframe, attrsMerge);
window.document.body.appendChild(iframe);
// 停止加载 iframe 得到一个纯净的iframe
sandbox.iframeReady = stopIframeLoading(iframeWindow).then(() => {});
return iframe;
}
通过设置 子应用 url 与父应用一致来解决通信问题。
Details
iframe 之间的通信
- 同源时可直接使用
window.parent|window.top
获取父应用信息,通过window.top === window.self
来判断自身是否为iframe
- 非同源时通过
possmessage
方法,以及window.addEventListener('message',()=>{})
但是这样也带来的另外一个问题,我本身是想要获取子应用的内容。现在src
设置成了父应用
的,那岂不是加载了父应用
内容? 这也很没必要,所以无界这边使用一个魔法来终止掉iframe
加载.
/**
* 防止运行主应用的js代码,给子应用带来很多副作用
*/
// TODO 更加准确抓取停止时机
function stopIframeLoading(iframeWindow: Window) {
const oldDoc = iframeWindow.document;
// 在一开始 oldDoc 是blank
// 加载html后就会变成具体的document对象
return new Promise<void>((resolve) => {
function loop() {
setTimeout(() => {
let newDoc;
try {
newDoc = iframeWindow.document;
} catch (err) {
newDoc = null;
}
// wait for document ready
if (!newDoc || newDoc == oldDoc) {
loop();
} else {
iframeWindow.stop
? iframeWindow.stop()
: iframeWindow.document.execCommand("Stop");
resolve();
}
}, 1);
}
loop();
});
}
window.stop() 方法的效果相当于点击了浏览器的停止按钮。由于脚本的加载顺序,该方法不能阻止已经包含在加载中的文档,但是它能够阻止图片、新窗口、和一些会延迟加载的对象的加载。
解析入口
Wujie
也是以 html
为入口,这个跟qiankun
或者说跟主流微前端框架是一致的,毕竟现在都是经过前端打包器打包出来的产物。
加载入口的方式有两个 startApp
运行无界 app 和 preloadApp
预加载无界 app 这两个api
中
export async function startApp(
startOptions: startOptions
): Promise<Function | void> {
// 实例化js沙箱
const newSandbox = new WuJie({
name,
url,
attrs,
degradeAttrs,
fiber,
degrade,
plugins,
lifecycles,
});
// 根据url请求对应的html文件
const { template, getExternalScripts, getExternalStyleSheets } =
await importHTML({
url,
html,
opts: {
fetch: fetch || window.fetch,
plugins: newSandbox.plugins,
loadError: newSandbox.lifecycles.loadError,
fiber,
},
});
}
在 importHTML
中会去分离 html
中的 css
脚本以及 script
脚本,得到纯 html
的文本。 就比如说有这样一段 html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>New Tab</title>
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="css/style.min.css" />
<style>
.bg {
background: red;
}
</style>
</head>
<body class="hide-overlay">
<div class="overlay loading-overlay"></div>
<div id="main-view" style="visibility: hidden">
<div class="backgrounds"></div>
<div class="dashboard"></div>
<div class="overlay drop-overlay">
<p>
Drop to upload backgrounds <span class="badge badge-plus">PLUS</span>
</p>
</div>
<div class="full-screen-portals"></div>
</div>
<script src="app/globals.js"></script>
<script src="js/lib.min.js" async></script>
<script src="app/app.min.js" defer></script>
</body>
</html>
经过 importHTML
处理后会得到 template
, getExternalScripts
, getExternalStyleSheets
其中 template
是去除 style
,css
,script
等标签后的纯html
内容
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>New Tab</title>
<meta name="viewport" content="width=device-width" />
</head>
<body class="hide-overlay">
<div class="overlay loading-overlay"></div>
<div id="main-view" style="visibility: hidden">
<div class="backgrounds"></div>
<div class="dashboard"></div>
<div class="overlay drop-overlay">
<p>
Drop to upload backgrounds <span class="badge badge-plus">PLUS</span>
</p>
</div>
<div class="full-screen-portals"></div>
</div>
</body>
</html>
getExternalScripts
获取文档中所有的内联script
具体格式如下
function getExternalScripts(): ScriptResultList {}
interface ScriptBaseObject {
/** 脚本地址,内联为空 */
src?: string;
/** 脚本是否为async执行 */
async?: boolean;
/** 脚本是否为defer执行 */
defer?: boolean;
/** 脚本是否为module模块 */
module?: boolean;
/** 脚本是否设置crossorigin */
crossorigin?: boolean;
/** 脚本crossorigin的类型 */
crossoriginType?: "anonymous" | "use-credentials" | "";
/** 脚本正则匹配属性 */
attrs?: ScriptAttributes;
}
type ScriptObject = ScriptBaseObject & {
/** 内联script的代码 */
content?: string;
/** 忽略,子应用自行请求 */
ignore?: boolean;
/** 子应用加载完毕事件 */
onload?: Function;
};
type ScriptResultList = (ScriptBaseObject & {
// 获取 script code 的Promise
contentPromise: Promise<string>;
})[];
getExternalStyleSheets
获取文档中所有样式(不包括内联样式)
function getExternalStyleSheets(): StyleResultList {}
interface StyleObject {
/** 样式地址, 内联为空 */
src?: string;
/** 样式代码 */
content?: string;
/** 忽略,子应用自行请求 */
ignore?: boolean;
}
type StyleResultList = {
src: string;
// 获取 样式内容的promise
contentPromise: Promise<string>;
ignore?: boolean;
}[];
为什么 wujie 要花这么大力气分离出 template,script 以及 style
因为 wujie
需要将 script
放入 iframe
中执行,将 template
,style
放入 webComponent
自定义元素中,所以将他们分离出来。
wujie 的解析与 qiankun 的有何不同
qiankun
自己实现了 import-html-entry
来解析 html
内容,而 wujie
则是实现在 importHtml
中。其实两者思路上都大体一致利用正则匹配出各类标签,然后再去解析这类标签的 attrs
.总体来说 wujie
的解析颗粒度更加细。
挂载 DOM 以及 style
上一步我们以及拿到模板中分离出来的 template
和 获取样式表具体内容的方法 getExternalStyleSheets
。
至于为什么要先挂载 Dom
有两点考虑
- 大部分的 UI 框架都需要有一个根节点才能挂载
- 后续沙箱中会劫持 dom
wujie
中选择将 子应用的 dom
塞入自定义元素 wujie-app
中,这样可以天然利用 webComponent
带来的 样式隔离 ,这也是与 qiankun
最大的区别。
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);
}
}
将 html
模板和 样式内容进行合并,插入 自定义元素中
这段逻辑在 Wujie.active
中
// packages\wujie-core\src\sandbox.ts
class Wujie {
async active() {
// 预执行无容器,暂时插入iframe内部触发Web Component的connect
const iframeBody = rawDocumentQuerySelector.call(
iframeWindow.document,
"body"
) as HTMLElement;
this.el = renderElementToContainer(
createWujieWebComponent(this.id),
el ?? iframeBody
);
// 注意这个 iframeWindow 还是那个纯净的沙箱
await renderTemplateToShadowRoot(
this.shadowRoot,
iframeWindow,
this.template
);
}
}
这样子就把解析出来的 样式和模板插入到自定义元素wujie-app
中,当然这里面还有其他一些优化逻辑这里就先不分析。
执行沙箱脚本
上一小节以及挂载Dom
了,接下来就可以执行 js
https://zhuanlan.zhihu.com/p/442815952