ddf26a177f
Co-Authored-By: Claude Code <noreply@anthropic.com>
6.6 KiB
6.6 KiB
PhotoLive 悬浮组件接入说明(embed-loader v1)
宿主页只需引入一个版本化脚本文件即可嵌入照片悬浮轮播组件。加载器零依赖、零全局污染
(仅 window.__photoliveEmbedV1 版本标记)、出错静默不破坏宿主。
接入代码
<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。 仅影响轮播切换,不影响取数轮询(恒 30s 契约基准)。data-image-fit(可选,UI-01):contain(缺省,4:3 舞台完整显示)|cover(填满裁切)。data-offset-x(可选,UI-01):距宿主边缘水平偏移 px,0–120,缺省 16。data-offset-bottom(可选,UI-01):距宿主底部垂直偏移 px,0–240,缺省 24。data-z-index(可选,UI-01):wrapper 层级,1–2147483000,缺省 1000。宿主页面 自身存在高层级元素(如全屏遮罩)时按需调高;把手与卡片共用该层级。data-poll(可选,仅测试):加速取数轮询毫秒数,经 iframe URL&poll=透传, 仅 dev 构建生效(生产构建忽略)。e2e 用;生产接入不需要。
同一页面引入多个不同 data-slot 的标签即嵌入多个展示位;同一 data-slot 重复出现时
后者静默忽略。
配置优先级(UI-01 §5.4)
任一字段:宿主 data-* 显式提供 > 后台展示位设置 > 契约默认值。未提供的
data-* 不视为覆盖,后台设置变更经 iframe 轮询发现后以 config 消息动态应用
(loader 二次校验:仅白名单字段 + 版本单调递增,不信任任意 CSS/HTML)。
多槽与响应式(UI-01)
- 同侧多槽纵排:同一侧(left/right)多个展示位按引入顺序自下而上堆叠, 间距 8px;视口高度不足时后引入的槽自动收起为把手(不写用户偏好——空间约束 非用户选择)。
- 窗口变化:
resize/orientationchange重新评估全部几何;宽度取「配置宽 与视口剩余宽」的较小值(极窄视口下限 180px 防溢出)。 - 窄屏(viewport <480px):强制小尺寸(280px 宽)且默认收起为把手。
- 收起把手:图片图标 + 「照片 / Photos」胶囊按钮,高 44px、有效点击区
≥44×44、可键盘操作(Enter/Space)。收起状态会话记忆在
sessionStorage(键pl-embed-collapsed:{slot}),刷新后保持,不反复弹出。
CSP 要求(宿主页)
| 指令 | 值 | 说明 |
|---|---|---|
script-src |
{EMBED_ORIGIN} |
加载 loader.js |
frame-src |
{EMBED_ORIGIN} |
悬浮轮播 iframe({EMBED_ORIGIN}/embed/{slot}) |
组件自身不内联脚本;iframe 内图片来自对象存储签名域名(img-src 无需为组件放开
项目域)。frame-ancestors 由我们的 iframe 页配置(限制可嵌入站点),不是宿主
的关注点。
生命周期消息(iframe → 宿主)
| type | 触发 | loader 行为 |
|---|---|---|
resize |
iframe 内容高度变化 | wrapper 高度夹取 60–640px 应用 |
collapse |
用户点击 iframe 内 × | 收起为把手 + 会话记忆 |
expand |
(保留) | — |
config |
后台设置变更(configVersion 升值) | 校验后动态应用非 data-* 显式字段 |
destroy |
槽 404/410/停用/解绑(服务端终止展示) | 移除 wrapper + 把手 + 会话键,不留无效按钮(与用户收起语义不同) |
宿主 → iframe 仅 expand(把手点击,目标源严格为 iframe 源)。
安全模型
- iframe sandbox:
allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin。必须含allow-same-origin:否则 iframe 源为 opaquenull, 下方 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|config|destroy, ...}; resize 高度夹取 60–640px,config 经版本闸 + 字段白名单校验。向 iframe 发消息 (把手展开)同样以 iframe 源为目标,绝不使用*。 - 零信任宿主数据:slot token 经 encodeURIComponent 进 URL; lang/position/interval/imageFit/offset/zIndex 白名单或夹取校验后才拼入查询串。
- 存储与 Cookie:loader 不读写 Cookie、不写宿主 localStorage;仅上述 sessionStorage 收起标记(destroy 时一并清除)。无埋点、无对外数据上报。
- DOM 足迹与样式免疫:仅向 body 追加 wrapper 与收起把手两个 div(全部 inline style,不注入样式表/类名,不影响宿主 CSS,也不受宿主全局样式影响——敌意 reset 下组件仍保持 fixed 定位与几何)。
产物与发布
- 发布物即文件本身(不走 npm):
packages/embed-loader/dist/v1/loader.js(esbuild bundle + minify,IIFE,target es2018)。 - 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/。
开发
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 出现/收起/记忆/窄屏/多槽堆叠/跨源宿主/postMessage 白名单/
后台设置动态生效)的端到端覆盖见 viewer Playwright e2e/embed.spec.ts 与
e2e/embed-crossorigin.spec.ts(宿主 fixture 页 / 3999 端口跨源静态页)。