方案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.
5.8 KiB
12 · 统一端口 + 玩家命名系统
做什么
用户要求两个改造(先给方案审核,通过后实施):
- 统一端口:不映射两个端口(之前 5173 页面 + 8000 API),改成页面和 API 都在同一个端口,如页面
:5173、API:5173/api。 - 命名系统:战局内可以修改自己的名字(大厅显示所有玩家名字)。
用户还问了一个关键问题:加新游戏(如德州扑克)是不是有 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)
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)
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)
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 ts6/6pnpm -r testengine 34 + server 9pnpm test:e2e4/4
用户验证路径
# 本机 + 局域网另一台
# 都开 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 文件"):
- 写
texas-holdem.ts(reducer + 5 个 phase + 8 个 move + 隐藏信息,约 400 行 TS) - 写
TexasHoldemBoard.tsx(手牌 + 公共牌 + 筹码 + 行动按钮,约 500 行 TSX) - 手牌评估库(10 种牌型 + kicker,约 200 行)
- 隐藏信息 plugin(每人只看自己手牌)
- 注册到 server + web
- 预估 3-4 周
如果要支持"玩家自己写游戏",需要单独做 脚本化层(Lua/JS 沙箱 + game-DSL + 热加载),那是另一个 1-2 个月的产品方向。详见 docs/impl.md Phase 3。
关联
- 11-user-test-feedback-round-1.md — 上一轮用户反馈
- 10-cors-and-join-bugs.md — CORS 修复(统一端口后不再需要)
- lessons-learned §12.1-12.6 — 本轮的坑
- decisions D27-D28 — 统一端口、命名系统