5.9 KiB
08 · 本地局域网联机
做什么
用户要求做联机对战,但不是部署到云服务器,而是本地局域网(用户机器上跑 server,同一 LAN 内的其他设备/浏览器通过 ws://[lan-ip]:8000 连接)。
目标:
- Server 能跑起来(tsx 模式即可,不一定要 Docker)
- Web 客户端有"本地" / "联机" 模式切换
- 联机模式下输入 server URL + 房间 ID + 玩家凭证
- 两台浏览器开同一房间,互相操作能实时同步
遇到什么问题
问题 1:指挥官方向错了
最初按 plan 走 P2.1"部署到公共域名",写了 Dockerfile + docker-compose.yml + 生产配置。但用户没有公共域名的服务器访问权限给我,也不想在云上部署。需要先在本地 LAN 跑起来验证。
解决:撤回云部署路径,专注本地开发。Dockerfile 作为 v2 部署的素材保留,但当前目标是:
- server 用
pnpm --filter @tts-like/server dev跑(tsx watch) - 客户端配置
http://localhost:8000(同一机器)或http://192.168.x.x:8000(LAN 内其他机器)
问题 2:SocketIO URL 协议搞错
最初我用 ws://localhost:8000,浏览器报错 ERR_NAME_NOT_RESOLVED for http://ws/socket.io/...。
原因:SocketIO 接受 HTTP URL,内部自动升级到 WebSocket。ws:// 是浏览器用来直接连 WebSocket 的协议,不是 SocketIO 期望的。
解决:改成 http://localhost:8000。
问题 3:player 1 调 move 被拒
点击 收牌(player 1 操作)时报 disallowed move: collect。
原因:boardgame.io 默认 currentPlayer = '0',SocketIO 传输层只允许 currentPlayer 调 move。War 没配置 turn,所以永远是 player 0 active。
解决:在 War 加 turn: { activePlayers: ActivePlayers.ALL }:
import { ActivePlayers } from 'boardgame.io/core';
// ...
turn: { activePlayers: ActivePlayers.ALL },
⚠️
boardgame.io(主入口)导出的是 typeActivePlayers,常量对象要从boardgame.io/core导入。
问题 4:ActivePlayers 类型/值混淆
import { ActivePlayers } from 'boardgame.io' 拿到的是 type alias(接口),不是 const 对象。编译报错:'ActivePlayers' only refers to a type, but is being used as a value。
解决:从 boardgame.io/core 导入:
import { ActivePlayers } from 'boardgame.io/core';
问题 5:Docker 镜像跑不起来(与本地无关)
试过 node packages/server/dist/index.js 启动 Docker 镜像,报错:
ERR_UNSUPPORTED_DIR_IMPORT: Directory import '...boardgame.io/server'
原因:ESM 模式下 Node 拒绝目录导入(即便 package.json 里有 main 字段)。
解决:暂时不用 Docker 镜像(撤回)。Task #8 标题改为"本地局域网联机"。Dockerfile 保留,v2 部署时再修。
📝 Docker 镜像运行的方案(留作未来):
- 方案 A:保持 ESM,在 server/package.json 加
"main": "./dist/index.js"+babel-plugin-transform-modules-commonjs编译产物- 方案 B:改 server 为 CJS(移除
"type": "module"、tsconfig 改"module": "CommonJS"、moduleResolution: "Node16")——但boardgame.io/server是 CJS,会让 server 内部 import 反向(from ESM to CJS,里 ESM 调用 CJS 通常 OK,但import.meta之类用不了)
怎么解决
步骤 1:让 server 跑起来
pnpm --filter @tts-like/server dev
# tsx watch src/index.ts
# 🎲 tts-like server listening on :8000
加 /healthz 端点 + 注册 3 个游戏 + 环境变量读 ALLOWED_ORIGINS。
步骤 2:web App 加 Online 模式
顶部加"本地 / 联机"切换按钮。联机模式下显示一个 OnlineConfigBar:
- Server URL(默认
http://localhost:8000) - Room ID(默认
room1) - Player(0 / 1)
- Secret(默认
p0/p1)
用 localStorage 持久化。
步骤 3:3 个游戏分别有 Online 视图
import { SocketIO } from 'boardgame.io/multiplayer';
Client({
game,
board: ...,
multiplayer: SocketIO({ server: config.serverUrl }),
debug: false,
});
// 使用时
<OnlineClient
matchID={config.matchID}
playerID={config.playerID}
credentials={config.credentials}
/>
步骤 4:end-to-end 验证
用 Playwright 打开 2 个 tab:
- Tab 0:player 0, secret p0
- Tab 1:player 1, secret p1
- 同一 room "room1"
操作:
- Tab 0 点 翻牌 → 双方 deck 26→25,双方 pile 各 1 张
- Tab 1 点 收牌 → 7♣ 赢,p0-deck 25→27
- 两个 tab 状态完全同步
成果
- Commit
585930c feat(multiplayer): LAN/local online mode via SocketIO - 7 文件改动,+426/-92 行
- 服务器 tsx 跑通:
pnpm --filter @tts-like/server dev→ 监听 :8000/healthz返回{"status":"ok","port":8000,"uptime":3.57}
- 2 客户端 SocketIO 状态实时同步验证通过
当前测试方式(你接下来怎么用)
同一台机器(最简单)
# 终端 1
pnpm --filter @tts-like/server dev
# 终端 2
pnpm --filter @tts-like/web dev
# 浏览器开 2 个 tab (或 2 个隐身窗口) → 都进联机模式
# Tab 1: Player 0, Secret p0
# Tab 2: Player 1, Secret p1
# 同一 Room ID (默认 room1)
跨设备(LAN)
- 看 server 机器的局域网 IP(如
ifconfig或ipconfig看192.168.x.x) - 客户端的 Server URL 填
http://192.168.x.x:8000 - 防火墙放行 8000 端口
用户验证
- Open 2 browser tabs to same
http://localhost:5173 - Switch to War → 联机 → 默认配置(player 0 / room1 / p0)
- 在 tab 1 改成 Player 1, Secret p1
- 在 tab 0 点 翻牌 → 两边都看到翻牌结果
- 在 tab 1 点 收牌 → 两边都看到收牌结果
关联
- 03-monorepo-scaffold.md — 之前 query 错误方向,撤回 Docker 部署
- lessons-learned.md §23 — 这一轮的踩坑(SocketIO URL protocol / ActivePlayers 导入 / disallowed move)
docs/decisions.md— 决策记录更新