// 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 | 生产对齐 / 无 E2B | syncStaticSitePreview: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 dev | 15-30s | 高(持续运行) | 不稳定 |
| next build + serve | 30-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 进程,依赖指纹跳过重复导出上传。触发时机
预览不是自动启动的,而是按需触发:
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