⸻
在可视化大屏 / 低代码平台里,最佳实践是把**UI 运行(第三方组件)与用户自定义逻辑(数据转换/脚本)**分别隔离处理——分别用 iframe(UI 隔离、版本控制、样式隔离)和 Web Worker(逻辑隔离、CPU 隔离、可终止)。把两者组合起来,可以在保证安全与稳定的前提下,提供强大的自定义能力。
下面分几个部分介绍整体架构、关键消息协议、生命周期管理、性能/安全/容错设计与示例实现。
⸻
一、整体架构概览
主应用(Editor / Preview)
├─ Layout & PageConfig(react-grid-layout)
├─ Material 管理(builtin / preset / user-upload)
├─ Preview Renderer(Grid → 每个 cell 渲染 RenderWrapper)
│ ├─ 内置组件(React 直接渲染)
│ └─ 自定义组件(IframeRenderer)
├─ 数据层
│ ├─ Worker 沙箱(run transformCode / user JS / mock)
│ └─ API fetch / WebSocket(主线程或由 Worker 协同执行)
└─ 管理功能(版本切换、保存/导出、权限)
每个自定义组件实例的渲染流程(高层):
1. 主应用读取 PageConfig 得到 material、layout、props、dataSource 等配置。
2. 在 预览模式(或发布时),RenderWrapper 发起数据执行请求(不在编辑态)。
3. 数据处理:先把 dataSource(api 或 mock 或 js)交给 Worker 沙箱 执行(或主线程 fetch 后交由 Worker 转换),得到最终 data。
4. 渲染:把 props + data 通过 postMessage 传入对应的 iframe(iframe 会加载对应 React 版本与 UMD 脚本),iframe 内部渲染组件。
5. 交互事件(组件内部发生用户交互)可通过 postMessage 回传到主应用或 Worker(按需)。
⸻
二、关键设计要点与消息协议
2.1 RenderWrapper(主应用端)
职责:
• 根据 PageConfig 触发数据计算(预览时)
• 管理 Worker 请求与超时
• 管理 iframe 实例(创建/销毁/重载)
• 组织最终 props 并发送到 iframe
简化请求流程(伪流程):
RenderWrapper.mount:
if component is builtin -> render directly
else:
ensure iframe exist
if mode === preview:
data = await runDataPipeline(component.dataSource)
iframe.postMessage({ type: ‘render’, props: component.props, data })
else:
iframe.postMessage({ type: ‘render-placeholder’, props: component.props })
2.2 Worker 沙箱协议(主线程 ⇄ Worker)
请求格式:
type WorkerReq = {
id: string;
action: ‘runTransform’ | ‘execCode’ | ‘mock’;
payload: { code?: string; input?: any; apiConfig?: any };
};
响应格式:
type WorkerRes = {
id: string;
ok: boolean;
result?: any;
error?: string;
};
主线程行为:
• 发起请求后设置超时(例如 3000 ~ 8000 ms),超时后 terminate worker 并返回错误
• 若 Worker 成功返回,继续下一步
2.3 Iframe 协议(主应用 ⇄ iframe)
消息类型(示例):
• 主应用 → iframe:
• init:告诉 iframe 加载哪个 React 版本
• render:传入 { props, data } 让 iframe 渲染组件
• update-props:仅更新 props
• destroy:提示 iframe 清理
• iframe → 主应用:
• ready:iframe 初始化完成
• error:加载或渲染错误
• event:组件事件(例如点击、钻取)回传到主应用
注意:通信要校验 origin 或用 token 做简单鉴权,避免任意页面发送消息。
⸻
三、数据管线(Data Pipeline)
目标:统一处理三种数据来源并返回「标准数据」给组件:
• HTTP 接口(fetch)
• Mock / JS(用户写代码生成数据)
• 转换代码(transformCode)——把原始数据转换为目标形态
实现步骤:
1. 如果 dataSource.type === ‘http’:主线程或 Worker 发起 fetch(可暴露 fetcher API 给 Worker)
2. 若 dataSource.type === ‘code’:把 code 传入 Worker,在 Worker 中执行(传入白名单 API)
3. 执行 transformCode:在 Worker 中执行,入参为 rawData,返回 finalData
4. 返回 finalData 给主线程 → 传入 iframe
为什么把变换放 Worker:
• 可能包含用户自定义逻辑(new Function)
• 防止长计算阻塞 UI
• 可在 Worker 中限制网络/时间/权限
⸻
四、生命周期与资源管理
4.1 Worker 池 或 单例
• 推荐:Worker 单例 + 可重启机制(对于小规模并发足够)
• 如果并发任务较多,可做 Worker 池(N 个 Worker,轮询/队列分配任务)
• 每次任务需设置超时,超时或异常时 terminate 并重建 Worker
4.2 Iframe 管理
• Iframe 初始化成本较高(加载 React + DLL),建议:
• 按 React 版本缓存 iframe 实例:相同 reactVersion 的组件可以复用同一个 iframe 模板(或使用多个但有限数量)
• 按组件实例隔离渲染:如果安全级别更高,每个组件都用独立 iframe(资源开销大)
• 清理策略:
• 在页面销毁或很久未使用时移除 iframe
• iframe 出现错误(加载失败)时采用退级策略(显示占位/错误信息)
⸻
五、性能优化策略
1. 静态资源缓存与 CDN:
• UMD 脚本、React UMD、iframe 模板等通过 CDN + 版本化缓存
2. 预加载:
• 在用户进入预览/页面打开前,预加载常用 React 版本的 iframe 模板(后台静默创建)
3. 批量请求:
• 若页面中多个组件请求同一 API,可在主线程合并请求或在 Worker 中缓存
4. Worker 池:
• 高并发时用 Worker 池避免频繁创建销毁 Worker 的开销
5. 轻量化 UMD:
• 建议用户组件按需打包(避免把 React 也打包进 UMD),并尽量减小 CSS/资源
⸻
六、安全与沙箱强化
1. 白名单 API:只暴露必须的、受控的 API 给 Worker 或 iframe(例如 fetcher、formatters)。不要把 auth token 或敏感接口裸露。
2. 消息鉴权:主应用发送消息给 iframe 带上 projectId 或一次性 token,iframe 校验后再执行。
3. AST 静态检查:对用户代码做基础的 AST 分析(拦截明显无限循环/危险语法)
4. 超时销毁:每个 Worker 调用使用超时保护;iframe 加载长时间无响应要重试或失败回退
5. CSP & COOP/COEP:
• 在生产环境考虑启用 Cross-Origin-Opener-Policy 与 Cross-Origin-Embedder-Policy 做高级隔离(尤其当使用 SharedArrayBuffer 时)
6. 沙箱两层策略:
• 对 UI 使用 iframe(防止 DOM 污染)
• 对逻辑使用 Worker(防止主线程阻塞)
• 必要时把 Worker 再放到 iframe 内部(Worker inside iframe)以进一步隔离来源(复杂场景)
⸻
七、容错与回退策略
• 组件加载失败:显示可配置占位(错误信息 + 重试按钮),日志上报
• Worker 执行失败:返回空数据或上次缓存数据(灰度回退)
• 网络异常:采用重试与退化显示(静态示例数据)
• 版本回滚:物料库保留旧版本 URL,允许页面实例回滚到旧版本
⸻
八、示例实现(简化版代码)
下面给出核心模块的精简示例,展示 RenderWrapper 如何协调 Worker 与 iframe。
8.1 RenderWrapper(核心流程)
// RenderWrapper.tsx (简化)
import React, { useEffect, useRef, useState } from 'react';
import { runUserCodeWithWorker } from './dataWorker'; // 返回 Promise<result>
import { createOrGetIframe } from './iframePool'; // 管理 iframe 池
export default function RenderWrapper({ compConfig, mode = 'preview' }) {
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// create iframe instance from pool based on reactVersion
iframeRef.current = createOrGetIframe(compConfig.reactVersion);
async function doRender() {
try {
let data = null;
if (mode === 'preview' && compConfig.dataSource) {
// Data pipeline in worker
const res = await runUserCodeWithWorker(compConfig.dataSource);
if (!res.ok) throw new Error(res.error || 'data error');
data = res.result;
}
// send render message to iframe
iframeRef.current!.contentWindow!.postMessage({
type: 'render',
globalName: compConfig.globalName,
url: compConfig.url,
props: compConfig.props,
data,
}, '*');
} catch (err: any) {
setError(err.message || String(err));
}
}
doRender();
return () => {
// optional: cleanup or put iframe back to pool
};
}, [compConfig, mode]);
if (error) return <div className="error">错误:{error}</div>;
return <div style={{ width: '100%', height: '100%' }}><div id={`iframe-host-${compConfig.id}`} /></div>;
}
8.2 runUserCodeWithWorker(Worker wrapper,返回标准响应)
// dataWorker.ts (伪代码)
export async function runUserCodeWithWorker(dataSource) {
// build worker request: include api/url/transformCode/mockCode
// return { ok: true, result } or { ok: false, error }
}
8.3 iframe 沙箱(接收 render 消息)
// iframe-sandbox.html 中脚本接收 postMessage
window.addEventListener('message', async (e) => {
const { type, url, globalName, props, data } = e.data || {};
if (type === 'render') {
try {
// load react if not loaded
// load url if not loaded or if cache miss
// render component with merged props: { ...props, data }
} catch(err) {
parent.postMessage({ type: 'iframe-error', error: String(err) }, '*');
}
}
});
⸻
九、测试策略
1. 单元测试:Worker wrapper、AST 检查、iframe 消息格式验证
2. 集成测试:模拟组件上传、版本切换、数据转换、preview 渲染完整链路
3. 压力测试:大量并发 Worker/iframe 创建的场景(评估缓存、池化效果)
4. 安全测试:注入恶意脚本、死循环、网络滥用场景,验证超时与隔离有效性
⸻
十、运维与部署建议
• 将 iframe 模板与 React UMD、用户组件托管在 稳定的 CDN(版本化路径)
• 对用户上传组件做白名单或审核(自动化静态检测 + 人工审核)
• 记录加载与执行日志(错误、超时、资源用量)并上报监控
• 为重要页面启用回滚策略(当新版本出问题时快速回滚到已知可用版本)
⸻
十一、总结(TL;DR)
• 把 UI 隔离给 iframe(支持按需加载 React 版本与 UMD)
• 把逻辑隔离给 Web Worker(执行用户代码、数据转换,且可终止)
• 主线程负责:布局渲染、管理 Worker/iframe 池、做消息校验、做鉴权与监控
• 关键点:消息协议、超时机制、API 白名单、版本缓存与回退
• 最终效果:既能保证高度自定义能力(用户上传组件 / 自定义数据脚本),又能保证平台的稳定性、安全性与可运维性