Huly 平台客户端接入指南:基于 @hcengineering/client-resources 构建与运行你的 Client
【免费下载链接】platformHuly — All-in-One Project Management Platform (alternative to Linear, Jira, Slack, Notion, Motion)项目地址: https://gitcode.com/GitHub_Trending/platform80/platform
导读
本文围绕 Huly 平台核心包之一@hcengineering/client-resources(位于 foundations/core/packages/client-resources)展开,系统讲解如何创建一个与正在运行的 Huly 平台交互的客户端(Client)。你将掌握GetClient的完整调用链、Token 与连接地址的拼接规则、模型过滤(FilterMode)与 IndexedDB 持久化原理、Node.js 环境下 WebSocket 工厂的配置方法,以及连接心跳、重连、二进制协议等底层容错机制,可直接照搬文中代码集成到自己的脚本或服务中。
一、包概览:client-resources 在整个 Huly 平台中的定位
@hcengineering/client-resources是一个"平台资源包":它不直接提供 API 类,而是以 Huly 平台标准的plugin资源形式,暴露一个名为GetClient的函数资源,供应用层按需加载。从 package.json 可以看到它的核心依赖:
@hcengineering/client:定义ClientFactoryOptions、ClientSocketFactory、FilterMode等类型与元数据(client/src/index.ts);@hcengineering/core:提供Client、Tx、TxHandler、createClient、ClientConnectEvent等核心抽象(core/src/client.ts);@hcengineering/rpc:提供 RPC 编解码(RPCHandler)与HelloRequest/HelloResponse握手协议;@hcengineering/platform:提供getMetadata、setPlatformStatus、Status等平台基础设施;snappyjs:用于服务端推送数据的 Snappy 解压。
该包自带jest测试(npm test),覆盖了 WebSocket 连接层与客户端集成层,后续章节会结合这些测试说明实际行为。
二、快速开始:三步拿到可用的 Client
官方 readme 给出了最简用法,完整代码如下:
import clientResources from '@hcengineering/client-resources' import core, { Client } from '@hcengineering/core' // token 通过登录/账号服务获得,内容为 JWT 格式(见下文 Token 解析一节) const token = ... // transactorUrl 为 Huly 平台的事务服务(transactor)端点,如 https://host/api/transactor const connection: Client = await (await clientResources()).function.GetClient(token, transactorUrl) // 至此 client 已可用,可以执行 findAll / tx 等操作 // 使用 close 优雅关闭连接 await connection.close()要点拆解:
clientResources()是一个异步工厂函数,调用后返回{ function: { GetClient } }结构(见 src/index.ts 的默认导出);GetClient(token, endpoint, opt?)返回Promise<Client>,其中Client接口由@hcengineering/core定义;- 调用
close()会关闭底层 WebSocket 并拒绝所有未完成的请求,务必在退出前调用。
三、GetClient 内部实现:从 Token 到连接
GetClient的签名是(token: string, endpoint: string, opt?: ClientFactoryOptions): Promise<Client>,其内部做了三件关键事情,源码位于 src/index.ts。
3.1 Token 解析与合法性校验
GetClient先把 JWT Token 的 payload 段(token.split('.')[1])做atob解码并JSON.parse,得到workspace与account两个字段(decodeTokenPayload与getWSFromToken两个函数,src/index.ts):
interface TokenPayload { workspace?: WorkspaceUuid account?: PersonUuid extra?: any }如果 payload 中缺少workspace或account,会直接抛出'Workspace or account not found in token'。也就是说,Token 中必须携带工作区与账号信息,客户端才能正确建立工作区上下文。
3.2 连接 URL 拼接
真正的 WebSocket 地址由concatLink(endpoint,/${token})生成(src/index.ts),即transactorUrl/token形式;随后connect()(见 src/connection.ts)创建Connection实例,并在 socket 打开后于 URL 末尾追加?sessionId=...用于断线重连时的会话恢复(见 src/connection.ts)。
3.3 服务端推送的事务拦截
连接建立后,所有服务端下推的事务(Tx)会先经过upgradeHandler过滤(src/index.ts):
- 收到
TxModelUpgrade:触发opt?.onUpgrade?.(),提示应用层模型已升级,需要重建客户端; - 收到
TxWorkspaceEvent且事件为MaintenanceNotification:通过setPlatformStatus抛出Severity.WARNING级别的维护提醒,附带的timeMinutes与message参数会透传给 UI。
四、模型过滤:FilterMode 的三种模式
GetClient会读取平台元数据client.metadata.FilterModel(默认'none')与client.metadata.ExtraFilter(默认[]),构造一个ModelFilter回调传给createClient(src/index.ts)。三种取值(定义于 client/src/index.ts)行为如下:
| FilterModel | 行为 |
|---|---|
'none' | 不过滤,原样返回全部模型事务 |
'client' | 过滤掉所有server-前缀插件与未启用插件,并剔除workbench:class:Application、view:class:Action、notification:class:NotificationGroup等 20 余类 UI 专属模型元素(见returnClientTxes的toExclude集合,src/index.ts) |
'ui' | 过滤掉所有服务端元素与未被启用的 UI 元素,额外叠加ExtraFilter中列出的插件 ID(returnUITxes,src/index.ts) |
// 示例:在创建 Client 前设置过滤模式与额外排除项 import client from '@hcengineering/client' import { setMetadata } from '@hcengineering/platform' setMetadata(client.metadata.FilterModel, 'ui') setMetadata(client.metadata.ExtraFilter, ['my-private-plugin'])ExtraPlugins元数据则用于在'ui'模式下把额外插件纳入"允许加载"集合(src/index.ts)。
五、模型持久化:IndexedDB 缓存
createModelPersistence(workspace)(src/index.ts)在浏览器环境下会打开indexedDB.open('model.db.persistence', 2),并在其中建立model对象仓库(keyPath 为id),以workspace 为键缓存LoadModelResponse:
load:按 workspace 读取缓存模型;若无缓存返回{ full: false, transactions: [], hash: '' };store:把拉取到的模型写入缓存,下次启动可跳过全量加载;- 可通过
client.metadata.OverridePersistenceStore传入自定义的TxPersistenceStore覆盖默认实现。
六、Node.js 环境:必须配置 WebSocket 工厂
readme 中特别强调:在 Node.js 环境必须用ws包替换默认 WebSocket 实现。原因是浏览器环境下Connection直接使用全局WebSocket,而 Node.js 没有该全局对象,需要通过client.metadata.ClientSocketFactory元数据注入ws:
import client from '@hcengineering/client' import { setMetadata } from '@hcengineering/platform' import WebSocket from 'ws' // 用 'ws' 覆盖默认的 WebSocket 工厂 setMetadata(client.metadata.ClientSocketFactory, (url) => new WebSocket(url)) const connection: Client = await (await clientResources()).function.GetClient(token, transactorUrl) // ... await connection.close()ClientSocketFactory的类型为(url: string) => ClientSocket,其中ClientSocket是平台自定义的最小 WebSocket 抽象(onmessage/onclose/onopen/onerror/send/close/readyState,见 client/src/index.ts),因此任何符合该形状的实现(浏览器 WebSocket、ws、mock 对象)都可以注入。ws已声明为 devDependencies(package.json),用于测试与 Node 侧运行。
七、ClientFactoryOptions 完整参数
GetClient的第三个可选参数opt类型为ClientFactoryOptions(定义于 client/src/index.ts),逐项说明:
| 参数 | 类型 | 作用 |
|---|---|---|
socketFactory | ClientSocketFactory | 按连接 URL 创建 socket,优先级最高(高于全局ClientSocketFactory元数据) |
useBinaryProtocol | boolean | 是否使用二进制 RPC 协议,默认取元数据UseBinaryProtocol,再回退到true(src/connection.ts) |
useProtocolCompression | boolean | 是否启用 Snappy 压缩,默认取元数据UseProtocolCompression,回退到false(src/connection.ts) |
connectionTimeout | number | 连接超时(毫秒),大于 0 时启用;超时未连接会触发onDialTimeout并拒绝连接 Promise(src/index.ts) |
onHello | (serverVersion?: string) => boolean | 收到服务端hello响应(含版本号)时回调,返回false将主动断开(src/connection.ts) |
onUpgrade | () => void | 检测到模型升级时回调 |
onError | (err: StatusCode) => void | 服务端返回terminate错误(如工作区归档/不存在)时回调 |
onConnect | (event, lastTx, data) => Promise<void> | 连接/重连/维护等事件回调,event为ClientConnectEvent |
onDialTimeout | () => void \| Promise<void> | 拨号超时回调 |
ctx | MeasureContext | 指标上下文,用于性能埋点 |
useGlobalRPCHandler | boolean | 为true时共享全局RPCHandler(默认每个连接新建一个,src/connection.ts) |
ClientConnectEvent枚举(core/src/client.ts)包含Connected(首次连接并收到完整模型)、Reconnected(重连后应用增量)、Upgraded(收到全量新模型需重建)、Refresh(需要刷新查询)、Maintenance(工作区维护中)。
八、连接生命周期与容错机制
Connection类(src/connection.ts)是连接层的核心,值得关注的机制包括:
- 握手协议:socket 打开后立即发送
hello请求,携带binary与compression标志;服务端以hello响应确认协议能力并返回lastHash、serverVersion、account,随后才把连接视为可用(helloReceived = true,src/connection.ts); - 心跳保活:以 10 秒为周期发送
ping(pingConst = 'ping'),若 5 分钟(hangTimeout)未收到 pong 则判定挂死并关闭 socket 触发重连(src/connection.ts、src/connection.ts); - 拨号超时:30 秒(
dialTimeout)内未完成握手会回调onDialTimeout并强制重建连接(src/connection.ts); - 自动重连与退避:socket 关闭时自动
scheduleOpen(force=true),错误发生后delay从 1 递增至上限 3 秒进行指数退避(src/connection.ts、src/connection.ts); - 会话恢复:
sessionId会写入sessionStorage,页面刷新后重连时携带原 sessionId,服务端据此恢复订阅(src/connection.ts); - 限流保护:服务端返回的
rateLimit信息被实时跟踪,remaining过低时引入slowDownTimer主动限速;剩余为 0 时按retryAfter延迟重试(src/connection.ts); - 分块响应:
findAll大结果集支持chunk分片,客户端按index排序拼接后再 resolve(src/connection.ts); - 去重与重试:
once请求会按 method+params 去重;非幂等tx在重连时会先查询事务是否已提交再决定是否重发(src/connection.ts、src/connection.ts)。
九、Client 常用操作
连接建立后,Client提供完整的存储与查询 API(ClientImpl见 core/src/client.ts),常用方法包括:
// 查询:findAll / findOne const tasks = await connection.findAll(core.class.Tx, { objectClass: 'tracker:class:Issue' }, { limit: 20 }) const one = await connection.findOne(core.class.Space, { name: 'My Space' }) // 写入:提交事务 await connection.tx(tx) // 模型相关 await connection.getHierarchy() await connection.getModel()底层 RPC 方法在 src/connection.ts 中均有对应实现(loadModel、findAll、tx、loadChunk、getDomainHash、searchFulltext、domainRequest、sendForceClose等),可作为排查网络问题的参考。
十、测试验证:连接层与集成层
该包用 Jest 覆盖了两层行为,是理解契约的最佳范例:
- 连接层单测(src/tests/connection.test.ts):自定义
MockWebSocket模拟hello、loadModel、findAll、tx响应与ping/pong回包,验证connect()能建立连接并正确分发服务端下推的事务; - 集成测试(src/tests/integration.test.ts):提供
MockClientConnection与createTestClient()辅助函数,覆盖事务端到端流转、findAll/findOne/searchFulltext/domainRequest、连接断开恢复、多 handler 分发、Upgraded/Maintenance事件以及并发事务提交等场景。
运行测试:
npm test --prefix foundations/core/packages/client-resources十一、常见问题排查
- Node.js 下
WebSocket is not defined:未设置client.metadata.ClientSocketFactory,按第六节注入ws即可; - 报错
Workspace or account not found in token:Token 的 payload 段缺少workspace/account字段,请检查登录流程签发的 Token 内容; - 连接长时间不建立:确认
transactorUrl正确且可达,并观察connectionTimeout是否过小(可在opt或ConnectionTimeout元数据中调整); - 模型升级后行为异常:监听
onUpgrade回调并重建 Client(ClientConnectEvent.Upgraded语义见 core/src/client.ts); - 需要最小化模型体积:为
FilterModel元数据设置'client'或'ui',结合ExtraFilter排除不需要的插件。
总结
@hcengineering/client-resources是 Huly 平台所有客户端接入的统一入口:GetClient负责 Token 校验、URL 拼接、模型过滤与本地持久化,底层Connection则封装了 WebSocket 握手、心跳、重连、限流与二进制/压缩协议等全部传输细节。无论你是编写浏览器端插件、Node.js 脚本,还是测试工具,只需提供合法 Token 与 transactor 端点,并(在 Node 侧)正确注入ws工厂,即可获得功能完整、具备断线自愈能力的平台客户端。
【免费下载链接】platformHuly — All-in-One Project Management Platform (alternative to Linear, Jira, Slack, Notion, Motion)项目地址: https://gitcode.com/GitHub_Trending/platform80/platform
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考