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

4.0 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。

同一页面引入多个不同 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/。

开发

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 页)。