一个 URL 背后的三层机制,以及该把哪个链接写进 README

kof-3d 在线试玩页的实现方法、工作原理,与「保留 github.io 多一跳 301」/「改成自定义域主链接」的实证对比

2026-11-06对象:https://wangjunjian.com/kof-3d/ 仓库:wang-junjian/kof-3d @ 25b3a86零外部依赖 · 手写 SVG 图表

00摘要 · 八条可证伪的核心论断

只读这一页也能拿走结论;后续每一节都是其中某条论断的展开与举证。

  1. 这个 URL 能打开,是域名路由、构建发布、浏览器运行三层机制叠加的结果,三层分别由不同的仓库与脚本负责,互不耦合。
  2. 域名层不是本项目配置的:自定义域 wangjunjian.com 归用户站点仓库所有,本仓库 cname=null;项目站点因此被 301 到自定义域子路径,全程零 DNS 改动。
  3. 发布层由 .github/workflows/pages.yml 驱动,每次 push 到 main 重建并部署;仓库只提交源码,可玩的单文件产物不入库。
  4. 单文件不是「把文件拼起来」那么简单:vite.config.ts 强制单 chunk,scripts/inline.mjs 做防御式内联,两者共同保证线上与 file:// 双击是同一个字节。
  5. 浏览器内以固定 60Hz 逻辑步进运行;键盘、触屏、手柄先归约成 8 个 InputRole 并按来源分桶,再进入与设备无关的纯逻辑层。
  6. 「一键 236」不是第二套规则:它把 ↓ / ↘ / →+攻击 排成 3 个逻辑帧,复用与键盘手摇完全相同的 detectQcf() 判定链。
  7. 保留 github.io 的代价可量化:本机 8 次采样中,跨主机重定向使 num_connects 从 1 变 2,time_redirect 中位数 0.980s。
  8. 这不是二选一:自定义域更体面也更快,但链接寿命受制于另一个仓库的 cname 配置;github.io 是 GitHub 官方兜底的永久地址。建议主链接换域名、备链保留。
线上单文件801 938 B约 783 KB,与本地产物 SHA256 一致
两层断言67 + 97纯逻辑单测 / 浏览器端到端断言
A 方案额外连接2 vs 1curl num_connects,跨主机必须新建连接
A 方案重定向净开销0.980 scurl time_redirect 中位数,n=8

01三层机制总览

把「打开一个链接」拆到底,会看到三个彼此独立的责任层。分层是理解后面所有实现细节的前提,也是排查线上问题时的定位顺序。

kof-3d 在线试玩页的三层机制 域名与路由层由主站仓库的自定义域和本仓库 Pages 构成;构建与部署层由 Actions 与单文件内联脚本构成;浏览器运行层由固定步进循环与设备归约输入构成。 一个 URL 能打开,是三层机制叠加的结果;每层由不同的仓库 / 脚本负责 ① 域名与路由 主站仓库持有 cname,本仓库只启用 Pages 主站仓库 cname 本仓库 Pages 站点 301 → 子路径 ② 构建与部署 push main 自动重建,构建产物不入库 强制单 chunk 内联与校验 deploy-pages ③ 浏览器运行 固定 60Hz 逻辑步进 + 渲染插值 FixedLoop 60Hz InputRole × 8 detectQcf()
图 1 · 三层机制与各自的责任主体。排查线上问题时按此顺序定位:先确认 301 与 Pages 配置,再看构建产物,最后才怀疑游戏逻辑。
层责任主体关键文件 / 配置失效时的典型症状
域名路由用户站点仓库 + GitHub Pages 规则wang-junjian.github.io/pages 的 cname301 消失或目标域名变化,链接整体失效
构建发布本仓库 Actions 工作流.github/workflows/pages.yml、vite.config.ts、scripts/inline.mjs页面 404、白屏、脚本被截断
浏览器运行内联在单文件里的游戏代码src/main.ts、src/core/*、src/view/Controls.ts能打开但不能操作、设备按键无响应

02第一层 · 域名路由:为什么链接落在自己的域名下

这是最反直觉的一层,也是最容易被误认为「改了 DNS」的一层。事实是:本项目一行域名配置都没写。

GitHub Pages 有一条账户级规则:当用户站点仓库(wang-junjian/wang-junjian.github.io)绑定了自定义域,该账户下所有项目站点都会自动挂到这个自定义域的 /<仓库名>/ 路径下,同时从 <username>.github.io/<repo>/ 发出 301。所以只要在 kof-3d 仓库启用 Pages,内容就自动出现在 wangjunjian.com/kof-3d/。

自定义域下的 301 请求时序 访客先请求 github.io 项目路径,得到 301 跳转到自定义域;因跨主机需新建连接并完成第二次 TLS 握手,最终由 kof-3d 仓库的 Pages 返回 200 与单文件页面。 访客浏览器 发起请求 wang-junjian.github.io 只负责 301 跳转 wangjunjian.com 内容来自 kof-3d Pages ① GET /kof-3d/ ② 301 Location: wangjunjian.com/kof-3d/ ③ 跨主机新建连接 + 第二次 TLS 握手 ④ 200 · index.html · 801938 B num_connects = 2 · time_redirect 中位数 0.980s · 本仓库 cname = null · 未改 DNS / 主站仓库
图 2 · 301 的四步时序。关键在第 ③ 步:github.io 与 wangjunjian.com 是不同主机,连接无法复用,因此多一次 DNS / TCP / TLS 往返。

可复核的配置事实

观测项实际值说明证据强度
主站仓库 Pagescname: wangjunjian.com, status: built自定义域归属在这里,不在 kof-3d 仓库配置事实
kof-3d 仓库 Pagescname: null, build_type: workflow只启用 Actions 部署模式,未绑定任何域名配置事实
重定向响应HTTP/2 301 → https://wangjunjian.com/kof-3d/server: GitHub.com实测
目标响应HTTP/2 200, content-length: 801938内容仍由本仓库的 Pages artifact 提供实测
边界提醒:正因为自定义域配置在另一个仓库,这个链接的寿命并不完全由本项目控制。若主站仓库更换域名、取消自定义域或 Pages 自定义域验证失效,wangjunjian.com/kof-3d/ 会直接失效,而 github.io/kof-3d/ 仍由 GitHub 兜底。这是第 06 节对比里最容易被忽略的一项。

03第二层 · 构建发布:从源码到 783 KB 单文件

发布链路解决「内容从哪来」,构建链路解决「为什么内容可以只有一个文件」。后者是这套方案能同时支持在线试玩与离线双击的关键。

npm run build 展开是三步:tsc --noEmit → vite build → node scripts/inline.mjs。vite.config.ts 用 inlineDynamicImports: true、cssCodeSplit: false、assetsInlineLimit: 100_000_000 和固定 entryFileNames: 'game.js',把 Three.js 与全部业务代码压成恰好一个 JS chunk 与最多一个 CSS chunk;base: './' 保证产物里不出现绝对路径。

单文件 HTML 的构建与内联管线 源码经 vite build 强制产出一个 JS 与一个 CSS chunk,再由 inline.mjs 内联进 HTML,最终得到零外部引用的 kof-3d.html,线上与本地双击是同一个字节。 源码与依赖 TS + Three.js + CSS vite build 强制 1 JS + 1 CSS inline.mjs 内联 + 防御式校验 kof-3d.html 783 KB · 零外链 同一个字节:线上 Pages / file:// 双击 / 下载附件 <style> 全部 CSS Three.js + 游戏逻辑 DOM 骨架 + HUD 校验:JS chunk 恰好 1 个 · 转义 </script 与 import.meta · 剥 modulepreload · 普通 script 放 body 末尾
图 3 · 构建管线的产物形态。默认版角色是纯 3D 几何、零贴图;只有 VRM 插槽演示版才把 assets/character.vrm 以 base64 内联,因此体积约 3.8 MB,不占 < 1 MB 的默认版约束。

四个决定成败的细节

细节做法不这么做的后果
脚本类型内联为普通 <script>,不是 modulefile:// 下 module 触发 CORS,离线双击直接白屏
插入位置放在 </body> 之前普通 script 没有 module 的 defer 语义,DOM 尚未解析完就取不到 HUD 节点
转义与剥离转义 </script / </style、替换 import.meta、删除 preload / modulepreload压缩产物里的字符串会提前闭合标签,或残留对 ./game.js 的请求
多 chunk 防御JS chunk ≠ 1 时直接抛错旧实现会静默用最后一个 chunk 覆盖前面的,产出「能打开但脚本残缺」的坏文件

发布工作流

.github/workflows/pages.yml 在 push 到 main(或手动 workflow_dispatch)时触发两个 job:build 跑 npm ci → test → typecheck → build,再组装 site/——把单文件复制成 index.html 与 kof-3d.html,加 .nojekyll 阻止 Jekyll 处理,用 build.txt 记录 $GITHUB_SHA 供线上溯源;deploy 调官方 deploy-pages@v4。permissions 只授 contents: read / pages: write / id-token: write,concurrency 用 cancel-in-progress: false 避免两次部署互相踩出站点空档。

CI 只跑纯逻辑层,因为 npm run verify 依赖本机 Chrome;浏览器端到端断言留在本地验收。这是有意的能力边界,不是遗漏。

04第三层 · 浏览器运行:固定步进与设备归约

单文件加载后,window.__kof.ready 置位即代表 Three.js 场景、帧表与 HUD 都已就绪。之后所有玩法都跑在一个与设备无关的纯逻辑层上。

src/main.ts:245 的 FixedLoop 把逻辑固定在 60Hz:每个逻辑帧先 controls.sync()(Gamepad 是轮询设备,没有 DOM 事件,必须在采样前汇入),再 match.step(input.sample());渲染回调按 alpha 插值更新姿态、特效与镜头。因此浏览器帧率波动不会改变判定结果,顿帧(hitstop)冻结逻辑时视觉时间也一起冻结。

输入归约与 236 判定链路 键盘、触屏虚拟键与 Gamepad 归约为 8 个 InputRole 汇入 InputBuffer;每逻辑帧 sample 出 InputFrame,由 Match 维护 20 帧历史并调用 detectQcf,最终由 Fighter 比对面向并消费 50 POW。 设备层 · 三种物理设备归约成同一套输入角色 键盘 / 触屏 / 手柄 三种物理设备 InputRole × 8 按 source 分桶持有 InputBuffer.sample() 产出 InputFrame match.step(input.sample()) 逻辑层 · 纯函数,设备无关,可在 Node 里单测 Match 20 帧历史 只记可推进对局的帧 detectQcf() 返回绝对方向 Fighter 起招 比对 facing + 扣 50 POW queueQcf() 把 ↓ / ↘ / →+攻击 排成 3 个逻辑帧,与键盘手摇复用同一判定链
图 4 · 两阶段输入链路。分界线以下全部是纯逻辑,可以在 Node 里用假手柄、假输入帧做单测,这也是 CI 不需要浏览器的原因。

三个不显眼但决定手感的设计

05可复核的验证:每个结论都有一条命令能复跑

这一节把前面所有断言压成可执行的观测。证据强度按「配置事实 / 实测 / 代码事实 / 推断」标注,实测数据注明样本量与口径。

结论复跑命令观测值强度
产物只提交源码git ls-files kof-3d.html无输出;.gitignore 忽略两份单文件与 shots/配置事实
单文件零外部引用grep -oE '(src|href)="[^"]*"' kof-3d.html无外部资源;<script> 与 <style> 各 1 个实测
线上与本地同一字节shasum -a 256 kof-3d.html 对比 curl -sSL …/index.html | shasum -a 256两者均为 0c9183d2…0fd16实测
部署溯源curl -sSL https://wangjunjian.com/kof-3d/build.txtPlayable build of commit 25b3a86c…实测
两层断言全绿npm test / npm run verify67 / 67 单测、97 / 97 浏览器断言,连跑 5 次实测
线上零报错、零外部请求本地 Chrome 打开线上 URL 抓 pageerror / console.error / requestfailed三项均为 0,__kof.ready 正常实测
Actions 部署成功gh run view 42140549807success,build 15s、deploy 9s实测
指令识别窗口单一来源读 src/core/input.ts / src/core/match.tsQCF_WINDOW = 20,仅 Match 维护历史代码事实
数据口径:本机 macOS、家庭网络、2026-11-06 单次会话采集。延迟类数字是单一案例,不是受控实验,不能外推到其他地区与网络;但 num_connects、content-length、SHA256 这类计数与内容类观测不受网络抖动影响。

06两个方案的正面对比

问题收窄成一句话:README 与仓库 homepage 继续写 github.io(方案 A,现状),还是改成 wangjunjian.com/kof-3d/(方案 B)。两者页面内容完全相同,差别只在访客抵达内容之前的那段路。

方案 A · 保留 github.io

现状。README 顶部与 homepage 都指向 https://wang-junjian.github.io/kof-3d/,浏览器收到 301 后跳到自定义域。可用,但每次首访多一次跨主机往返。

方案 B · 改成主链接

把这两处文本改成 https://wangjunjian.com/kof-3d/,访客一步直达 200。不改 DNS、不动主站仓库、不碰构建脚本。

方案 A 与方案 B 的首访耗时对比 本机 8 次采样中位数:A 经 301 重定向总耗时 3.279 秒,其中重定向阶段 0.980 秒;B 直达总耗时 2.788 秒,连接数 1 次而非 2 次。 n = 8 次采样取中位数 · curl time_total / time_redirect · 本机网络,单一案例 A · github.io 多一跳 301 301 · 0.980s 正文传输 · 2.299s 3.279s B · 自定义域 零重定向 直达 200 · 2.788s 2.788s 0s 1.0s 2.0s 3.0s num_connects:A = 2,B = 1 · TLS 中位数:A = 0.945s,B = 0.567s · total 差值 0.491s 受正文波动影响,勿单独解读
图 5 · 首访耗时分解。条形按同一线性刻度换算(x = 220 + 194.3 × 秒数),两行共用刻度,可直接比长度。time_redirect = 0.980s 是 curl 直接给出的重定向阶段耗时,比两次独立采样的 total 差值更纯粹。

实测数据

指标A · github.io(跟随 301)B · 自定义域直达口径与强度
num_redirects10计数实测
num_connects21计数实测
time_appconnect 中位数0.945s0.567sn=8,本机
time_redirect 中位数0.980s不适用n=8,本机
time_total 中位数3.279s2.788sn=8,波动大
响应内容同一份 index.html,801938 B,SHA256 0c9183d2…内容实测

采样区间:A 的 total 最小 2.339s / 最大 4.056s,B 的最小 1.844s / 最大 5.274s。两组分布明显重叠,因此「B 一定更快」这个说法证据不足;能站得住的结论是「B 少一次连接、少一次 TLS、少一段确定的重定向阶段耗时」。

对比矩阵

维度A · 保留 github.ioB · 改成主链接谁更好
首访往返1 次 301 + 跨主机新连接 + 第二次 TLS直接 200,单连接B
回访差异301 可被浏览器缓存,但跨主机连接仍无法复用;差距缩小但不归零B 略优
品牌与信任显示 github.io 子域,像演示页自有域名,像正式产品B
分享预览 / SEO301 权重传递基本无损,但预览卡片可能展示原始 github.io 地址预览即最终地址B
链接寿命与依赖GitHub 官方地址,本项目自己可控依赖主站仓库的 cname 配置,域名一变全断A
fork / 迁移友好度改用户名即可推导新地址硬编码个人域名,fork 后指向原站A
改动成本零改动(现状)2 处文本:README 第 5 行 + gh repo edit --homepageA 略优
失效模式Pages 关闭才失效,与域名无关主站换域 / 自定义域验证失效 / Pages 关闭,任一即断A
反证据必须同列:上表第 5、6、8 行是方案 B 的真实代价,也是「多一跳 301」之所以能被容忍的理由。wangjunjian.com 的 cname 配置不在本仓库,把所有对外链接都押在它上面,等于把项目的可访问性外包给另一个仓库的长期维护状态。

07判断与落地

先明确标注:以下是观点,不是共识。数据与配置事实在前一节,判断建立在这些事实与你的使用场景之上。

建议:把自定义域写成主链接,同时保留 github.io 作为备用地址

不是二选一。对外只主推 https://wangjunjian.com/kof-3d/(更快、更体面、少一跳),但在 README 里以「备用地址 / permalink」的形式留一行 github.io 链接。这样既拿到 B 的体验与观感,又保留 A 的永久性与 fork 友好度;万一主站域名变更,读者仍能顺着备用地址找到内容。

如果更看重「链接十年后还能打开」而不是首访那不到一秒,反过来把 github.io 留作主链接也完全站得住——它是 GitHub 官方兜底,且 301 会被缓存,代价主要在首次访问。

落地清单

一个容易被漏掉的副作用:工作流监听的是 push: branches: [main],没有路径过滤。改 README 并 push 会触发一次完整构建与部署,产物内容不变但 build.txt 的 commit 会更新。这是预期行为,不是故障;若不想触发,可在工作流加 paths-ignore: ['**.md'],但那样又会让文档变更失去一次 CI 校验,需自行权衡。

复跑命令

# 1) 量 301 的净开销(8 次采样,比较 num_connects 与 time_redirect)
for i in $(seq 1 8); do
  curl -sSL -o /dev/null -w '%{num_connects} %{time_redirect} %{time_total}\n' \
    https://wang-junjian.github.io/kof-3d/
done

# 2) 直达对照
for i in $(seq 1 8); do
  curl -sS  -o /dev/null -w '%{num_connects} %{time_total}\n' \
    https://wangjunjian.com/kof-3d/
done

# 3) 确认两种入口拿到同一份产物
shasum -a 256 kof-3d.html
curl -sSL https://wang-junjian.github.io/kof-3d/index.html | shasum -a 256
curl -sSL https://wangjunjian.com/kof-3d/build.txt

# 4) 本地两层验收(按 README 固定顺序)
npm test && npm run typecheck && npm run build && npm run verify

仍然存在的差距

竖屏手机目前只给提示、不提供完整竖屏布局;手柄键位写死、无重绑定与振动;AI 不放必杀;无选人界面;M3 的美术打磨与联机未解禁。这些与链接选型无关,列在这里避免把「能打开」误读成「已完工」。