修复在线游玩加载体验:加载期黑屏/空白与发行资源传输开销
- packages/shared 新增共享游戏加载面 PlatformGameLoadingSurface:封面/标题 + 动效进度 + 按时长推进的阶段文案(准备/资源/引擎)+ 慢加载提示,附用例 - 网页游玩页用共享加载面替换近黑壳上的一行小字,加载上限改为共享常量 PLATFORM_GAME_LOADING_TIMEOUT_MS - 后台审核试玩补加载面、20 秒超时与「重新创建试玩会话」入口,新会话重置加载态;补两条用例 - 发行网关(api-server)对文本类发行资源按 Accept-Encoding 下发 gzip,并下发强 ETag、命中 If-None-Match 返回 304;补 5 条 Rust 用例 - E2E 脚本 check-game-distribution-ops-rollback-e2e 补压缩/Vary/ETag/304/不接受 gzip 四条断言 - 同步 pitfalls、开发运维文档、nginx README 与两份里程碑记录
This commit is contained in:
@@ -49,3 +49,10 @@
|
||||
- 自动化:admin-web 详情/动作测试、api-server 预览会话和权限测试、DTO/类型检查、编码和 diff 检查。
|
||||
- 运行时:本地管理员打开待审版本,确认资料、封面/截图和待审包试玩;确认旧公开版本不会被误播。
|
||||
- 边界:过期 Token、错误 versionId、Cookie、越权管理员、缺入口、资源 404、CAS 冲突和幂等重放。
|
||||
|
||||
## 本轮核对(2026-10-05,试玩加载体验与超时收口)
|
||||
|
||||
- 「试玩当前待审版本」原先只渲染 `<iframe class="admin-game-review-preview">`:加载期是一块 `min-height: 42rem` 的空白,没有加载文案、没有超时、没有失败提示(会话过期或资源 404 会永远停在空白)。2026-10-05 起与网页游玩页共用共享加载面 `PlatformGameLoadingSurface`(`packages/shared`):加载期显示游戏标题/封面/动效进度与阶段文案,iframe `onLoad` 后才让位给画面。
|
||||
- 加载上限与网页游玩页统一为 `PLATFORM_GAME_LOADING_TIMEOUT_MS`(20 秒):超时后加载面换成「试玩版本加载超时」并给出「重新创建试玩会话」入口;新建会话会重置加载态,不会沿用上一个会话的 ready。
|
||||
- 用例:`apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx` 新增两条(加载面在画面载入后让位、超过上限后给出超时说明与重建入口,重建后新会话 URL 生效且回到加载态)。
|
||||
- 仍未变化:预览会话 token 与 `no-store`、`sandbox="allow-scripts"`、Cookie 拒绝、版本绑定与审核 CAS 口径都不动。
|
||||
|
||||
@@ -333,3 +333,9 @@
|
||||
- **详情页返回按来源回到上一页**:`GameDetailPage` 左上角原来是写死的「返回游戏广场」,并且直接 `setSelectionStage('games')`(等于 push 一条新的 `/games`)。从「我的游戏」点进详情再返回就落到广场,从广场进详情返回也会多压一条重复历史。现在文案简化为「返回」,行为改成优先 `window.history.back()`,由 `ActiveApp` 已有的 `popstate` 同步把舞台还原成真正的来源页(我的游戏 / 广场);游玩页的返回同样处理,避免详情↔游玩互相 push。协议侧给应用写入的历史条目补了 `__genarrativeAppHistoryDepth`,新增 `hasAppHistoryBackEntry()` 判断「当前条目是应用内导航写入的且存在上一页」;直接打开详情深链、或原生壳里没有可回退条目时,才 `replaceAppHistoryPath` 兜底到广场(游玩页兜底到自己的详情)。回归用例 `activeAppPageRoutes.test.ts` 锁定深度标记语义,`PlatformEntryActiveFlowShell.test.tsx`「游戏详情返回」两条锁定原生返回与深链兜底。
|
||||
- **游戏分发的返回文案统一成「返回」**:`GameDetailPage`、`GamePlayPage`(含超时面板里的按钮)、`MyGamesPage`、`GamePublishPage` 四处原本是「返回游戏广场 / 返回详情」,同一套流程里出现三种写法;现在统一成「返回」,目标页仍由各自的 `onBack`(广场 / 我的游戏 / 详情)决定,按钮不再承诺一个可能不对的目的地。`GameDistributionPages.test.tsx` 的断言同步改为 `/^返回$/u`。游玩页工具栏上那个按钮展示的是游戏名(退出开始面板后就靠它认游戏),不属于「返回 xx」文案,保持不动。
|
||||
- 验证:`npx vitest run src/components/game-distribution/GameDistributionPages.test.tsx`(21 passed)、`npx vitest run src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx`(22 passed)、`npx vitest run src/routing/activeAppPageRoutes.test.ts`、`npm run typecheck`、`npx eslint`(五个改动文件)、`npm run check:encoding`、`git diff --check` 全部通过;启动面板的溢出可达性、「我的游戏」移动端横向溢出都用真实 Chromium 静态夹具量过(见 pitfalls 同日条目),详情返回链在真实 Chromium(dev 栈 390x844)实测 `/games`→详情→返回=`/games`、深链直开详情→返回=`/games`、详情→立即玩→返回详情=`/games/detail?id=…`。视觉与真机横竖屏走查仍需人工/截图评审(沿用阶段 C 第 8 条未取证口径)。
|
||||
|
||||
## 本轮核对(2026-10-05,游玩页加载体验与发行资源传输)
|
||||
|
||||
- 游玩页此前在 `onLoad` 前只盖一行 `游戏正在启动…`(近黑壳上),用户反馈「一直黑屏」。现在加载期显示共享加载面 `PlatformGameLoadingSurface`(`packages/shared`,封面/标题 + 动效进度 + 按时长推进的阶段文案),`onLoad` 后淡入画面;超时面板与重试/返回不变,上限仍与后台试玩统一为 `PLATFORM_GAME_LOADING_TIMEOUT_MS`(20 秒)。
|
||||
- 发行网关(`api-server`)对文本类发行资源下发 `Content-Encoding: gzip` + `Vary: Accept-Encoding`,并下发强 `ETag`:`If-None-Match` 命中返回 304。`max-age=60, must-revalidate` 与撤销窗口不变,差别只是 60 秒之后重复游玩不再重下整包。后台试玩会话仍是 `no-store` 且无 ETag。
|
||||
- 证据:真实栈 4 Mbps/100 ms 模拟链路对照(改前 3298 ms / 4587 ms 才出画面,且期间无有效加载反馈),发行网关字节数对照见 PR;`cargo test -p api-server --bin api-server -- game_distribution` 与三条前端用例(共享加载面、游玩页、后台试玩)为回归门禁。
|
||||
|
||||
@@ -6406,3 +6406,12 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- **别踩**:不要写成裸前缀正则(`^/pay`)——它会吞掉 `/payment/x`、`/paycheckout/x` 这类同名邻居;也不要把深链塞进精确 allowlist 的 alternatives 里(`pay` 的 alternatives 只匹配 `/pay`)。
|
||||
- **判据/取证**:`node --test scripts/check-nginx-spa-routes.test.mjs`(正/反用例,含「写回精确匹配即红」)、`npm run check:nginx-spa-routes`、`npm run check:pingora-route-parity`、`cargo test -p pingora-gateway -- pay_checkout_deep_link matches_nginx_route_parity_matrix`;线上复验 `curl -s -o /dev/null -w '%{http_code}' https://<平台域名>/pay/<checkoutToken>` → 200 且正文与 `/` 同一份 `index.html`。
|
||||
- **关联**:`scripts/check-nginx-spa-routes.mjs`、`deploy/pingora/nginx-route-parity.matrix.json`、`server-rs/crates/pingora-gateway/src/main.rs`、`server-rs/crates/api-server/src/payment.rs`、`deploy/nginx/genarrative.conf`。
|
||||
|
||||
## 2026-10-05 在线游玩「一直黑屏」:加载面缺失 + 发行网关不压不发 ETag
|
||||
|
||||
- **现象**:用户反馈「进入游玩…加载有点慢,一直黑屏体验不好」。真实栈(真实发行包 + Chromium + 4 Mbps/100 ms 模拟链路)实测:网页游玩页点击「开始游戏」后 99.3% 像素亮度 < 24 的近黑面板 + 一行 `游戏正在启动…`,游戏画面 **3298 ms** 才出现;后台审核页点「试玩当前待审版本」后 iframe 直接以 `opacity:1` 出现、区域**全白空白** 4587 ms,页面**全程没有任何加载文案**。
|
||||
- **根因**:① 网页游玩页的加载态只有一行小字盖在 `#17131b` 近黑壳上,没有封面/进度/阶段文案,`ready` 判定又只看 iframe `onLoad`(文档加载完成 ≠ 游戏可玩);② 后台试玩只有 `<iframe class="admin-game-review-preview">`,没有加载态、没有超时、没有失败处理,会话过期或资源 404 会永远停在空白;③ 发行网关把 ZIP 解压后的原文直出,`phaser.min.js` 1,375,976 B 原样下发(`Content-Encoding: none`),也没有 ETag —— 4 Mbps 下光这一个文件就 ~2.7 s,且 60 秒 `max-age` 过后浏览器只能重下整包。
|
||||
- **处理(现行口径)**:新增共享组件 `PlatformGameLoadingSurface`(`packages/shared`,封面/标题 + 动效进度 + 按时长推进的阶段文案 + 慢加载提示),网页游玩页与后台试玩共用;两端的加载上限统一为 `PLATFORM_GAME_LOADING_TIMEOUT_MS`(20 秒),后台超时后给出「重新创建试玩会话」。发行网关对文本类资源(HTML/JS/CSS/JSON/SVG/WASM,≥1 KiB、客户端接受 gzip)下发 `Content-Encoding: gzip` + `Vary: Accept-Encoding`,并下发按 `versionId + 资源路径` 摘要的强 ETag,命中 `If-None-Match` 返回 304(无正文)。
|
||||
- **边界**:镜像/音频/视频等已是压缩格式的资源不压;后台试玩会话仍是 `no-store` 且不给 ETag;`max-age=60, must-revalidate` 与撤销窗口不变(换版/下架仍最迟 60 秒对新请求生效,304 只是在窗口之后省掉重下)。边缘 gzip/Brotli 与本层不冲突:两边都以「响应已带 `Content-Encoding` 就跳过」收口;但 Pingora 网关在自身压缩关闭时会**移除** `accept-encoding`(`normalize_accept_encoding_for_gateway_compression`),那种部署形态下源站压缩不会生效,属于网关侧口径。
|
||||
- **验证**:`cargo test -p api-server --bin api-server -- game_distribution`(新增 ETag 作用域、`If-None-Match` 列表/弱校验命中、`gzip;q=0` 拒绝、文本压缩与二进制/小文件不压、304 无正文等用例);`npx vitest run packages/shared/src/components/PlatformGameLoadingSurface.test.tsx`、`src/components/game-distribution/GameDistributionPages.test.tsx`、`apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx`;真实栈复跑探针后发行网关 `phaser.min.js` 由 1,376 KB 降到约 330 KB,用户端/后台端改后同链路对照见 PR。
|
||||
- **关联**:`packages/shared/src/components/PlatformGameLoadingSurface.tsx`、`src/components/game-distribution/GamePlayPage.tsx`、`apps/admin-web/src/pages/AdminGameDistributionReviewPage.tsx`、`server-rs/crates/api-server/src/modules/game_distribution.rs`、`deploy/nginx/README.md`。
|
||||
|
||||
@@ -744,6 +744,7 @@ Jenkins 按 web / api / Spacetime module / build / deploy / publish 拆分
|
||||
- 路由约定:`https://<平台域名>/games/<gameId>/` 是该游戏的入口,`/games/<gameId>/<asset>` 映射到发行网关 `/api/game-distribution/releases/<gameId>/<asset>`。同源发行入口 location 写在 `deploy/nginx/genarrative.conf`(生产 HTTPS)、`deploy/nginx/genarrative-dev-http.conf`(开发 HTTP)与 `deploy/container/nginx.conf`(容器)里。正则必须整体加双引号:`location ~ "^/games/(?<game_id>game_[0-9a-f]{32})(?<game_path>/.*)?$"`,否则 nginx 会把 `{32}` 当块定界符并在 `nginx -t` 报 missing closing parenthesis。
|
||||
- 会话与隔离:边缘在转发前 `proxy_set_header Cookie ""`,发行网关自身也对带 `Cookie` 的请求返回 `403`;游戏文档跑在 iframe `sandbox="allow-scripts"` 的不透明来源里,读不到主站 Cookie、storage 与 DOM,离开页面即随 iframe 卸载整套游戏代码。
|
||||
- 响应头与缓存:`X-Content-Type-Options`、CORP(`cross-origin`)、无凭据 CORS、HTML CSP、内容类型白名单与 `Cache-Control: public, max-age=60, must-revalidate` 都由发行网关设置,边缘不覆盖。换版与下架只改变后端公开投影,因此**最迟 60 秒**内新请求不再拿到旧版本;已经下载到浏览器的脚本无法远程抹除,撤销能力以"停止继续分发"为准。
|
||||
- 传输与条件请求(2026-10-05):发行网关对文本类资源(HTML/JS/CSS/JSON/SVG/WASM,≥1 KiB)在客户端接受 `gzip` 时下发 `Content-Encoding: gzip` 并带 `Vary: Accept-Encoding`;图片、音频、视频等已是压缩格式的资源保持原样。同时下发强 `ETag`(按 `versionId + 资源路径` 摘要),命中 `If-None-Match` 时返回 `304`(无正文、保留 `Cache-Control` 与 `ETag`),所以 60 秒缓存窗口之后浏览器只需重验证而不是重下整包;`max-age` 与 `must-revalidate` 都不变,撤销窗口仍是 60 秒。后台试玩会话固定 `no-store`,不给 ETag。
|
||||
- 审核动作:管理员只提交审核结论与公开修订号,`entryUrl` 由 `api-server` 按 gameId 派生**同源路径** `/games/{gameId}/` 写入公开投影;dev / release / 预览环境口径完全一致,入口不再由部署侧配置,历史数据里的绝对 URL 继续兼容。
|
||||
- 门禁:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user