feat(rpc,network): 新增 RPC 库并迁移网络模块 (#364)

* feat(rpc,network): 新增 RPC 库并迁移网络模块

## @esengine/rpc (新增)
- 新增类型安全的 RPC 库,支持 WebSocket 通信
- 新增 RpcClient 类:connect/disconnect, call/send/on/off/once 方法
- 新增 RpcServer 类:Node.js WebSocket 服务端
- 新增编解码系统:支持 JSON 和 MessagePack
- 新增 TextEncoder/TextDecoder polyfill,兼容微信小游戏平台
- 新增 WebSocketAdapter 接口,支持跨平台 WebSocket 抽象

## @esengine/network (重构)
- 重构 NetworkService:拆分为 RpcService 基类和 GameNetworkService
- 新增 gameProtocol:类型安全的 API 和消息定义
- 新增类型安全便捷方法:sendInput(), onSync(), onSpawn(), onDespawn()
- 更新 NetworkPlugin 使用新的服务架构
- 移除 TSRPC 依赖,改用 @esengine/rpc

## 文档
- 新增 RPC 模块文档(中英文)
- 更新 Network 模块文档(中英文)
- 更新侧边栏导航

* fix(network,cli): 修复 CI 构建和更新 CLI 适配器

## 修复
- 在 tsconfig.build.json 添加 rpc 引用,修复类型声明生成

## CLI 更新
- 更新 nodejs 适配器使用新的 @esengine/rpc
- 生成的服务器代码使用 RpcServer 替代旧的 GameServer
- 添加 ws 和 @types/ws 依赖
- 更新 README 模板中的客户端连接示例

* chore: 添加 CLI changeset

* fix(ci): add @esengine/rpc to build and check scripts

- Add rpc package to CI build step (must build before network)
- Add rpc to type-check:framework, lint:framework, test:ci:framework

* fix(rpc,network): fix tsconfig for declaration generation

- Remove composite mode from rpc (not needed, causes CI issues)
- Remove rpc from network project references (resolves via node_modules)
- Remove unused references from network tsconfig.build.json
This commit is contained in:
YHH
2025-12-28 10:54:51 +08:00
committed by GitHub
parent 8605888f11
commit 7940f581a6
39 changed files with 3505 additions and 784 deletions

View File

@@ -0,0 +1,9 @@
/**
* @zh 编解码器模块
* @en Codec Module
*/
export type { Codec } from './types'
export { json } from './json'
export { msgpack } from './msgpack'
export { textEncode, textDecode } from './polyfill'

View File

@@ -0,0 +1,30 @@
/**
* @zh JSON 编解码器
* @en JSON Codec
*/
import type { Packet } from '../types'
import type { Codec } from './types'
import { textDecode } from './polyfill'
/**
* @zh 创建 JSON 编解码器
* @en Create JSON codec
*
* @zh 适用于开发调试,可读性好
* @en Suitable for development, human-readable
*/
export function json(): Codec {
return {
encode(packet: Packet): string {
return JSON.stringify(packet)
},
decode(data: string | Uint8Array): Packet {
const str = typeof data === 'string'
? data
: textDecode(data)
return JSON.parse(str) as Packet
},
}
}

View File

@@ -0,0 +1,34 @@
/**
* @zh MessagePack 编解码器
* @en MessagePack Codec
*/
import { Packr, Unpackr } from 'msgpackr'
import type { Packet } from '../types'
import type { Codec } from './types'
import { textEncode } from './polyfill'
/**
* @zh 创建 MessagePack 编解码器
* @en Create MessagePack codec
*
* @zh 适用于生产环境,体积更小、速度更快
* @en Suitable for production, smaller size and faster speed
*/
export function msgpack(): Codec {
const encoder = new Packr({ structuredClone: true })
const decoder = new Unpackr({ structuredClone: true })
return {
encode(packet: Packet): Uint8Array {
return encoder.pack(packet)
},
decode(data: string | Uint8Array): Packet {
const buf = typeof data === 'string'
? textEncode(data)
: data
return decoder.unpack(buf) as Packet
},
}
}

View File

@@ -0,0 +1,112 @@
/**
* @zh 平台兼容性 polyfill
* @en Platform compatibility polyfill
*
* @zh 为微信小游戏等不支持原生 TextEncoder/TextDecoder 的平台提供兼容层
* @en Provides compatibility layer for platforms like WeChat Mini Games that don't support native TextEncoder/TextDecoder
*/
/**
* @zh 获取全局 TextEncoder 实现
* @en Get global TextEncoder implementation
*/
function getTextEncoder(): { encode(str: string): Uint8Array } {
if (typeof TextEncoder !== 'undefined') {
return new TextEncoder()
}
return {
encode(str: string): Uint8Array {
const utf8: number[] = []
for (let i = 0; i < str.length; i++) {
let charCode = str.charCodeAt(i)
if (charCode < 0x80) {
utf8.push(charCode)
} else if (charCode < 0x800) {
utf8.push(0xc0 | (charCode >> 6), 0x80 | (charCode & 0x3f))
} else if (charCode >= 0xd800 && charCode <= 0xdbff) {
i++
const low = str.charCodeAt(i)
charCode = 0x10000 + ((charCode - 0xd800) << 10) + (low - 0xdc00)
utf8.push(
0xf0 | (charCode >> 18),
0x80 | ((charCode >> 12) & 0x3f),
0x80 | ((charCode >> 6) & 0x3f),
0x80 | (charCode & 0x3f)
)
} else {
utf8.push(
0xe0 | (charCode >> 12),
0x80 | ((charCode >> 6) & 0x3f),
0x80 | (charCode & 0x3f)
)
}
}
return new Uint8Array(utf8)
},
}
}
/**
* @zh 获取全局 TextDecoder 实现
* @en Get global TextDecoder implementation
*/
function getTextDecoder(): { decode(data: Uint8Array): string } {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder()
}
return {
decode(data: Uint8Array): string {
let str = ''
let i = 0
while (i < data.length) {
const byte1 = data[i++]
if (byte1 < 0x80) {
str += String.fromCharCode(byte1)
} else if ((byte1 & 0xe0) === 0xc0) {
const byte2 = data[i++]
str += String.fromCharCode(((byte1 & 0x1f) << 6) | (byte2 & 0x3f))
} else if ((byte1 & 0xf0) === 0xe0) {
const byte2 = data[i++]
const byte3 = data[i++]
str += String.fromCharCode(
((byte1 & 0x0f) << 12) | ((byte2 & 0x3f) << 6) | (byte3 & 0x3f)
)
} else if ((byte1 & 0xf8) === 0xf0) {
const byte2 = data[i++]
const byte3 = data[i++]
const byte4 = data[i++]
const codePoint =
((byte1 & 0x07) << 18) |
((byte2 & 0x3f) << 12) |
((byte3 & 0x3f) << 6) |
(byte4 & 0x3f)
const offset = codePoint - 0x10000
str += String.fromCharCode(
0xd800 + (offset >> 10),
0xdc00 + (offset & 0x3ff)
)
}
}
return str
},
}
}
const encoder = getTextEncoder()
const decoder = getTextDecoder()
/**
* @zh 将字符串编码为 UTF-8 字节数组
* @en Encode string to UTF-8 byte array
*/
export function textEncode(str: string): Uint8Array {
return encoder.encode(str)
}
/**
* @zh 将 UTF-8 字节数组解码为字符串
* @en Decode UTF-8 byte array to string
*/
export function textDecode(data: Uint8Array): string {
return decoder.decode(data)
}

View File

@@ -0,0 +1,24 @@
/**
* @zh 编解码器类型定义
* @en Codec Type Definitions
*/
import type { Packet } from '../types'
/**
* @zh 编解码器接口
* @en Codec interface
*/
export interface Codec {
/**
* @zh 编码数据包
* @en Encode packet
*/
encode(packet: Packet): string | Uint8Array
/**
* @zh 解码数据包
* @en Decode packet
*/
decode(data: string | Uint8Array): Packet
}