Skip to content

大型前端系统插件生态容器 ​

、插件完整生命周期(宿主全权管理) ​

宿主的 PluginManager 统一管控:

  1. 解析阶段:读取 manifest,校验插件 ID、版本、权限,检查是否和宿主版本兼容
  2. 实例化:创建沙箱(iframe/worker),注入 SDK 代理对象
  3. 激活 activate:调用插件 activate();插件注册命令、事件监听
  4. 运行期:插件通过消息总线和宿主交互;宿主监听插件异常
  5. 停用 deactivate:卸载插件,调用 deactivate(),清理事件订阅、销毁沙箱
  6. 销毁:销毁 iframe/worker,释放内存,清除所有插件状态

宿主强制捕获插件沙箱内异常:插件报错不能崩溃主应用,只会标记插件异常、弹窗提示。

独立域 iframe

Worker

Figma 当年放弃 iframe 的两个硬数据是——消息往返 0.1ms(约 1000 次/秒),而大型文件整体序列化要 14 秒——所以它把 QuickJS 编译成 WASM 在主线程内跑独立 JS 虚拟机,UI 才放 null-origin iframe。大多数业务系统答案相反,异步够用,iframe/Worker 更划算。

核心目标:安全隔离 + 能力受控暴露 + 统一插件生命周期 + 外部开发者可开发、可分发、可安装

  1. 宿主主应用(你的大型前端系统)作为容器;
  2. 插件运行在隔离沙箱,不能直接访问宿主 DOM / 全局变量;
  3. 宿主提供一套标准化 API 能力层,插件只能通过这层 API 调用平台能力;
  4. 宿主管理插件生命周期、权限、消息通信、资源加载。

整体架构分层(自下而上) ​

关键原则:插件永远不能直接调用宿主内部代码,所有能力必须经过代理层转发。和 Figma 一样,插件只能调用 figma.xxx;VSCode 插件只能调用 vscode.xxx。

宿主主应用(你的大型前端系统)
├── 插件容器核心层(Plugin Runtime Container)
│   ├── 插件管理器 PluginManager
│   ├── 沙箱运行时 Sandbox
│   ├── 消息通信总线 MessageBus
│   ├── 生命周期管理器 Lifecycle
│   └── 权限控制中心 Permission
├── 平台能力代理层(Platform API / 插件SDK)⭐核心
│   ├── 宿主能力封装(页面、组件、数据、画布、命令、事件)
│   ├── 类型定义(d.ts,给外部开发者)
│   └── 版本兼容适配器
├── 宿主内部业务层(原有系统业务逻辑,**禁止直接暴露给插件**)
└── 插件市场/后台(插件上传、审核、版本管理、安装)

外部插件:由第三方开发者编写,只依赖平台SDK,运行在沙箱内

插件沙箱方案(前端最重要的安全问题) ​

方案 1:iframe 沙箱(最推荐,对标 Figma) ​

  • 每个插件独立 iframe,设置 sandbox 属性,禁用脚本以外权限
  • iframe 内是独立上下文,window、DOM 完全隔离,插件无法直接篡改宿主页面 DOM
  • 通信:postMessage 作为消息总线,所有调用都序列化消息传递
  • 优点:隔离性最好,简单稳定;插件崩溃不会搞挂主应用
  • 缺点:iframe 实例多的时候有内存开销;样式隔离;不能共享 WASM 等资源

Figma 就是这套:插件代码跑在 iframe,figma 对象是宿主注入的代理对象,插件调用方法只是发 postMessage。

方案 2:WebWorker 沙箱(适合纯计算、无 UI 插件) ​

  • 插件代码跑在 Worker,没有 DOM
  • 适合:数据处理、导出、校验、批量计算类插件
  • 局限性:插件不能直接渲染 UI;如果插件需要弹窗 / 面板,UI 必须由宿主渲染

方案 3:基于 ES Module + 动态 import + 代理(轻量沙箱,慎用) ​

不推荐作为主方案。原生 import 无法完全隔离全局对象,容易逃逸,适合内部可信插件,不适合开放给外部第三方。

运行时层:四种沙箱的取舍与"可替换"原则 ​

方案 隔离强度 同步访问宿主对象 性能开销 生态与维护成本 最适用场景 主线程 QuickJS-in-WASM 高:独立线性内存,无浏览器 API,对象表示与宿主完全不同,不可能混淆内外对象 可以,宿主注入函数可同步调用 无 JIT,解释执行;酷家乐公开分享称相对 V8 慢 30–40 倍、经 WASM 后约百倍(单一来源,定性) 高:跨 VM 绑定、句柄生命周期、调试困难 编辑器/画布类,需同步高频读写文档 SES Compartment 中高:lockdown 冻结 intrinsics 防原型污染 可以,同 realm 同线程 冻结 intrinsics + with/Proxy 作用域开销,官方未公布百分比 中:需审计每个 endowment 同线程插件但信任度较高 Web Worker 中:无 DOM、独立线程,可 terminate() 不可以,异步 MessagePort 通信与结构化克隆 低 计算密集、可异步 独立域/null-origin iframe 高:浏览器进程级隔离,null origin 下 CORS 天然拒绝 不可以,postMessage 通信与序列化;大对象序列化可能极慢 低,生态最成熟 插件 UI 面板、异步业务插件 WASM/WASI(Extism、Javy) 高:能力式,默认无 syscall 不可以(除非宿主逐个授予 import) 接近原生,可设 timeout_ms/maxPages 中:需用 Rust/Go 等写插件 批处理、服务端函数

能力层:API 网关与能力清单 ​

能力域 风险 建议形态 权限要求 只读查询(当前文档/选中节点/视口) 低 快照 + 句柄,异步或同步均可 默认授予 写入修改(增删改节点/属性) 高 必须走事务层,单事务节点数与深度上限 声明 + 首次使用时授权 用户身份(currentUser) 中 只给最小字段,不给 token permissions: currentuser 本地持久化(clientStorage) 中 按插件命名空间分区,卸载即清 默认授予,有容量上限 网络请求 高 manifest 声明 allowedDomains,宿主 CSP 强制;高安全场景经宿主出口代理注入凭据 声明域名 + 公示理由 UI(面板、模态、通知) 中 宿主预留容器,禁自绘全屏 默认授予 导出/下载、剪贴板、外链 中高 需用户手势,宿主代执行 运行时申请 宿主主窗口 DOM 极高 永不开放 禁止

落地路线图 ​

分五个阶段推进,每个阶段都能独立交付价值。 第一阶段做内核可插拔化:把状态读写收敛到命令/事务层,把撤销栈与协同 origin 机制做扎实。这一步不给外部插件用,但它是后面一切的地基,内部功能先以插件形式重写(吃自己的狗粮)。 第二阶段只开放只读 API + UI 面板,运行时用独立域 iframe,通信用 JSON-RPC。先只读后写入,是所有成熟生态的共同节奏。 第三阶段开放写入,引入事务校验、配额与权限中间件,把 API 网关做成唯一出口。 第四阶段按真实瓶颈决定要不要上同步运行时:只有当出现"插件必须高频同步读写文档模型"的场景时,才引入 QuickJS-in-WASM 或 SES Compartment,并且把它设计成可开关、可回退的第二个运行时,而不是替换现有 iframe 路径。 第五阶段做生态运营:市场、签名分级、审核流程、脚手架与文档站、灰度与可观测性闭环。

平台能力代理层(Plugin SDK,对标 figma /vscode) ​

// 外部插件,只依赖 @your-platform/sdk
import { platform } from "@your-platform/sdk";

// 插件激活
export function activate() {
  // 调用宿主能力:读取画布数据
  const nodes = platform.canvas.getSelection();
  // 注册宿主命令
  platform.commands.register("my-plugin.hello", () => {
    platform.ui.showToast("插件运行成功");
  });
}

export function deactivate() {
  // 插件卸载清理
}

可以对外暴露的能力分类(参考 Figma/VSCode) ​

  1. 核心数据能力
    • 获取 / 修改画布、元素、页面数据
    • 查询选中项、创建节点、删除节点
    • 读取项目元数据
  2. UI 能力(宿主提供基础 UI 组件)
    • 弹窗、toast、通知、侧边面板、模态框
    • 宿主提供基础组件(按钮、输入框、下拉),插件调用 API 渲染,插件不能直接渲染 DOM 到宿主页面
    • 如果插件需要自定义 UI:在 iframe 内部渲染自己的页面
  3. 命令与快捷键
    • 注册命令、绑定快捷键,宿主菜单可以挂载插件命令
  4. 事件订阅
    • 监听宿主事件:选中变更、页面切换、保存事件
  5. 文件 / 导出能力
    • 导出图片、JSON、下载文件(宿主代理文件读写)
  6. 存储能力
    • 插件本地存储:隔离存储,每个插件独立存储空间,不能互相访问

红线:不要把宿主内部的状态库、组件实例直接暴露出去。内部随便重构,只要 SDK 接口不变,外部插件不用改。

第三方开发示例 ​

Released under the MIT License.