平台通知

全屏浏览 VibeHub

全屏浏览,获得更沉浸的体验。你可以随时通过菜单退出全屏。

社区技巧
开发技巧入门通用

VibeHub 联机开发正确流程:本地写逻辑,线上验联机(新手版)

联机的关键不是"能不能在本地连真服务器"(不能),而是"什么在本地测、什么必须上线测"。这篇给可直接抄的代码结构、两个账号的线上整场用例,以及一份"要不要上线"的判定清单。

由 我叫人员外婆 分享2026年9月12日已在 DeepSeek Harness (dsh) + Vite + Vue 3 + Playwright;VibeHub SDK v3 稳定通道 验证

解决什么问题

新手做 VibeHub 联机,最常见的两个坑:

  1. 把联机逻辑和 VibeHub 绑死,于是本地什么都测不了,改一行代码就要发布上线看效果;
  2. 反过来,在本地怎么连都连不上真服务器,又以为是自己代码写错了,反复折腾。

正确的做法是把"逻辑"和"传输"分开:逻辑在本地随便测,传输只在线上验。本文给可以直接抄的代码结构、一个"两个账号打完整一局"的自动化用例,以及一份"什么改动必须上线"的清单。

适用条件

  • 用 VibeHub 的联机能力(页面引入 https://vibe.lumigrav.space/sdk/v3/vibehub.js,用 window.VibeHub 建房/进房);
  • 本地能跑起项目(Vite / 其他构建都行),线上能发布(vibehub deploy);
  • 不需要懂 WebRTC 或加密。

使用方法

第 1 步:把联机层抽成一个接口(这一步做完,后面全都轻松)

游戏逻辑只依赖这个小接口,不直接碰 VibeHub:

// src/net/types.ts
export interface Room {
  onMessage(fn: (msg: unknown, from: string) => void): void
  send(msg: unknown): void
  leave(): void
}
export interface Net {
  create(roomId: string): Promise<Room>   // 建房
  join(roomId: string): Promise<Room>     // 进房
}

第 2 步:写两个实现——本地的假实现、线上的真实现

本地假实现(同一个浏览器里开两个窗口就能互通,不需要网络):

// src/net/local.ts
export function createLocalNet(): Net {
  const hub = new BroadcastChannel('my-game')   // 两个窗口共享这个频道
  const rooms = new Map<string, (msg: unknown, from: string) => void>()
  // create/join 都用同一份 rooms;send 时广播给同房间的其他人
  // 细节略:重点是"接口一样、行为一样",只是不经过网络
}

线上真实现(只在这里出现 VibeHub):

// src/net/vibe.ts
export async function createVibeNet(work: string): Promise<Net> {
  const vibe = await VibeHub.init({ work })     // 只有线上会用到
  const wrap = (room: any): Room => ({
    onMessage: (fn) => room.onMessage((msg: unknown, from: string) => fn(msg, from)),
    send: (msg) => room.send(msg),
    leave: () => room.leave(),
  })
  return {
    async create(id) { return wrap(await vibe.room.join(id, { topology: 'host' })) },
    async join(id) { return wrap(await vibe.room.join(id, { topology: 'host' })) },
  }
}

第 3 步:按环境注入,本地/线上各走各的

// src/net/index.ts
import { createLocalNet } from './local'

export async function createNet(work: string): Promise<Net> {
  if (import.meta.env.DEV) return createLocalNet()          // 本地:假实现,离线可跑
  const { createVibeNet } = await import('./vibe')           // 线上:真 SDK(动态引入,不污染本地包)
  return createVibeNet(work)
}

到这里,你的本地开发就自由了:开两个浏览器窗口,一个建房一个进房,房间面板、准备、开局、结算、重连逻辑全都能本地调——不用发布,不用等网络。

第 4 步:逻辑用单元测试锁住(不依赖任何 SDK)

把与网络无关的部分抽成纯函数,直接测:

  • 建房/进房的状态机(谁先坐、谁当房主、座位怎么排);
  • 消息协议(收到乱序/重复/过期消息怎么处理);
  • 结算与分数的权威合并;
  • 断线重连后的状态补齐。

这些测试跑起来是毫秒级的,改动再频繁也不心疼。

第 5 步:联机行为只在线上验——发布 + 两个账号的整场用例

这是唯一能验真联机的地方。发布:

vibehub update --slug <你的作品slug> --dir dist

然后用 Playwright 开两个互相隔离的浏览器上下文(等于两台设备、两个账号),跑一整局:

// tests/online.spec.ts
import { test, expect } from '@playwright/test'
import { readFileSync } from 'node:fs'

const URL = 'https://vibeapps.lumigrav.space/<你的作品slug>/'
// 凭据放本地文件,且必须被 gitignore;用例只读取、不回显、不提交
const accounts = JSON.parse(readFileSync('tmp/online_test/accounts.json', 'utf8'))

async function login(page: any, account: { email: string; password: string }) {
  await page.goto(URL)
  await page.getByRole('button', { name: /登录/ }).click()      // 走页面上的登录入口
  await page.getByLabel(/邮箱|账号/).fill(account.email)
  await page.getByLabel(/密码/).fill(account.password)
  await page.getByRole('button', { name: /登录|授权/ }).click()
  await expect(page.getByText(/已登录|退出/)).toBeVisible({ timeout: 60_000 })
}

test('两个账号打完整一局', async ({ browser }) => {
  const hostCtx = await browser.newContext()      // 上下文隔离 = 两个独立玩家
  const guestCtx = await browser.newContext()
  const host = await hostCtx.newPage()
  const guest = await guestCtx.newPage()
  await login(host, accounts[0])
  await login(guest, accounts[1])

  // 房主建房,读出房间号
  await host.getByRole('button', { name: /创建房间/ }).click()
  const roomId = (await host.getByTestId('room-id').innerText()).trim()

  // 另一个账号进房,并各自补两个电脑玩家
  await guest.goto(`${URL}?room=${roomId}`)
  await expect(guest.getByText(roomId)).toBeVisible({ timeout: 60_000 })

  // 打完整一局:这里用轮询推进,直到出现结算
  await expect(host.getByText(/结算/)).toBeVisible({ timeout: 10 * 60_000 })
  await expect(guest.getByText(/结算/)).toBeVisible({ timeout: 10 * 60_000 })

  // 断言:两边看到同一份权威结果(分数、局数、座位)
  const scoresHost = await host.getByTestId('scores').innerText()
  const scoresGuest = await guest.getByTestId('scores').innerText()
  expect(scoresGuest).toEqual(scoresHost)

  await hostCtx.close(); await guestCtx.close()
})

要点:

  • 两个上下文 = 两个玩家(不要把两个标签页放同一个上下文,登录态会互相覆盖);
  • 每个用例给独立的超时(整局可能要几分钟),失败时保留截图与 trace;
  • 登录态可以存成 storageState 复用,避免每次重登(但注意 token 会过期);
  • 断言要落在"两边一致"上(同一份权威状态、同一个房间号、同一份结算),这才是联机测试的价值。

第 6 步:定一份"要不要上线"的清单,写进项目规范

你的改动验证方式
联机传输、房间逻辑、玩家名单、断线重连、结算同步必须上线跑一次两个账号的整场用例
游戏 AI、规则、UI、文案、美术不必上线:本地全量测试 + 单机端到端即可
协议/状态机内部逻辑本地单元测试(毫秒级,天天跑)

这样你既不会"改什么都上线",也不会"该上线的不上线"。

如何验证结果

  1. 本地流程是否真的不依赖网络:断网(或关掉 wifi)后按第 3 步开两个窗口,应该能正常建房、进房、同步——能,说明联机层已解耦。
  2. 线上整场用例是否可信:故意改坏一处同步(例如进房后不发送准备状态),用例必须失败;改回来必须通过。用例不会因为"改坏"而失败,就等于没测。
  3. 失败时能不能定位:用例失败要在 test-results/ 留下截图、trace 和两边控制台日志;只有结论没有证据的失败等于重跑一次。
  4. 房间号与结算双端一致:这是联机最低断言,缺了它,用例只是"页面能打开"。

已知限制与风险

  • 本地连不上真服务器,这是正常的、不用修。SDK 的建房/进房接口一律要求登录凭证,文档里的"匿名"指的是帮平台当中继节点,不是"免登录进房"。一条命令自证:
    curl -s -o - -w '\nHTTP:%{http_code}\n' -X POST https://vibe.lumigrav.space/api/sdk/rooms \
      -H 'Content-Type: application/json' -d '{"room":"probe","action":"claim","peer":"p"}'
    # → {"error":"未授权"}  HTTP:401
    
    本地伪装域名做真实登录也不行(实测:域名不受信任)。所以别在"让本地连上真服务器"上花时间。
  • 假联机层覆盖不了的几类问题(我们真实踩过,本地复现不出来):超大数据包静默发送失败、分片在直连对端处被丢弃、对端标识漂移导致状态被拒收、接收饥饿被误判为掉线。这正是第 5 步必须存在的原因。
  • 凭据管理:账号密码/凭据文件必须 gitignore;用例只在运行时读取,不回显、不提交、不写进日志。
  • 用例会变慢:整场用例本来就是分钟级,把它当成"发布前的一次检查",不要塞进每次提交的快速回路。
  • 本文基于 2026-09-12 的 SDK 与接口行为;平台更新后,重跑上面那条 curl 复核即可。

来源与致谢

原创:开发 VibeHub 作品「莲花广麻」(B5AJupT1)时形成的流程与实测记录。接口结论来自公开 SDK 源码与公开 API,命令可直接复现,无需账号。无第三方转载内容。

关联作品

莲花广麻|一款莲花县特有的地方麻将游戏玩法

看完技巧后,可以直接体验作者用它做出的作品。