DOCUMENTATION · 跨域与嵌入

跨域与嵌入

---

同步自 docs/CROSS_ORIGIN.md(218 行) · commit b2b4124 · 读源文件 ↗

本文回答:COOP / COEP 到底买到了什么、代价是什么、什么时候该开、部署与 iframe 嵌入怎么做。 这是本项目最容易翻车的一节:SharedArrayBuffer 的报错很直白,但 require-corp 的封杀与 same-origin 的断链是静默的——页面看上去正常,只是某些资源没了、某些弹窗不工作了。

0. 30 秒结论

  1. COOP/COEP 不是"打开 SAB 的开关",而是整页的全局约束:开启后页面进入「跨域隔离」模式, 所有跨域子资源的加载规则与所有弹窗/opener 关系全部改变。
  2. 项目决策(2026-10-01):做成「可选开关」,默认关闭(vite.config.ts 的 resolveIsolationHeaders())。 需要 WASM 多线程 / 共享内存(SharedArrayBuffer)时用 CROSS_ORIGIN_ISOLATION=1 显式打开,当前档位 C(require-corp)。开关怎么用见 §3.1。
  3. 当前"开着"的实际代价是 0:main 从未使用 SAB(src/ 0 命中),且页面没有任何跨域子资源 (index.html / src/ 里 CDN 字体、第三方脚本、外链图片:0 命中,全部自托管)。 代价为 0 的前提就是"全站自托管"——所以它必须变成一条纪律(§2.1),而不是一个偶然。
  4. ⚠️ 纠正一个常见误区:普通(单线程)WASM 插件并不需要跨域隔离。 本项目的 WASM ABI 是单线程的:宿主只给 4 个 import,插件导出的是自己的私有 memory, 没有共享内存;docs/PLUGIN_WASM.md 明写「多线程 std::thread ❌ 无 …… 插件天生单线程」, docs/ABI.md 也写「WASM 插件默认单线程」。 真正需要隔离的是「WASM + Worker + SharedArrayBuffer / 共享内存 / Atomics」那条路。

1. 它买到了什么


2. 代价矩阵(重点,逐条都会真的发生)

受影响的东西触发机制表现解法
CDN 字体 / 图片 / 脚本 / CSS(<link>、<img>、<script>、@font-face 指向第三方域)COEP: require-corp 要求每个跨域子资源自带 Cross-Origin-Resource-Policy: cross-origin(或走 CORS)资源被 block,控制台报 net::ERR_BLOCKED_BY_RESPONSE,字体回退、图标缺失、页面"看起来没样式"① 自托管(同源)最稳;② 让 CDN 返回 Cross-Origin-Resource-Policy: cross-origin;③ 换 credentialless(§3 档位 B)
第三方统计 / 埋点 / 客服 / 支付 SDK同上(它们普遍不加 CORP)静默失效,没有任何报错(宿主 JS 自己吞了)同上;或把这类脚本挪到不带隔离的父页/独立域
OAuth 弹窗登录、window.open + postMessage、window.opener 通信COOP: same-origin 会把 opener 关系切断window.opener === null,回调页收不到消息,登录流程卡死改整页跳转;或把 COOP 放宽为 same-origin-allow-popups
<iframe src="第三方">COEP 对该 iframe 文档同样生效iframe 白屏 / block同源化、让 iframe 带 CORP、或 credentialless
把本引擎嵌进别人的 iframe隔离无法跨非同源边界继承:父页不隔离 → 子页 crossOriginIsolated=false用到 SAB 的插件静默降级为结构化克隆通路(不报错,只是慢 / 功能弱)父页也下发两个头,且 iframe 上写 allow="cross-origin-isolated";跨域父页基本无解
老代码里的 document.domain = ...与 COOP 互斥隔离失效移除
嵌套 iframe(多层)隔离逐层要求同源或显式授权内层拿不到隔离减少层级;同源化

一句话:require-corp 会惩罚页面上任何一个你控制不了的资源,same-origin 会惩罚 任何依赖 opener 的第三方流程。这两件事都不会给你一个明确的错误提示。


2.1 一旦开启(CROSS_ORIGIN_ISOLATION=1):必须遵守的纪律

隔离开着的时候,"代价 0"是被下面这些约束换来的。违反任何一条,症状都是静默的:

✅ 允许

❌ 禁止(会静默失效)

📌 一旦要引入上述任何一种,先回来读 §3 重新选档位,别直接加依赖 —— 因为失败时控制台 可能什么都不说。

可选:把"豁免"集中成一处(若确实需要第三方资源)


3. 三个档位(按需选一,别默认全开)

档位响应头能拿到 SAB对第三方资源适用
A. 不开(✅ 本项目默认)无❌✅ 完全不受影响不做 WASM 多线程 / 零拷贝的一切场景(默认)
B. credentialless(折中)COOP: same-origin + COEP: credentialless✅(仅支持的浏览器,见脚注)✅ 允许无凭据的跨域 no-cors 资源(图片/字体/脚本)通过想要 SAB、又必须拉 CDN 资源;兼容性有限(见下方脚注)
C. require-corp(开启开关时的默认档位)COOP: same-origin + COEP: require-corp✅❌ 跨域资源必须自带 CORP 或 CORS全部资源自托管 + 需要 SAB。前提是 §2.1 的纪律被遵守

脚注 · 档位 B(COEP: credentialless)的浏览器兼容性

浏览器起始版本
Chrome / Edge96+
Chrome for Android96+
Opera82+
Firefox(桌面)119+
Firefox for Android❌ 不支持
Safari / Safari on iOS❌ 不支持

数据来源:MDN browser-compat-data 8.1.3(键 http.headers['Cross-Origin-Embedder-Policy'].credentialless)。 不支持的浏览器会忽略这个无法识别的 COEP 取值:页面不报错、照常打开,但 crossOriginIsolated === false → SAB 不可用(效果等同档位 A)。所以档位 B 只在"主流桌面浏览器 + 能接受 Safari/移动 Firefox 降级"时才划算。

3.1 开关用法(vite.config.ts)

开关只影响 dev / preview(生产响应头由托管方回传,见 §4)。默认关闭。

npm run dev                                                    # 默认:关闭(不下发任何隔离头)
CROSS_ORIGIN_ISOLATION=1 npm run dev                           # 打开(档位 C,require-corp)
CROSS_ORIGIN_ISOLATION=1 CROSS_ORIGIN_EMBEDDER_POLICY=credentialless npm run dev      # 档位 B
CROSS_ORIGIN_ISOLATION=1 CROSS_ORIGIN_OPENER_POLICY=same-origin-allow-popups npm run dev  # 放宽 COOP(仍失去 SAB)
环境变量取值默认
CROSS_ORIGIN_ISOLATION显式真值 1/true/on/yes = 开;其余(含未设置、0/false/off/no)= 关关
CROSS_ORIGIN_EMBEDDER_POLICYrequire-corp / credentiallessrequire-corp
CROSS_ORIGIN_OPENER_POLICYsame-origin / same-origin-allow-popupssame-origin

恢复 archive/original 里的压测资产(WASM 多线程 / SAB)时,必须用 CROSS_ORIGIN_ISOLATION=1 启动, 否则那条通路会静默降级为结构化克隆。

改完请同步更新本节与 §0 的结论。tests/docs-consistency.test.ts 会真实加载 vite.config.ts (不是正则猜)验证:默认关闭、=1 时下发 require-corp、=0 时不下发、两个档位变量能透传; 并检查本文档是否写明了开关名与当前档位。

档位 B/C 只对支持该特性的浏览器生效;不支持时页面照常打开,只是 crossOriginIsolated 为 false。


4. 生产部署清单

vite.config.ts 的头只在 dev/preview 生效,dist/ 部署到哪、由谁回传这两个头,是运维问题。

托管方式怎么加头备注
Nginxadd_header Cross-Origin-Opener-Policy same-origin always;
add_header Cross-Origin-Embedder-Policy require-corp always;
放在 HTML 文档的 location 上;always 保证错误页也带
Caddyheader { Cross-Origin-Opener-Policy same-origin ... }同上
Netlify根目录 _headers 文件需要随 dist/ 一起发布
Vercelvercel.json 的 headers 段需按路径匹配 HTML
GitHub Pages无法自定义响应头想开隔离只能前面套一层 Cloudflare Worker / 自有反向代理
对象存储 + CDN(OSS/COS/S3+CDN)多数默认不支持自定义响应头,或会剥掉优先选档位 A;必要时走反向代理
跨域部署(页面在 A 域、public/plugins/** 在 B 域)除了 COEP,还得给 WASM/剧情资源配 CORS否则 fetch 被拦,与 SAB 无关也会挂

上线后 10 秒自检(控制台):

crossOriginIsolated                 // 期望:按选定档位为 true / false(不是"必须 true")
document.querySelector('link[rel=stylesheet]')  // 结合 Network 面板看有没有 ERR_BLOCKED_BY_RESPONSE

5. 把引擎嵌进 iframe

引擎自身不使用 iframe(插件 UI 是 DOM 挂载在宿主给的容器里),但"把引擎放进 iframe"是很常见的集成方式, 两种拓扑的结论完全不同:

5.1 同源 iframe(父页与引擎同域)

5.2 跨域 iframe(父页在别的域)

postMessage 桥的最小契约(当前仓库未实现,属于待建能力):

父 → 子 : { type:'load', url, autoStart } / { type:'start' } / { type:'save', slot } / { type:'event.subscribe', name }
子 → 父 : { type:'ready' } / { type:'event', name, payload } / { type:'state', snapshot } / { type:'error', message }

6. 症状 → 原因速查

症状最可能原因
字体/图标/样式"没加载",控制台 ERR_BLOCKED_BY_RESPONSECOEP 是 require-corp,但该跨域资源没带 CORP
第三方统计/客服静默失效同上(对方不加 CORP)
OAuth 弹窗回调收不到 / window.opener 为 nullCOOP 是 same-origin
iframe 里的引擎"看起来正常但很慢"父页未隔离 → crossOriginIsolated=false → SAB 通路降级
跨域 iframe 里 window.__storyRuntime 是 undefined跨域窗口属性不可读,需要 postMessage 桥
SharedArrayBuffer is not defined没隔离,或缺 HTTPS/localhost
本地 npm run dev 一切正常,上线就出问题vite.config.ts 的头没在生产侧复刻

7. 如果将来真的要 SAB,推荐做法

  1. 默认 A 档位,只有确实要跑 SAB 的部署(如压测/多线程 WASM)才切到 B 或 C;
  2. 切档位前,先扫一遍页面上所有你不控制的资源,逐个确认能否自托管或带 CORP;
  3. 把"隔离是否生效"做成启动期可见的诊断(而不是等某个插件行为异常才回头查);
  4. 跨域 iframe 集成一律走 postMessage,不要依赖 window.__storyRuntime。

相关链接