Files
webgame-collection/docs/superpowers/specs/2026-09-17-webgame-collection-design.md

13 KiB
Raw Permalink Blame History

webgame-collection 设计文档

2026-09-17 · 状态:待评审

1. 项目目标

一个收集"静态网页游戏"(无需服务端、纯前端交互的游戏)的开源目录站:

  • 站点本身是纯静态页面(Vue3 构建产物),无服务端依赖
  • 数据源为仓库内 JSON,一游戏一文件,通过 PR 提交收录
  • 数据结构严格(JSON Schema 校验),并设计数据抽象层,未来可无缝接入后端 API
  • 内置 markdown 文档渲染系统,访问文档时显示文档目录(TOC)
  • 部署链路:GitHub Actions 构建镜像 → 推送 GHCR → 调 keel.sh webhook → k3s 拉取并滚动更新

2. 非目标(v1 不做)

  • 站内 iframe 试玩(一律新标签外链跳转)
  • 用户账号、评分、评论、收藏
  • 后端 API、服务端搜索、分页(数据量预计 < 1000,纯前端处理)
  • 多语言 i18n(中文为主;schema 不预留多语言字段)
  • HPA、CDN、监控(纯静态 nginx,无必要)

3. 仓库结构

webgame-collection/            # 仓库根
├─ src/                        # 完整站点工程 = 唯一构建输入
│  ├─ package.json             # npm/vite 工程在此,cd src 后 npm run dev/build
│  ├─ vite.config.ts / index.html / tsconfig.json
│  ├─ app/                     # Vue 源码
│  │  ├─ main.ts / App.vue
│  │  ├─ router/               # 路由
│  │  ├─ views/                # 页面
│  │  ├─ components/           # 组件
│  │  ├─ data/                 # 数据抽象层(repository 接口 + fetch 实现)
│  │  └─ lib/                  # markdown 渲染、筛选/排序等纯函数
│  ├─ games/                   # 游戏 JSON 数据源(一游戏一文件,PR 提交点)
│  ├─ docs/                    # 站点文档源(*.md,frontmatter: title/order)
│  ├─ assets/covers/           # 仓库内封面图(文件名 = 游戏 id)
│  ├─ schema/game.schema.json  # JSON Schema(draft 2020-12)
│  ├─ scripts/build-data.mjs   # 校验 + 生成 public/data/*
│  └─ public/                  # favicon 等手写资源;public/data/ 为生成物(gitignore)
├─ docs/                       # 只服务 GitHub 仓库,不参与站点构建
│  ├─ README.md                # GitHub 首页会自动展示 docs/README.md
│  ├─ CONTRIBUTING.md          # GitHub 也会识别 docs/CONTRIBUTING.md
│  └─ superpowers/specs/       # 设计文档与实施计划
├─ deploy/                     # Dockerfile、nginx.conf、k8s manifests
├─ LICENSE                     # MIT
├─ .gitignore                  # node_modules、src/dist、src/public/data 等
└─ .github/workflows/          # validate.yml(PR 校验)、publish.yml(构建发布)

约束:站点构建只针对 src/(npm 工程根 = src/);Docker 构建上下文为仓库根,但只拷贝 src/ 与 deploy/nginx.conf。

4. 游戏数据 schema

4.1 文件与命名

  • 位置:src/games/<id>.json,一游戏一文件
  • <id>:^[a-z0-9-]{1,64}$,且必须等于文件名(CI 强制,防止 id/文件名漂移)

4.2 字段定义

字段 必填 类型 规则
id 是 string ^[a-z0-9-]{1,64}$,等于文件名
name 是 string 1–60 字符
url 是 string 仅 https,合法 URL
author.name 是 string 1–60 字符
author.url 否 string 仅 https
description 是 string 1–140 字符,一句话简介(列表页展示)
intro 否 string Markdown,≤ 5000 字符(详情页渲染)
durationMinutes.min 是 int ≥ 1
durationMinutes.max 是 int ≥ min,≤ 600
type 是 enum 见 4.3
tags 是 string[] ≤ 8 个;每个 1–12 字符、无首尾空白、不重复
cover 否 string https 外链,或 /data/assets/covers/<文件名>(对应文件必须存在于 src/assets/covers/,文件名须为 <id>.<png|jpg|jpeg|webp|avif|gif>)
addedAt 是 string YYYY-MM-DD

additionalProperties: false(所有对象层级),未知字段直接报错,保证严格。

4.3 type 枚举与中文标签

值 中文
puzzle 解谜
action 动作
idle 放置
strategy 策略
simulation 模拟
narrative 文字叙事
music 音乐
creative 创意
casual 休闲
other 其他

4.4 示例

{
  "id": "2048",
  "name": "2048",
  "url": "https://play2048.co/",
  "author": { "name": "Gabriele Cirulli", "url": "https://github.com/gabrielecirulli/2048" },
  "description": "滑动合并数字方块的经典益智游戏。",
  "intro": "可选,Markdown 长简介。",
  "durationMinutes": { "min": 5, "max": 20 },
  "type": "puzzle",
  "tags": ["数字", "休闲"],
  "cover": "/data/assets/covers/2048.png",
  "addedAt": "2026-09-17"
}

4.5 校验规则(构建脚本 + PR CI 同源)

  1. JSON Schema 校验(Ajv,draft 2020-12)
  2. id 等于文件名
  3. 全部游戏 id 全局唯一
  4. tags 去重、无首尾空白、长度与数量限制
  5. cover 为本地路径时,文件必须存在且扩展名合法
  6. 校验失败输出可读错误(文件、字段、原因),退出码非 0

5. 生成数据(构建产物)

scripts/build-data.mjs 在校验通过后生成,写入 src/public/data/(gitignore,predev/prebuild 自动执行):

产物 内容
public/data/index.json { schemaVersion, generatedAt, games: GameSummary[] },GameSummary = 除 intro 外的全部字段(目录页搜索/筛选用)
public/data/games/<id>.json 完整游戏对象(含 intro),详情页按需拉取
public/data/docs.json { generatedAt, docs: [{ slug, title, order, content }] },content 为 markdown 原文(内联,文档数少,一次请求)
public/data/assets/covers/* 从 src/assets/covers/ 复制

脚本参数:无参 = 校验 + 生成;--check = 只校验(PR CI 用)。

6. 文档系统

  • 数据源:src/docs/*.md,slug = 文件名(^[a-z0-9-]+$)
  • frontmatter:title 必填;order 可选(数字,缺省 999,升序排序,同序按文件名)
  • 缺失 title、文件名非法、slug 重复 → 构建失败
  • 渲染:markdown-it(linkify + typographer),标题自动生成锚点(slug 化保留中文,重名自动去重)
  • TOC:从 markdown-it token 流提取 h2/h3,右侧固定目录,IntersectionObserver 滚动高亮,点击平滑滚动
  • 内容为仓库内 PR 审核过的可信来源,不引 DOMPurify;将来若接入用户投稿内容再加

7. 前端架构

7.1 技术栈

Vue 3 + TypeScript + Vite · Vue Router · Tailwind CSS v4 · markdown-it · Vitest · vue-tsc。 不引 Pinia(数据量小,模块级 composable + URL query 足够)。

7.2 路由

路由 页面
/ 目录页:搜索 + 类型/时长/标签筛选 + 排序,响应式卡片网格
/games/:id 详情页:封面、元信息、intro markdown、"开始游戏"新标签外链、返回
/docs/:slug 文档页:左侧文档列表 + 右侧 TOC + 正文
/docs 重定向到第一篇文档
* 404(含"游戏不存在")

7.3 筛选状态

  • 状态全部同步 URL query:q(关键词)、type、tag(逗号分隔多选)、dur、sort
  • q 匹配 name / description / author.name / tags(忽略大小写,includes,中文可用)
  • dur 桶(按 durationMinutes.max 判定,无重叠):short ≤5 分钟;mid (5, 30];long >30
  • sort:new(addedAt 倒序,默认)/ name(中文 locale 排序)/ duration(min 升序)

7.4 数据抽象层(未来接后端的唯一边界)

// app/data/repository.ts —— 页面只依赖此接口
interface ContentRepository {
  listGames(): Promise<GameSummary[]>
  getGame(id: string): Promise<Game>          // 不存在抛 NotFoundError
  listDocs(): Promise<DocMeta[]>
  getDoc(slug: string): Promise<Doc>
}
  • app/data/staticRepo.ts:fetch 实现,GET {base}/index.json、{base}/games/<id>.json、{base}/docs.json;内存缓存;错误类型化
  • app/data/index.ts:导出单例;未来换 ApiRepository 只改这一处
  • base = import.meta.env.VITE_DATA_BASE_URL,默认 /data;未来后端的 URL 结构照抄 /data 即可无缝切换
  • 抽象层做轻量运行时校验(按 TS 类型检查响应体字段),不只信构建期
  • 页面消费 loading / error / data 三态

7.5 视觉与兜底

  • Tailwind v4,深色为主的现代目录站风格;卡片网格 1/2/3/4 列响应式
  • 封面加载失败或无封面:游戏名首字 + 按 id 哈希取色的确定性渐变占位卡(稳定不闪)
  • 空状态(筛选无结果)、加载骨架、错误重试面板

7.6 测试(Vitest,跑在 src/)

  • 筛选/排序/搜索纯函数(含中文)
  • TOC slug 化、提取、去重
  • repository URL 拼接与错误映射(404 → NotFoundError)
  • build-data.mjs 校验函数单测(合法/非法样例)

8. 部署

8.1 镜像构建

deploy/Dockerfile 多阶段:

  1. node:24-alpine:拷 src/package*.json → npm ci → 拷 src/ → npm run build
  2. nginx:alpine:拷 deploy/nginx.conf 与构建产物 dist/

.dockerignore 排除 docs/、deploy/k8s/、.git、node_modules、src/dist、src/public/data。 镜像名:ghcr.io/<owner>/webgame-collection:<sha-短哈希> + :latest;包设为 public,k3s 免 imagePullSecret。

8.2 nginx 配置

  • SPA fallback:try_files $uri /index.html
  • gzip 开启
  • 缓存:/assets/(Vite 哈希产物)max-age=31536000, immutable;/data/* ETag 协商缓存(no-cache);index.html 不缓存

8.3 GitHub Actions

validate.yml(PR 触发,paths: src/**):

  1. setup-node 24 + npm cache(cache-dependency-path: src/package-lock.json)
  2. cd src && npm ci
  3. npm run validate:data(只校验,错误信息面向贡献者)
  4. npm run check(vue-tsc --noEmit && vitest run && vite build)

publish.yml(push main):

  1. permissions: contents: read, packages: write
  2. 登录 GHCR(GITHUB_TOKEN),docker/build-push-action 推 sha-<短哈希> + latest
  3. 调 keel native webhook:POST ${{ secrets.KEEL_WEBHOOK_URL }}(/v1/webhooks/native),body {"name":"<镜像名>","tag":"latest"};keel 开启 AUTHENTICATED_WEBHOOKS 时带 Basic auth(用户名 keel,密码 KEEL_TOKEN);secret 未配置则跳过并 log 提示(依赖轮询兜底)
  4. concurrency 保证同时间仅一次发布

8.4 k3s 清单(deploy/k8s/)

  • namespace.yaml、deployment.yaml:2 副本、RollingUpdate、readinessProbe GET /、资源 requests cpu 10m / 内存 16Mi,limits cpu 200m / 内存 128Mi
  • service.yaml:ClusterIP:80
  • ingress.yaml:k3s 默认 Traefik;host 与 TLS 用占位注释,按实际集群填
  • keel 注解(Deployment,依据 keel 官方文档:latest 这类可变标签更新用 force + match-tag 对比 manifest digest;设置 trigger: poll 后 webhook 事件仍然有效):
keel.sh/policy: force
keel.sh/match-tag: "true"
keel.sh/trigger: poll       # 轮询兜底(webhook 未配置/失败时)
keel.sh/pollSchedule: "@every 5m"
  • 不引入 HPA(纯静态 nginx)

8.5 需要一次性提供/配置的项

  1. GitHub 仓库与 owner(决定镜像路径 ghcr.io/<owner>/webgame-collection)
  2. GHCR 包可见性设为 public
  3. k3s 中部署 keel.sh(或确认已有),取得 webhook URL 与 token → 写入 GH 仓库 secrets
  4. 域名与 Ingress host(deploy/k8s/ingress.yaml 占位处)

9. 本地开发与验收

  • 开发:cd src && npm run dev(predev 自动生成数据)
  • 门槛:npm run check(vue-tsc + vitest + vite build)
  • 生产形态:docker build -f deploy/Dockerfile . && docker run -p 8080:80 <image>,人工过一遍目录/筛选/详情/文档 TOC
  • 端到端:合并一个游戏 JSON 的 PR → validate 通过;push main → GHCR 新镜像 → keel 更新 k3s → 站点出现新游戏

10. 未来接后端迁移路径

  1. 后端提供与 /data 相同结构的接口(或标准 REST),URL 可直接沿用
  2. 前端新增 ApiRepository implements ContentRepository,改 app/data/index.ts 单例导出即可,页面与组件零改动
  3. 数据 schema 不变,PR 流程与校验规则继续适用于仓库内数据
  4. 用户投稿/审核等有服务端的行为届时另开 spec,不在本设计内

11. 验收标准

  • src/games/ 新增合法 JSON 通过 npm run validate:data;非法样例(id 与文件名不符、tags 重复、cover 文件缺失、未知字段)全部报错
  • npm run dev 目录页可搜索、按类型/时长/标签筛选、排序,URL query 可分享/刷新保持
  • 详情页展示完整信息与 intro markdown,"开始游戏"新标签打开外链
  • 文档页渲染 markdown,左侧列表 + 右侧 TOC 滚动高亮、锚点跳转正确
  • 无封面/封面加载失败显示确定性占位卡
  • docker build 产出镜像可跑通生产形态,缓存头符合 8.2
  • PR 触发 validate.yml;push main 触发 publish.yml 推送 GHCR 并调 keel webhook
  • deploy/k8s/ 清单在 k3s 可 apply,keel 能拉新镜像滚动更新