Files
photo-live/packages/embed-loader/README-embed.md
T
浅念 3bba8d1dc5 feat(admin,viewer,ui04): E06 management preview and v2 integration — real-component preview page, form mode fields, README
- viewer /embed-preview route: real EmbedStream with sample photography (picsum seeds), zero requests/WS (preview mode), postMessage config + simulated bursts through the real validation pipeline
- EmbedStream: preview prop (disables real WS subscription per §8.2 isolation) + exposed simulateBurst
- admin form: displayMode radio (new default), fixed 195x280 readonly, speed slider, reactions switch; v1 layout fields hidden in v2 (not silently effective)
- admin preview: v2 branch embeds the real preview iframe, white v2 handle, simulate-like tool button with sample-data note
- README-embed: v2 section (integration, hard sizing/docking, interactions/limits, artifact/version caching)
- tests: admin vitest 83 (+1 v2 placement), admin e2e 5 (v1 pinned carousel regression + full v2 chain incl. preview WYSIWYG heart and ov settings projection), viewer 244, v2 e2e 16

Note: part of the admin-side E06 changes (form/preview/utils/locales, mid-state of e2e spec) was swept into parallel commit eebf3b3 by a concurrent session's git add; this commit carries the final state of the remaining files and completes the set.

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

9.4 KiB
Raw Blame History

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 源为 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|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 端口跨源静态页)。


UI-04 v2:垂直照片流窗(/embed/v2/loader.js)

新版展示模式:PC/手机均固定 195×280 CSS px、单列照片连续自动上滚、真实点赞 心形气泡、无标题栏与底部工具栏(仅右上收起入口;点击照片打开观众深链)。 v1(旧轮播)源码冻结保留——存量接入不受影响;切换新版需更新 script 地址。

接入代码

<script src="{EMBED_ORIGIN}/embed/v2/loader.js" data-slot="{publicToken}" defer></script>
  • 后台展示位 displayMode=vertical-stream 时,嵌入代码卡自动生成 v2 入口;
  • data-speed(8–32,像素/秒,缺省 16)与 data-reactions="off" 为显式覆盖, 缺省让后台设置生效(优先级同 v1:data-* > 后台 > 默认);
  • v2 下 data-size / data-interval / data-image-fit 无布局作用(固定 195×280、 连续滚动),不生成也不解析。

尺寸与停靠(硬性)

  • 宽 195 / 高 280(border-box,含 1px 白边);视口不足时按 min(195, 可用宽) / min(280, 可用高) 收缩,低于可用下限转收起入口;不整体缩放(transform scale 会同缩文字与触控区);
  • 默认停靠:桌面 right 16 / 手机(≤599px 视口)right 12,bottom 24 + 安全区 (offsetX/offsetBottom 可配置,0–120 / 0–240);z-index 缺省 1000 可配置;
  • 窄屏(<480px)默认收起(与 v1 策略一致);展开后两端尺寸一致;
  • iframe 内容不得经 resize 消息反向改变窗口高度(v2 忽略 resize)。

交互与限制

  • 悬停(鼠标)/键盘焦点/按住暂停,离开 500ms 恢复;Space 显式暂停(不自动 恢复);Escape 收起;↑↓/滚轮/拖动手动滚动(双向回绕,跟手 1:1);
  • 点击照片:pointerdown 锁定目标,拖动 >8px 取消——动画中不点错图;新标签 打开 /a/{albumToken}/p/{photoId}(noopener);
  • 点赞气泡只由真实 likes.burst(WebSocket 只读订阅相册公开 token)驱动: 断线不出泡、不伪造;组件自身不提交点赞、不上报浏览;
  • 减少动态效果(prefers-reduced-motion):停自动滚动与粒子,手动可查看。

产物与版本

  • 构建产物 dist/v1/loader.js 与 dist/v2/loader.js(同 build/sync 脚本; viewer publicDir public/embed/{v1,v2}/ 携带)。版本入口稳定可长缓存 (immutable 语义同 /embed/v1/,见 viewer nginx 模板);升级走新版本路径, 不原地变更已发布文件语义。

v2 行为的端到端覆盖见 viewer Playwright e2e/embed-v2.spec.ts(E01–E05 出口 门/尺寸测量/滚动连续性/交互/真实点赞气泡/录屏证据);管理端表单与真实组件 预览(/embed-preview,样例数据 + postMessage 配置/模拟事件)见 admin e2e/embed-slots.spec.ts UI-04 用例。