Files
photo-live/packages/embed-loader/README-embed.md
T
浅念 ddb4c8175f feat(embed): versioned loader with postMessage whitelist
packages/embed-loader:手写零依赖加载器(esbuild bundle+minify→dist/v1/loader.js,
IIFE target es2018 4.7KB)。自发现 EMBED_ORIGIN(currentScript 兜底扫描)、
data-slot 必填缺失静默 no-op、__photoliveEmbedV1 标记去重+同槽去重、
窄屏 <480px 强制 sm+默认收起、postMessage 双向白名单(source+origin 严格
校验,resize 夹取 60-640)、sessionStorage 收起记忆、零全局零 console
全静默。sync 脚本携 sha256 校验拷贝至 viewer publicDir(两份产物入库);
src/params.js 为 loader/单测共用参数源(11 vitest 用例)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 22:31:40 +08:00

78 lines
4.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PhotoLive 悬浮组件接入说明(embed-loader v1)
宿主页只需引入一个版本化脚本文件即可嵌入照片悬浮轮播组件。加载器零依赖、零全局污染
(仅 `window.__photoliveEmbedV1` 版本标记)、出错静默不破坏宿主。
## 接入代码
```html
<script
src="https://{EMBED_ORIGIN}/embed/v1/loader.js"
data-slot="{展示位 token(后台展示位管理页复制)}"
defer
></script>
```
- `data-slot`(必填):展示位 publicToken;缺失时加载器静默不渲染任何内容。
- `data-lang`(可选):`auto`(缺省,按相册 defaultLocale)| `zh-CN` | `en`,非法值回退 `auto`。
- `data-position`(可选):`right`(缺省)| `left`,悬浮侧。
- `data-interval`(可选):轮播间隔毫秒,夹取到 3000–30000,缺省 5000。
同一页面引入多个不同 `data-slot` 的标签即嵌入多个展示位;同一 `data-slot` 重复出现时
后者静默忽略。位置/尺寸/语言等展示位级设置以后台配置为准,data-* 为宿主侧覆盖。
## CSP 要求(宿主页)
| 指令 | 值 | 说明 |
| --- | --- | --- |
| `script-src` | `{EMBED_ORIGIN}` | 加载 loader.js |
| `frame-src` | `{EMBED_ORIGIN}` | 悬浮轮播 iframe(`{EMBED_ORIGIN}/embed/{slot}`) |
组件自身不内联脚本、不加载其他域资源。`frame-ancestors` 由**我们**的 iframe 页在 P5
配置(限制可嵌入站点),不是宿主的关注点。
## 窄屏行为
viewport 宽度 <480px 时:组件强制降为小尺寸(280px 宽)并默认收起为侧边圆形把手,
点击把手展开。会话内收起状态记忆在 `sessionStorage`(键 `pl-embed-collapsed:{slot}`),
刷新后保持收起,不反复弹出。
## 安全模型
- **iframe sandbox**:`allow-scripts allow-popups allow-popups-to-escape-sandbox
allow-same-origin`。必须含 `allow-same-origin`:否则 iframe 源为 opaque `null`,
下方 origin 白名单会拒掉自己 iframe 的合法消息,且 iframe 内调公开 API 需要全站
ACAO `*`。iframe 对宿主天然跨源(宿主域 ≠ 项目域),`allow-same-origin` 仅使
iframe 保有**项目域自己的**源,不会获得宿主页源。
- **postMessage 双向白名单**:loader 仅接受「`event.source` 等于已登记 iframe 的
`contentWindow` 且 `event.origin` 严格等于该 iframe src 源」的消息,且载荷须为
`{photoliveEmbed: true, type: resize|collapse|expand, ...}`;resize 高度夹取
60–640px。向 iframe 发消息(把手展开)同样以 iframe 源为目标,绝不使用 `*`。
- **零信任宿主数据**:slot token 经 encodeURIComponent 进 URL;lang/position/interval
白名单校验后拼入查询串。
- **存储与 Cookie**:loader 不读写 Cookie、不写宿主 localStorage;仅上述
sessionStorage 收起标记。无埋点、无对外数据上报。
- **DOM 足迹**:仅向 body 追加 wrapper 与收起把手两个 div(全部 inline style,
不注入样式表/类名,不影响宿主 CSS)。
## 产物与发布
- 发布物即文件本身(不走 npm):`packages/embed-loader/dist/v1/loader.js`
(esbuild bundle + minify,IIFE,target es2018,≤5KB 目标)。
- **viewer 构建携带**:`pnpm --filter @photo/embed-loader run sync` 将产物拷贝至
`apps/viewer/public/embed/v1/loader.js`(sha256 校验)。viewer 的 vite publicDir
使 dev server 直接可访问、`vite build` 自动将其带入 dist/。两份产物均提交入库,
修改 loader 后需重新 build + sync。
- 路径 `/embed/v1/` 长期稳定;破坏性变更才会出现 `/embed/v2/`。
## 开发
```bash
pnpm --filter @photo/embed-loader run build # 打包 dist/v1/loader.js
pnpm --filter @photo/embed-loader run test # params 纯函数单测(vitest)
pnpm --filter @photo/embed-loader run sync # 拷贝至 viewer public/(含 hash 校验)
```
宿主页交互行为(iframe 出现/收起/记忆/窄屏)的端到端覆盖见 viewer Playwright
`e2e/embed.spec.ts`(P4b Task 4,宿主 fixture 页)。