Skip to content
On this page

由无界启发的一种前端沙箱实现

整体思路实现源码:https://github.com/Jmingzi/sandbox/blob/main/src/Sandbox/index.tsx

概述

本文讨论并实现了一种基于 iframe 的沙箱方案——不是无界的实现,该方案解决了 iframe 作为沙箱时几个比较明显的问题:

  • 弹窗问题
  • 路由丢失
  • 应用间通信
  • 白屏及应用加载慢

下面开始讲述是如何使用 iframe 作为沙箱并解决这些问题的。

为什么采用该方案?

20e6409baa60b36c8f43a032064729c3630b4aa0

通读了几遍 无界 源码及官方文档,其是使用 iframe + webComponents 来达到应用完全隔离的效果以及不会产生“沙箱场景中常见的问题(性能)和副作用”。

  • 应用 js 在 iframe 中隔离运行。
  • 视图通过 webComponents “脱离 iframe 的束缚”,使应用和主项目在同一个文档流中。

在没看源码之前,都会对无界中的 iframe 与 webComponents 的关联是如何实现的充满疑问。在我读完源码后,可以说下我的理解:

  1. webComponents 本身是基于 shadow DOM 实现,那么将 iframe.document 上的事件、方法、属性都代理到 shadow DOM 的 shadowRoot 上,理论上初步实现了 iframe 中操作的 DOM 都会被同步到该 shadowRoot 上的这一关系。
  2. 同时,iframe 中的 Location 和 History 都需要代理,以此来处理路由跳转、相对路径的显示。

5e83efc0a6a3de19ea291f94a3a53145510c6851

无界中处理了很多原生的 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
      • 便于应用的技术栈和一些依赖统一升级,统一管控,在修复一些公共问题时,不用一一构建子应用,而只需要发布对应的资源。

细节流程如下图:

55f86ddade93cc76fd50337d4792551d684a7afd

使用包装

在无界中,是将沙箱包装为 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 的优缺点总结:

56f9ad6d7c9cef8fad4275e7d6ad073b3998f87d

相对于无界为了解决这些缺点带来的更彻底的 webComponents 方案,当然成本也很高。我们提供的方案成本很小,唯一不太优雅的是弹窗的 hack 实现——是个假的全局弹窗,但是对于开发者是无感知的,倒也能接受。

整体思路实现源码:https://github.com/Jmingzi/sandbox/blob/main/src/Sandbox/index.tsx