方案1 统一端口:
- vite proxy: /api 和 /socket.io 转发到内部 :8000(不 rewrite)
- server: mountApiPrefix 把 boardgame.io router 挂到 /api 前缀
- server: 用原生 fs 实现静态文件 serve(SPA fallback),生产单进程
- 客户端 createLobbyClient('') 走同源 /api,CORS 完全消失
方案2 命名系统:
- App header 加"你的名字"输入框,localStorage 持久化
- LobbyRoomList 显示所有玩家名字("Alice · Bob")替代 "2/2"
- 房间内"改名"按钮调 POST /api/games/:name/:id/update
修复:
- LobbyClient 用 fetch 包装替代继承(原 request 是 TS private)
- mountApiPrefix 抽到独立文件(避免 test import index.ts 触发 run(8000))
- vite proxy 不 rewrite(避免和 server static serve 冲突)
Tests: pnpm ts 6/6, engine 34 + server 9, E2E 4/4.
Docs: dev-log 12 + lessons §11 + decisions D27-D29.
155 lines
5.8 KiB
Markdown
155 lines
5.8 KiB
Markdown
# 12 · 统一端口 + 玩家命名系统
|
||
|
||
## 做什么
|
||
|
||
用户要求两个改造(先给方案审核,通过后实施):
|
||
|
||
1. **统一端口**:不映射两个端口(之前 5173 页面 + 8000 API),改成页面和 API 都在同一个端口,如页面 `:5173`、API `:5173/api`。
|
||
2. **命名系统**:战局内可以修改自己的名字(大厅显示所有玩家名字)。
|
||
|
||
用户还问了一个关键问题:**加新游戏(如德州扑克)是不是有 Lua 之类的脚本语言让玩家直接写?** 答案记录在本文末尾。
|
||
|
||
## 方案(审核通过)
|
||
|
||
### 方案 1:统一端口
|
||
|
||
**开发环境**:vite proxy 把 `/api` 和 `/socket.io` 转发到内部 `:8000`。
|
||
|
||
**生产环境**:server 直接 serve 静态文件 + mount `/api` 路由,单一进程。
|
||
|
||
```
|
||
浏览器 ──> :5173 (vite / server static)
|
||
├── / → index.html (SPA)
|
||
├── /api/* → boardgame.io lobby REST
|
||
└── /socket.io/* → WebSocket
|
||
```
|
||
|
||
### 方案 2:命名系统
|
||
|
||
- `joinMatch` 时传 `playerName`
|
||
- 大厅显示所有玩家名字(替代 `2/2` 这种数字)
|
||
- 游戏内改名字调 `POST /api/games/:name/:id/update` `{newName}`
|
||
- 名字持久化到 localStorage
|
||
|
||
## 遇到什么问题
|
||
|
||
### 问题 1:`koa-send` 装包被拒(权限)
|
||
|
||
**修法**:用 Node 原生 `fs/promises` 的 `readFile` + `stat` 自己实现静态文件 serve(约 40 行),不依赖第三方包。
|
||
|
||
### 问题 2:boardgame.io 的 `LobbyClient.request` 是 TS private,无法继承加前缀
|
||
|
||
**修法**:放弃继承,直接写一个 fetch 包装(`packages/engine/src/lobby-client.ts`),行为兼容 `LobbyClient` 但请求路径自动加 `/api` 前缀。
|
||
|
||
### 问题 3:`ctx.URL` 是只读 getter,临时改 path 时报错
|
||
|
||
**修法**:只改 `ctx.path`(Koa 里这是 getter/setter 可写),不改 `ctx.URL`。
|
||
|
||
### 问题 4:服务端测试 import index.ts 会触发 `server.run(8000)` 导致 EADDRINUSE
|
||
|
||
**修法**:把 `mountApiPrefix` 抽到独立文件 `packages/server/src/mount-api-prefix.ts`,index.ts 和 test 都从那里 import。
|
||
|
||
### 问题 5:vite proxy rewrite 和 server static serve 冲突
|
||
|
||
最初 vite proxy 把 `/api` 剥掉转发到 `:8000/games`,但 server 端 static serve 拦截了 `/games`(返回 index.html),导致 `/api/games` 返回 HTML。
|
||
|
||
**修法**:vite proxy **不 rewrite**,保留 `/api` 转发到 `:8000/api/games`,由 server 端的 `mountApiPrefix` 剥掉 `/api` 转给 boardgame.io router。
|
||
|
||
### 问题 6:`pnpm dev` 在错误目录执行
|
||
|
||
之前 `cd` 到 `packages/server` 后跑 `pnpm dev` 启动的是 server(tsx watch),导致 EADDRINUSE。**教训**:跑命令前明确 `cd apps/web`。
|
||
|
||
## 怎么解决(具体做法)
|
||
|
||
### 1. vite proxy(apps/web/vite.config.ts)
|
||
|
||
```ts
|
||
server: {
|
||
port: 5173,
|
||
strictPort: true,
|
||
host: '0.0.0.0',
|
||
proxy: {
|
||
'/api': { target: 'http://localhost:8000', changeOrigin: true }, // 不 rewrite
|
||
'/socket.io': { target: 'http://localhost:8000', ws: true, changeOrigin: true },
|
||
},
|
||
},
|
||
```
|
||
|
||
### 2. server /api 挂载(packages/server/src/mount-api-prefix.ts)
|
||
|
||
```ts
|
||
export function mountApiPrefix(app, router) {
|
||
app.use(async (ctx, next) => {
|
||
if (!ctx.path.startsWith('/api/')) return next();
|
||
const original = ctx.path;
|
||
ctx.path = ctx.path.replace(/^\/api/, '') || '/';
|
||
try {
|
||
await router.routes()(ctx, async () => {});
|
||
await router.allowedMethods()(ctx, async () => {});
|
||
} finally {
|
||
ctx.path = original;
|
||
}
|
||
});
|
||
}
|
||
```
|
||
|
||
### 3. server 静态文件 serve(packages/server/src/index.ts)
|
||
|
||
用 `fs/promises` 实现,非 `/api`、`/socket.io`、`/healthz` 的请求返回 `apps/web/dist/index.html`(SPA fallback)。
|
||
|
||
### 4. 客户端 LobbyClient(packages/engine/src/lobby-client.ts)
|
||
|
||
```ts
|
||
export function createLobbyClient(serverUrl: string, apiPrefix: string = '/api') {
|
||
// fetch 包装,请求路径自动加 /api 前缀
|
||
// createLobbyClient('') → 同源 + /api(默认)
|
||
}
|
||
```
|
||
|
||
### 5. 命名系统(App.tsx + LobbyRoomList.tsx)
|
||
|
||
- App header 加"你的名字"输入框,localStorage 持久化
|
||
- `LobbyRoomList` 显示 `formatPlayers()`("Alice · Bob")
|
||
- 房间内"改名"按钮调 `updatePlayer`
|
||
|
||
## 成果
|
||
|
||
- Commit: 见 git log
|
||
- 测试:
|
||
- `pnpm ts` 6/6
|
||
- `pnpm -r test` engine 34 + server 9
|
||
- `pnpm test:e2e` 4/4
|
||
|
||
## 用户验证路径
|
||
|
||
```bash
|
||
# 本机 + 局域网另一台
|
||
# 都开 http://192.168.5.11:5173/(同一个端口!)
|
||
# 1. 输入名字 → 创建房间 → 顶部显示"在房间 X 作为 P0"
|
||
# 2. 另一台输入名字 → 刷新列表 → 看到 "Alice" → 加入 P1
|
||
# 3. 大厅显示 "Alice · Bob"
|
||
# 4. 点"改名" → 输入新名字 → 大厅同步
|
||
```
|
||
|
||
## 关于加新游戏(用户问的问题)
|
||
|
||
**当前系统没有 Lua/脚本语言层。** 所有游戏都用 TypeScript 写(`packages/engine/src/games/*.ts`),编译进 server 进程,运行期不能替换。
|
||
|
||
加德州扑克需要(不是"调一下 Lua 文件"):
|
||
|
||
1. 写 `texas-holdem.ts`(reducer + 5 个 phase + 8 个 move + 隐藏信息,约 400 行 TS)
|
||
2. 写 `TexasHoldemBoard.tsx`(手牌 + 公共牌 + 筹码 + 行动按钮,约 500 行 TSX)
|
||
3. 手牌评估库(10 种牌型 + kicker,约 200 行)
|
||
4. 隐藏信息 plugin(每人只看自己手牌)
|
||
5. 注册到 server + web
|
||
6. 预估 3-4 周
|
||
|
||
如果要支持"玩家自己写游戏",需要单独做 **脚本化层**(Lua/JS 沙箱 + game-DSL + 热加载),那是另一个 1-2 个月的产品方向。详见 `docs/impl.md` Phase 3。
|
||
|
||
## 关联
|
||
|
||
- [11-user-test-feedback-round-1.md](./11-user-test-feedback-round-1.md) — 上一轮用户反馈
|
||
- [10-cors-and-join-bugs.md](./10-cors-and-join-bugs.md) — CORS 修复(统一端口后不再需要)
|
||
- lessons-learned §12.1-12.6 — 本轮的坑
|
||
- decisions D27-D28 — 统一端口、命名系统
|