在Vue 3项目中构建代码沙箱,核心目标是让用户输入的JavaScript、Vue模板或CSS能够在受控环境中运行,既支持实时预览,又不会篡改主应用的全局状态、窃取数据或发起恶意请求。实现这一能力不能依赖简单的eval或new Function,因为这类方式直接运行在主线程全局作用域,风险极高。我们需要从浏览器提供的隔离机制出发,结合Vue 3的响应式与组件系统,设计一套分层防御方案。

为什么主线程直接执行用户代码不可取
很多初学者在写在线代码编辑器时,会习惯性使用new Function或eval把用户代码包成函数执行。这种做法在技术上能跑通,但安全模型完全崩溃。用户代码可以访问window、document、localStorage等全部全局接口,也能修改Vue 3应用所依赖的响应式对象,甚至通过fetch把用户令牌发到外部地址。更严重的是,一旦用户代码进入死循环,整个页面主线程被阻塞,预览区和编辑器全部卡死,用户只能强制关闭标签页。
从浏览器进程模型看,主线程与渲染管线紧密耦合。任何长时间同步计算或无限递归都会触发“页面无响应”。Vue 3的调度系统依赖微任务队列更新视图,若用户代码垄断了执行栈,组件永远无法重新渲染。因此,安全执行用户代码的第一原则就是隔离执行上下文,把不可信逻辑搬到独立环境,通过消息通道和主应用通信,而不是共享内存。
另一个常被忽视的问题是错误边界。主线程抛出的未捕获异常会冒泡到全局,可能中断Vue 3的错误处理中间件。即使用try-catch包裹eval,也拦不住用户代码里设置的定时器后续报错。沙箱机制必须提供生命周期管控,比如执行超时杀掉任务、卸载时清理所有副作用,这些在主线程内很难干净实现。
基于iframe的隔离沙箱与Vue 3通信设计
iframe是浏览器原生提供的隔离容器。给iframe设置sandbox属性并移除allow-same-origin等危险权限后,其内部脚本便无法访问父页面的DOM和Cookie,也不能发起跨域请求,除非显式放行。在Vue 3里,我们可以用一个隐藏的iframe充当沙箱运行时,把用户代码序列化后通过postMessage发送进去,iframe执行完毕再把结果回传。
具体通信协议建议定义明确类型字段。父组件监听message事件,根据type区分是“执行结果”“运行时错误”还是“控制台输出”。为避免混淆,最好给每次执行分配唯一taskId。下面的示例展示Vue 3组合式函数中如何创建沙箱并发送代码:
import { ref, onUnmounted } from 'vue';
export function useCodeSandbox() {
const iframeRef = ref(null);
const output = ref('');
let taskId = 0;
function ensureSandbox() {
if (iframeRef.value) return;
const el = document.createElement('iframe');
el.sandbox = 'allow-scripts';
el.style.display = 'none';
document.body.appendChild(el);
iframeRef.value = el;
window.addEventListener('message', handler);
}
function handler(e) {
const data = e.data || {};
if (data.from !== 'sandbox') return;
if (data.type === 'log') {
output.value += data.payload + 'n';
}
}
function run(code) {
ensureSandbox();
const id = ++taskId;
iframeRef.value.contentWindow.postMessage({
from: 'host',
taskId: id,
code: code
}, '*');
}
onUnmounted(() => {
if (iframeRef.value) {
window.removeEventListener('message', handler);
iframeRef.value.remove();
}
});
return { output, run };
}
上述代码里,iframe仅赋予allow-scripts,没有同源权限,因此沙箱脚本读不到父页Cookie。在iframe内部,需要一段引导脚本接收消息、用try-catch包裹eval并执行,再把console.log重定向到postMessage。这样Vue 3侧就能收集用户代码的日志,而用户代码无法反向操作主应用。相比Web Worker,iframe能真实渲染DOM与CSS,更适合“预览”场景;Worker则更适合纯逻辑计算,且不接触页面结构。
权限收敛还要考虑网络。如果预览需要加载外部字体或图片,可在sandbox中加入allow-same-origin配合特定CSP,但这样会削弱隔离。更稳妥的做法是主应用代理资源请求,把内容以文本形式注入沙箱,避免沙箱自身发起任意请求。Vue 3侧可用computed把注入资源与用户代码拼接,再统一下发。
预览层渲染与样式隔离的实践方案
当用户代码包含Vue单文件组件或HTML模板时,沙箱iframe内可以独立挂载一个Vue 3应用实例来做预览。主应用把编译后的render函数或组件选项发给iframe,iframe用自己内部的Vue副本创建createApp并挂载到本地容器。由于iframe是独立文档,用户写的CSS不会泄漏到主页面,天然实现样式隔离。
如果坚持在主页面内预览(比如为了更顺滑的组件通信),则要用Shadow DOM包裹预览容器。Shadow DOM能限制用户CSS作用域,但无法阻止用户脚本访问外部window。因此主页面内预览通常配合严格的代码静态检查,比如用AST扫描禁止window.location、document.cookie等敏感路径。下面示例展示用Shadow Root挂载用户组件外壳:
const host = document.getElementById('preview');
const shadow = host.attachShadow({ mode: 'open' });
const wrap = document.createElement('div');
shadow.appendChild(wrap);
// 假设 userComponent 是经编译的可信组件选项
import { createApp } from 'vue';
const app = createApp(userComponent);
app.mount(wrap);
样式隔离之外,还要处理执行超时。沙箱iframe可通过setTimeout在宿主侧强制移除并重建,切断死循环。Vue 3组合式函数里可以维护一个执行计时器,超过两秒无回应就销毁iframe重建,保证编辑器不卡顿。错误捕获方面,iframe内的window.onerror和unhandledrejection都应转发给主应用,由主应用以em或blockquote展示错误栈,帮助用户调试。
最后注意依赖注入。用户代码若使用import语法,沙箱需提供模块解析。简单方案是在下发代码前用正则改写裸模块名为CDN完整URL,并在iframe内用动态import加载;复杂场景可集成esbuild-wasm在宿主侧预编译。无论哪种,都要确保第三方包也运行在沙箱内,而不是被提升到主应用作用域,否则隔离形同虚设。
Vue3code_sandboxuser_code_execution修改时间:2026-08-14 05:36:33