插件式游戏中心的目录与播放边界:一次工程复盘
本文目录
本文是对个人项目当前本地工作树的一次静态源码审阅。检查时游戏目录文件、包配置和 UI 文件存在未提交修改,因此本文不代表 GitHub 默认分支或发布包。项目使用 React、TypeScript 与 Vite;当前工作树有 156 条分类入口引用,覆盖 78 个唯一游戏 ID。为保护项目边界,不公开仓库名称、地址、宿主内部依赖名或具体游戏清单。目录和构建配置能说明代码如何组织,不能证明每个游戏都已发布、授权或完成设备验收。
目录数据和页面职责分开
源码把分类、游戏列表、类型定义、筛选 Hook、目录视图和播放视图放在不同模块。条目数据保存稳定 ID、名称、图片、说明和入口地址;页面根据目录数据呈现卡片、筛选结果与当前选中项。这样新增或调整一个条目不必复制整套页面,分类和搜索也可以围绕同一份数据工作。
这个模型仍需守住两个不变量:
- ID 要稳定且唯一,不能用数组下标作为跨版本身份;改名、排序和新增分类不应让用户收藏或统计串到另一款游戏。
- 展示数据不是可信配置。入口地址必须在加载时再次解析和校验,不能因为 URL 是从前端目录读出来就默认可信。
筛选视图只负责找到内容,播放视图才负责加载内容。把二者拆开,能让列表先显示、搜索不被 iframe 拖慢,并为收藏、最近游玩、加载失败和退出播放预留清晰的状态边界。
当前目录验证脚本实际检查 156 条本地入口:要求入口采用约定的相对路径、解析结果留在游戏资源目录内,且目标文件存在。本轮执行 node scripts/validate-game-catalog.mjs 通过,说明这 156 条引用都能解析。它不验证页面是否运行、重复元数据、入口重定向、外部依赖、逐款授权或 iframe 隔离;路径校验不能替代来源台账与运行验收。
双构建目标不是双重部署保证
仓库将浏览器 UI 与服务端入口设为不同构建目标。这有利于隔离依赖和发布产物,但也要求分别验证入口、环境变量、路由、输出目录和宿主契约;一次 UI 构建成功,不代表服务端目标可用,反过来也一样。共享类型可以减少接口漂移,仍需用版本化 schema 或契约测试验证运行时输入。
源码中有独立的目录视图、播放视图和服务构建配置;本次没有安装这个远端项目的依赖、执行其构建、部署或测试宿主,因此不把这些配置描述成经过运行验收的插件平台。
当前源码的实际权限边界
这次复核 PlayView 后需要更正一个容易误读的前提:目录中的相对游戏地址会被改写成当前站点下的静态资源 URL;播放组件创建 iframe 时只设置了 src 和标题,没有设置 sandbox 或 referrerpolicy。它们因此不是“天然跨源的第三方页面”,而是同源页面。未沙箱化的同源脚本可以访问嵌入页的 DOM 和同源 Web API;若它执行恶意代码,影响面可能包括宿主页面和同源存储。源码展示的目录是静态硬编码数据,没有证据表明访客可以直接提交任意 URL,所以不要把潜在影响写成已经确认的远程利用。
播放组件还会把以 http:// 或 https:// 开头的地址原样用作 src;当前路径上没有 origin 白名单或 HTTPS-only 检查。管理面未来若允许编辑入口,必须先解析 URL 并限制来源、重定向和协议;但即便管理者维护清单,也不能把“管理员输入”当作 iframe 隔离措施。
因此 iframe 安全段落里的 sandbox、来源白名单和 CSP 是发布前要求,不是对当前实现的通过结论。本次只读源码检查没有在 iframe 内执行利用验证,也没有改动 GitHub 远端项目;公开文章不复制仓库源码、项目名或游戏清单。
iframe 是安全边界,不是装饰容器
不可信游戏应优先托管在独立站点,并在父页面使用最小权限的 sandbox、referrerpolicy="no-referrer" 和来源白名单。游戏需要脚本时再单独授予 allow-scripts;不要对同源内容同时授予 allow-scripts allow-same-origin,MDN 明确说明这样的组合可能让同源嵌入内容移除沙箱并重新加载。每个游戏的表单、弹窗、下载、全屏、指针锁定等能力都应逐项证明必要性。
父站 CSP 的 frame-src 应限制页面能嵌入哪些来源,但它不代替 iframe sandbox,也不修复子页面自身的脚本安全。若游戏确实需要和宿主通信,消息接口只定义少量动作与版本化字段;发送时指定确切的 targetOrigin,接收时同时校验 event.origin、event.source 和数据 schema。没有消息需求,就不要注册通用 message 监听器。来源隔离、最小权限和对沙箱的实际行为测试要同时成立。
加载、退出和弱设备体验
游戏应在读者主动点击“开始”后才创建 iframe;首屏先展示固定比例的封面和可读说明,避免嵌入内容改变卡片尺寸或抢焦点。加载中、来源拒绝嵌入、网络失败、超时和退出都要有明确状态,并提供经过标记的“在新标签页打开”替代入口。
退出播放或切换条目时移除旧 iframe,停止其音频、计时和网络活动;重新打开一个游戏不应累计旧的窗口监听器。手机需要检查横竖屏、缩放、触屏、返回键、键盘焦点和低内存设备。浏览器不支持某项能力时,给出静态预览或外链入口,不要让页面无提示地空白。
发布前验收表
- 来源:逐个核对域名、许可、素材归属和嵌入条款;来源不明的条目不上线。
- 隔离:验证 sandbox、CSP、Referrer-Policy、权限白名单与外链回退;在 iframe 内尝试导航、弹窗和下载,确认实际权限符合预期。
- 消息:用错误 origin、错误 source、未知版本、畸形字段和超大 payload 测试拒绝路径;协议不需要消息时保持零监听。
- 生命周期:连续打开/退出多次,确认 iframe、音频、计时器和观察器会停止,不留后台活动。
- 设备:覆盖窄屏、触屏、键盘、返回导航、弱网和低内存;首屏卡片尺寸稳定,用户始终知道当前内容来自外部站点。
- 构建:分别构建 UI 与服务端目标,核对依赖清单、产物大小、环境配置和回滚办法。
这次文章是源代码结构复盘,不是对远端游戏宿主、第三方游戏许可或线上 Demo 安全性的认证。把一个入口嵌进页面只是开始;可解释的来源、最小权限、退出清理和可重复验收,才让它成为可以长期维护的博客能力。
参考:MDN iframe、MDN postMessage、Content Security Policy、Vite 生产构建。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。