由无界启发的一种前端沙箱实现
整体思路实现源码:https://github.com/Jmingzi/sandbox/blob/main/src/Sandbox/index.tsx
概述
本文讨论并实现了一种基于 iframe 的沙箱方案——不是无界的实现,该方案解决了 iframe 作为沙箱时几个比较明显的问题:
- 弹窗问题
- 路由丢失
- 应用间通信
- 白屏及应用加载慢
下面开始讲述是如何使用 iframe 作为沙箱并解决这些问题的。
为什么采用该方案?
通读了几遍 无界 源码及官方文档,其是使用 iframe + webComponents 来达到应用完全隔离的效果以及不会产生“沙箱场景中常见的问题(性能)和副作用”。
- 应用 js 在 iframe 中隔离运行。
- 视图通过 webComponents “脱离 iframe 的束缚”,使应用和主项目在同一个文档流中。
在没看源码之前,都会对无界中的 iframe 与 webComponents 的关联是如何实现的充满疑问。在我读完源码后,可以说下我的理解:
- webComponents 本身是基于 shadow DOM 实现,那么将
iframe.document
上的事件、方法、属性都代理到 shadow DOM 的shadowRoot
上,理论上初步实现了 iframe 中操作的 DOM 都会被同步到该shadowRoot
上的这一关系。 - 同时,iframe 中的 Location 和 History 都需要代理,以此来处理路由跳转、相对路径的显示。
无界中处理了很多原生的 DOM 事件和 BOM 对象的代理,其中也做了一些 hack:
- iframe generator 的 Stop 处理
- root => :host 和 @font-face 的映射处理
- shadowRoot 的 head 和 body 上 DOM 的映射处理
相比于实现 iframe 到 webComponents 映射关系的成本,反过来再看待 iframe 作为沙箱的一些问题,我觉得性价比不高,容易出现一些问题。所以才有了这一版方案。
实现方案
使用 iframe 作为沙箱,借鉴无界的 iframe generator 过程绕过应用跨域及实现基于 template 组装应用 html。核心要点:
- 使用主项目的 href 加载 iframe 后立即 Stop,欺骗浏览器为我们提供一个同源的 iframe 沙箱。这样可以带来的好处:
- 改写 iframe 的 history,处理路由保持
- 为
iframe.contentWindow
注入主项目上下文,解决数据共享和应用通信 - 自由的根据提供的 template 组装子应用的 html,在实际的商业项目中将非常重要。
- 子应用入口资源为 var 模式的 js 资源。
- 想要独立运行时,可使用自己的 html
- 想要集成到主项目中运行时,可使用主项目的 html
- 便于应用的技术栈和一些依赖统一升级,统一管控,在修复一些公共问题时,不用一一构建子应用,而只需要发布对应的资源。
细节流程如下图:
使用包装
在无界中,是将沙箱包装为 customElement,我们这版方案包装为 React 组件来达到非常容易上手的效果:
jsx
import SandboxEntry from 'xxx-sandbox'
function MicroApp() {
return (
<SandboxEntry
name={'子应用的 name'}
entry={'子应用入口 js'}
inject={(iframeWindow) => {
// 给 iframe window 注入变量
iframeWindow.__XAPP__ = {}
}}
htmlLoader={(htmlParseResult) => {
// 对于使用主项目的 html
// 可以在这里对不需要的内容做过滤或添加
return htmlParseResult
}}
/>
)
}
使用组件带来的问题:
路由切换时,会销毁当前组件,也会销毁 iframe DOM,这显然是不行的,下次进入应用资源得重新加载了。
为了解决该问题,iframe 是通过绝对定位盖在当前路由组件容器之上的,iframe DOM 节点仍然被创建在 body 下,所以才会有流程图中的 “display” 环节,我们会将样式做事件同步。
这样做带来的好处:
子应用资源不用重复加载,可以达到保活的效果,一些特殊场景需要重载子应用时,只需要将子应用 iframe 中创建的 proxyWindow 卸载,然后再重载子应用 js 即可实现,子应用 iframe 中的 contentWindow 一样很干净。
弹窗问题
弹窗问题是通过 hack 实现,开发者在使用弹窗时是无感知的,处理的场景分为 2 类(针对 antd 组件):
- 通过公共 CDN 引入到项目中的 antd 组件
- 本地 node_modules 引入的 antd 组件
首先,我们需要 hack 一下弹窗涉及的 antd 组件的位置及事件:
- Modal
- Modal.confirm / Modal.success 等
- message.success / message.error 等
hack 的内容:
- 处理位置偏移
- 处理 iframe 范围之外的遮罩同步
在实际的项目中,第一类会比较常见,即通过 CDN 统一引入的 antd 版本,只需要重写 window.antd 即可
jsx
const antd = window.antd
if (antd && !antd.hasPatched) {
window.antd = {
...antd,
// 覆盖
Modal: ProxyModal,
hasPatched: true
}
console.warn('[sandbox]: antd.Modal has been replaced with patchModal')
}
对于本地 node_modules 引入,可以通过 externals 改写引入文件实现:
在代码中通过 import { Modal } from ‘antd’
导入时,即使有无按需引入机制,都会被最终引入到真实文件路径:antd/es/modal
,那么通过 externals 的函数解析,针对该 request 的资源,并 context 为 antd(避免循环处理),改写为我们实现的文件。
总结
引用无界对 iframe 的优缺点总结:
相对于无界为了解决这些缺点带来的更彻底的 webComponents 方案,当然成本也很高。我们提供的方案成本很小,唯一不太优雅的是弹窗的 hack 实现——是个假的全局弹窗,但是对于开发者是无感知的,倒也能接受。
整体思路实现源码:https://github.com/Jmingzi/sandbox/blob/main/src/Sandbox/index.tsx