OX
OPEN-OX

// docs / preview

预览沙箱

预览实现由 OPEN_OX_PREVIEW_BACKEND 与环境决定:local(每站点 next dev)、storage(静态导出上传到 Supabase bucket site-previews,浏览器经本应用的 /site-previews/{projectId} 代理加载,避免 Storage 默认 CSP 阻断脚本),以及 e2b(云端沙箱内 next build + npx serve out)。本地开发若已配置 Service Role +NEXT_PUBLIC_SITE_URL 且未显式设置 env,默认与典型生产一致走 storage

三种预览后端

模式典型场景要点
local强制 iframe 内 dev HMR`OPEN_OX_PREVIEW_BACKEND=local`,进程跑在宿主
storage生产对齐 / 无 E2BsyncStaticSitePreview:build → 上传 `out/` → 指纹跳过重复构建
e2b隔离云端 Node`OPEN_OX_PREVIEW_BACKEND=e2b`,sandbox_id 持久化可重连
POST /api/projects/[id]/preview:已登录用户始终走 startDevServer 的分支逻辑;未登录且 OPEN_OX_PREVIEW_BACKEND=storage 时,可对就绪项目返回公开的静态预览 URL(无需沙箱)。

为何静态导出(E2B / Storage 共通)

方案启动时间资源占用URL 稳定性
next dev15-30s高(持续运行)不稳定
next build + serve30-60s(首次)低(静态文件)稳定

Storage 与 E2B 路径最终都向浏览器提供静态 HTML + chunk。 相较长时间驻留的 next dev,导出站点 URL 稳定、宿主机占用更低;local 后端仍保留 dev server 以便调试 HMR。

Storage 路径

lib/staticSitePreview.ts 在宿主(或 CI worker)对 sites/{id}/ 执行带 basePath 的静态导出,将 out/ 同步至 bucket site-previews,对象键前缀 p/{projectId}/…。浏览器不直连 Storage,而是请求同源 /site-previews/{projectId}/…,由 App Router 代理响应并放宽 CSP, 以便 iframe 内脚本与 chunk 正常执行。

E2B 路径

startDevServer(projectId)
│
├── 1. 查 Supabase sandbox_id → 尝试重连已有沙箱
│      ├── 沙箱存活 + server 运行 → 直接返回 URL(最快路径)
│      ├── 沙箱存活 + /out 存在 → 重启 serve
│      └── 沙箱不存在/已过期 → 创建新沙箱
│
├── 2. 创建 E2B 沙箱(模板:NEXTJS_TEMPLATE)
│      模板预装:Node.js、npm、Next.js 及所有基础依赖
│
├── 3. 批量上传项目文件(20 个/批,并行)
│      跳过:node_modules、.next、.git
│      特殊处理:package.json 使用模板版本(避免版本冲突)
│
├── 4. 注入 output: 'export' 到 next.config.ts
│      (静态导出必需,images.unoptimized: true)
│
├── 5. 智能依赖安装
│      对比项目 package.json 与模板 package.json
│      只安装模板中没有的新增依赖(大幅减少安装时间)
│
├── 6. next build → 静态导出到 /out
│
└── 7. npx serve out -l 3000(后台运行)
       等待 "Accepting connections" 输出
       返回 https://{sandbox.getHost(3000)}

增量重建

storage 后端下,同一路由会触发 syncStaticSitePreview:重新静态导出并上传(指纹未变时可跳过)。e2b 后端则复用沙箱进程并按需全量/热更新上传。

rebuildDevServer(projectId)
├── 连接已有沙箱(不重建,节省 30s+)
├── kill 旧的 serve 进程
├── 重新上传修改的文件
├── 安装新增依赖(如有)
├── next build
└── 重启 serve
下图主要为 e2b 路径;storage 路径无常驻 serve 进程,依赖指纹跳过重复导出上传。

触发时机

预览不是自动启动的,而是按需触发:

用户点击 Preview 面板POST /api/projects/[id]/preview首次启动沙箱
修改完成后PUT /api/projects/[id]/preview自动触发增量重建
重新进入已有项目预览状态为 idle,等待用户主动点击

E2B 模式下按需创建沙箱可避免无谓开销;Storage 模式依赖指纹跳过未变更的导出上传。

沙箱重连

sandbox_id 持久化在 Supabase 的 projects 表中。 下次请求预览时,系统先尝试用这个 ID 重连已有沙箱:

// 重连逻辑
const sandbox = await Sandbox.connect(project.sandbox_id);
if (sandbox.isRunning) {
  // 检查 serve 进程是否还在
  // 如果在 → 直接返回 URL
  // 如果不在但 /out 存在 → 重启 serve
}
// 沙箱已过期 → 创建新沙箱,更新 sandbox_id
E2B 沙箱有生命周期限制。长时间不活跃的沙箱会被回收,此时需要重新创建。 但由于模板预装了所有基础依赖,重建速度仍然可接受。