项目:html5-games | 版本:v1.0 | 生成日期:2026-10-06 | 依据:源码分析
本篇覆盖环境要求、命令级本地操作、构建产物结构与静态部署、测试体系与质量门禁,并以「新增一款游戏标准操作流程(SOP)」收尾——这是接手维护最常用的一节。
| 项 | 要求 | 依据 |
|---|---|---|
| Node.js | ≥ 22 | package.json 的 engines.node |
| 包管理器 | pnpm 10.23.0 | packageManager 字段钉版(CI 用 pnpm/action-setup@v4 按 corepack 声明自动取版) |
| 浏览器 | 支持 ES2022+ 的现代浏览器 | vite.config.ts 的 build.target: 'esnext',无 legacy 降级 |
| 系统 API | ResizeObserver、OffscreenCanvas/Web Worker(可选)、WebAudio、localStorage | 前两者不可用时均有降级路径(封面同步编码 / 音频静默) |
# 1. 安装依赖(首次或锁文件变更后)
pnpm install
# 2. 启动开发服务器
pnpm dev # → http://localhost:5556(vite.config.ts 固定 host 127.0.0.1 / port 5556)
# 3. 其他常用命令
pnpm test # vitest run(一次性跑完单元测试)
pnpm test:watch # vitest watch 模式
pnpm lint # ESLint 检查(目标:零错误)
pnpm lint:fix # ESLint 自动修复
pnpm format # Prettier 格式化 src/**/*.{ts,vue,js,css,json}
pnpm typecheck # vue-tsc --noEmit 类型检查
pnpm build # 生产构建 → dist/
pnpm preview # 本地预览生产构建开发环境行为要点:@ 别名指向 src/;路由使用 createWebHistory,dev server 自带 SPA 回退,无需额外配置;index.html 中的 Google Fonts 与 Umami 统计脚本不可达时不影响功能(字体懒加载有系统字体兜底,统计脚本 fail-safe)。index.html 的内联启动屏 #boot-splash 在 Vue 挂载且首个路由组件就绪后由 App.vue 移除(8 秒超时兜底),开发时若页面长时间显示启动屏说明路由组件加载失败,应查终端报错。
pnpm build 输出 dist/,chunk 策略由 vite.config.ts 的 manualChunks 与 chunkFileNames 决定:
| 产物 | 来源 | 说明 |
|---|---|---|
assets/index-<hash>.js / CSS | 应用入口 | 极薄:createApp + 路由表 |
assets/vendor-<hash>.js | manualChunks vendor | Vue + vue-router + @vue 全家桶 |
assets/vueuse-vendor-<hash>.js | manualChunks | @vueuse/core |
assets/pixi-vendor-<hash>.js | manualChunks | pixi.js 8(仅 3 款游戏触发下载) |
assets/<游戏id>-<hash>.js × 79 | import.meta.glob 代码分割 | 每款游戏一个独立懒加载 chunk |
assets/GamesPage-<hash>.js 等路由 chunk | 路由懒加载 | 大厅/详情两个页面组件 |
assets/<id>-<hash>.webp 相关 worker chunk | coverEncode.worker.ts | 封面编码 worker(new Worker(new URL(...), { type:'module' }) 由 Vite 处理) |
media/mov_bbb.mp4 | public/ 原样拷贝 | 视频拼图素材 |
favicon.svg | public/ 原样拷贝 | — |
cssCodeSplit: true 使每 chunk 自带样式;chunkSizeWarningLimit: 600 提示超大 chunk;reportCompressedSize: false 加速构建。
产物是纯静态文件,部署到任意静态托管(Nginx / GitHub Pages / Vercel / Cloudflare Pages 等)即可,唯一必须的配置是 SPA 回退(createWebHistory 需要):
location / { try_files $uri $uri/ /index.html; }可选:index.html 内置 Umami 统计(https://analytics.zhangzhengyang.com/script.js,data-website-id 硬编码),自部署时如不需要可删除该 <script> 标签;Google Fonts 两条 preconnect 与样式表同理(删除后回退系统字体)。
部署相关的三个实操提醒。缓存策略:产物文件名带内容 hash,因此 assets/ 下的 JS/CSS 可以放心设置长缓存(如 immutable, max-age=31536000),而 index.html 必须短缓存或 no-cache——否则用户会拿着旧 HTML 去请求已被新版本淘汰的旧 hash chunk,触发 404(尽管 router.onError 能整页刷新自愈,但多一次往返)。发布原子性:先全量上传新产物、再覆盖 index.html,能将「新旧混合」窗口压到最短;router.onError 的自愈逻辑(匹配动态模块加载失败后 location.assign 目标路径)正是为这个窗口准备的最后防线。路径前缀:若部署在子路径(如 https://example.com/games-hall/),当前 Vite 配置未设置 base(默认 '/'),需要在 vite.config.ts 增加 base: '/games-hall/' 并重新构建——import.meta.glob 生成的 chunk 引用与 worker URL 都会随之改写,无需改游戏代码。
vitest.config.ts:environment: 'node'、只收集 src/**/__tests__/**/*.test.ts、@ 别名同构建。现有 5 个测试文件:
| 测试文件 | 守护目标 |
|---|---|
src/config/__tests__/games.test.ts | 元数据完整性 8 个用例:总数 79、id 唯一/格式、sortOrder 唯一、必填非空、category 合法、分类非空且分类色 hex、配置与磁盘文件一一双向对应、核心精选 ≥4、分类 Map 全覆盖(用例级拆解见《09-测试与质量保障》2.1) |
src/composables/__tests__/useGameBgm.test.ts | BGM 纯函数:确定性、hash/PRNG 性质、参数区间、32 步结构、曲目多样性 |
src/composables/__tests__/usePagination.test.ts | 分页 clamp/重置语义 |
src/utils/__tests__/highScore.test.ts | 最高分读写契约(缺失读 0、写读回、非法值容错),node 环境注入 localStorage 内存桩 |
src/utils/__tests__/keyboard.test.ts | isTypingTarget 判定 |
.github/workflows/(CI,push main/master 与全部 PR 触发)按序执行五关,任一失败即红:
pnpm install --frozen-lockfile → pnpm typecheck → pnpm lint → pnpm test → pnpm build本地等价复现即「提交前五连」:pnpm typecheck && pnpm lint && pnpm test && pnpm build。质量规则要点(eslint.config.js):@typescript-eslint/no-explicit-any 为 error;未用变量以 ^_ 前缀豁免;Vue 取 flat/essential 档并关闭多词组件名等对游戏 SFC 不友好的规则;Prettier 兼容层最后加载(.prettierrc:无分号/单引号/100 列/2 空格/es5 尾逗号/LF)。tsconfig.json 为 strict 全开(noUnusedLocals/noUnusedParameters 等),但注意 vue-tsc 只检查 TS——游戏层 JS SFC 不在其覆盖内,游戏质量主要靠 lint 与 code review。
对测试策略再补充两点解读。为什么测试如此之少却够用:5 个测试文件守护的都是「跨文件契约」(配置↔文件一致性、纯函数确定性、存储读写语义),而 79 款游戏的玩法正确性没有自动化覆盖——因为这个项目没有可断言的「正确答案」(一款贪吃蛇「玩起来对不对」无法用单测表达),团队把验证重心放在了 CI 的构建关与人工验收(06 篇 SOP 第 4 步的手工清单)。新增测试时建议延续这一哲学:优先守护契约与纯函数,不要为游戏玩法补 fragile 的快照测试。为什么环境是 node 而非 jsdom:现有测试全部不触碰 DOM(BGM 测纯函数、分页测 ref 运算、存储测试注入内存桩),node 环境启动最快、依赖最少;若未来要测 DOM 相关逻辑,需要把 environment 换成 jsdom 并补依赖,这是一个显式决策而不是顺手改动。
以下流程以「新增一款假想的连连消游戏 bubble-pop-game(休闲类,Canvas 实现,带最高分)」为例。全程只需改 1 个文件 + 新增 1 个文件,外加 3 处文案同步。
src/games/bubble-pop-game.vuesrc/games/snake.vue 作为骨架(JS 版范式)或 src/games/sokoban-game.vue(TS 版范式)。<GameShell>;title 用中文展示名;自包含(不 import 其他游戏/路由);onBeforeUnmount 清理 rAF 与 window 监听;键盘+触屏至少双通道;canvas 加 touch-action: none;音效走 useGameAudio().play()。h5g:bubble-pop-game-best(完整 id 拼接,勿用短键,见 04 篇 2.3)。bubble-pop-game.vue。src/config/games.ts在 localGames 数组追加一条(建议放文件末尾并续排 sortOrder;注释分区可按批次新增):
{
id: 'bubble-pop-game', // 与文件名一致,[a-z0-9-]+
category: 'casual', // puzzle | board | action | casual
engine: 'game', // 固定值(死字段,照抄即可,见《13-技术债务与演进建议》E-02)
library: 'showcase', // 固定值(死字段,同上)
title: '泡泡消消消', // 展示名(也是 BGM 种子)
description: '一句话玩法描述(会参与搜索匹配)',
tags: ['GitHub 示例'], // 至少 1 个标签(参与搜索)
group: 'GitHub 示例集成', // 来源分组;进首页精选则用 '核心精选'
difficulty: '',
sortOrder: 80, // 全表唯一,接续最大值 +1
}id 与 src/games/bubble-pop-game.vue 的对应关系无需任何 import——registry.ts 的 import.meta.glob 在下次构建自动纳入。
src/config/__tests__/games.test.ts 断言总数为 79,因此必须同步:
src/config/__tests__/games.test.ts:expect(localGames).toHaveLength(79) → 80;README.md:游戏目录表数量与「79 款」字样;package.json 的 description 与 index.html 的 meta description 中的「79」。pnpm dev # 打开 http://localhost:5556/games,确认新游戏卡片出现、分类计数正确
pnpm test # games.test.ts 通过(含配置/文件一一对应断言)
pnpm typecheck && pnpm lint
pnpm build # 确认产出 assets/bubble-pop-game-<hash>.js手工验收清单:键盘可玩(方向键不滚动页面)、触屏可玩(滑动/点按响应、touch-action 生效)、暂停/重开按钮行为正确、静音开关全局生效、最高分刷新后保留、返回大厅滚动位置恢复、直接访问 /game/bubble-pop-game 深链可用、拼错 id 显示「未找到游戏」。
对这份 SOP 再做几点说明,帮助理解每一步的必要性。为什么第 1 步强调复制现有游戏而非从零写:骨架里沉淀了七十九条游戏验证过的细节——canvas 的 tabindex="0" 与 focusCanvas() 配合(防止方向键滚动页面)、@touchstart.passive 与 @touchend.prevent 的组合(既注册起点又不触发浏览器手势)、结算时 focusCanvas 的焦点归还——这些细节从零写几乎必然漏掉一两条,且漏掉的代价要到真机上才暴露。为什么总数文案要同步三处:这是测试注释里明文记载的防漂移设计,games.test.ts 只能守护它自己的 toHaveLength,README/index.html/package.json 三处文案没有自动化检查,只能靠 SOP 约定。为什么 sortOrder 要接续最大值:默认排序按其升序,随手乱排会让新游戏插进首页第一屏;接续编号则保证「新游戏排最后」符合「批次演进」的既有语义(sortOrder 0-14 是最早的核心批次,79 是最新一批)。
.vue 文件 + 删 config 条目 + 总数文案 -1 +(可选)清理 h5g:<id>-* 孤儿键。pixi-pong.vue 的 new Application() + await app.init() 与 destroyed 竞态保护模式;pixi 依赖自动进 pixi-vendor chunk,无需配置。group 设为 '核心精选'(测试要求该组 ≥4 款,当前 7 款);首页横带取该组前 4 款(按 sortOrder)。GameCategoryMeta.icon 换名,注意 material-symbols@0.45.8 存在个别连字缺失(casual 分类的 catching_pokemon 教训,见 src/config/games.ts 注释),上线前实测渲染。| 症状 | 首查位置 |
|---|---|
| 控制台「Failed to fetch dynamically imported module」 | 发版后旧 chunk 失效,router.onError 应已自动刷新;若循环出现,检查部署是否残留旧 hash 文件与 CDN 缓存策略 |
| 大厅卡片封面空白 | coverEncode.worker 加载失败会同步兜底;仍空白检查 gameCoverRender.ts 的字体竞态逻辑与 OffscreenCanvas 支持 |
| 无声音 | 浏览器自动播放策略——需先有用户手势;再查 game-audio-muted/h5g:bgm-muted 键 |
| 游戏页整屏空白但有返回按钮 | onErrorCaptured 已兜住渲染异常,看页面是否渲染 ErrorFallback 及其错误文案定位到具体游戏 |
| 测试「配置有但缺文件」红 | config 新增条目但 .vue 文件名不一致(大小写/后缀) |
项目依赖较新(Vue 3.5 / Vite 5 / Tailwind 4 / ESLint 10),升级时建议遵守「一次一类、五连验证」:每次只升级一类依赖(如只动 vite 系),跑完 pnpm typecheck && pnpm lint && pnpm test && pnpm build 并用 pnpm preview 抽查大厅与三类范式游戏(Canvas/DOM/Pixi)后再继续。两个高危升级点提前标记:pixi.js 大版本(3 款游戏的 Application API 与 Graphics 用法可能变动,升级后必须实玩 pixi-pong);material-symbols 小版本(个别图标连字可能缺失,casual 分类的 catching_pokemon 事故即源于此,升级后抽查侧栏图标渲染)。回滚直接 git revert + 重新构建发布即可——构建产物无状态、localStorage 键与代码版本解耦(旧版本读新键只会得到缺省值),不存在需要脚本处理的回滚路径。