| 版本 | 日期 | 描述 | 作者 |
|---|---|---|---|
| 1.0 | 2026-01-03 | 初始版本,基于故事010.003修复经验总结 | James (Claude Code) |
| 1.1 | 2026-01-03 | 添加中线(kebab-case)路由规范 | James (Claude Code) |
| 1.2 | 2026-01-03 | 明确规范同样适用于前端RPC client (hc) | James (Claude Code) |
| 1.3 | 2026-01-03 | 修正前端使用rpcClient而非直接使用hc | James (Claude Code) |
本文档定义了使用 Hono RPC 客户端时的正确调用方式,包括:
hono/testing 的 testClient - 用于后端集成测试@d8d/shared-ui-components/utils/hc 的 rpcClient - 用于前端 UI 包 RPC 调用两者的路径映射规则完全一致,本文档的规范适用于所有 Hono RPC 客户端。
Hono RPC 客户端(包括 testClient 和 rpcClient)会根据路由的路径结构自动生成类型安全的客户端对象。关键是理解路径到属性的映射规则。
所有 Hono RPC 客户端共享相同的路径映射规则,无论是:
testClient(来自 hono/testing)rpcClient(来自 @d8d/shared-ui-components/utils/hc)/ 转换为嵌套对象:param 转换为 [':param'] 索引/ 被移除/ 被压缩为单层嵌套- (kebab-case) 必须使用方括号 ['path-with-hyphen']// 路由定义
const route = createRoute({
method: 'get',
path: '/', // ✅ 根路径
// ...
});
app.openapi(route, handler);
// 测试调用
const client = testClient(app);
const response = await client.$get({ // ✅ 使用 .$get()
query: { page: 1 }
});
// 路由定义
const route = createRoute({
method: 'post',
path: '/users', // 单层路径
// ...
});
app.openapi(route, handler);
// 测试调用
const response = await client.users.$post({ // ✅ client.users.$post()
json: userData
});
// 路由定义
const route = createRoute({
method: 'get',
path: '/:id', // 参数路径
// ...
});
app.openapi(route, handler);
// 测试调用
const response = await client[':id'].$get({ // ✅ client[':id'].$get()
param: { id: 123 }
});
// 路由定义
const route = createRoute({
method: 'put',
path: '/users/:id', // 嵌套路由+参数
// ...
});
app.openapi(route, handler);
// 测试调用
const response = await client.users[':id'].$put({ // ✅ client.users[':id'].$put()
param: { id: 123 },
json: updateData
});
// 路由定义
const route = createRoute({
method: 'get',
path: '/admin/users/:id', // 多层嵌套+参数
// ...
});
app.openapi(route, handler);
// 测试调用
const response = await client.admin.users[':id'].$get({ // ✅ 嵌套调用
param: { id: 123 }
});
重要: JavaScript中带连字符的属性名无法用点号访问,必须使用方括号表示法。
// 路由定义
const route = createRoute({
method: 'get',
path: '/admin-users', // 中线路径
// ...
});
app.openapi(route, handler);
// ❌ 错误:点号访问中线路径会报语法错误
const response = await client.admin-users.$get();
// ✅ 正确:使用方括号表示法
const response = await client['admin-users'].$get();
// 路由定义 - 中线+参数
const route = createRoute({
method: 'put',
path: '/unified-advertisements/:id',
// ...
});
app.openapi(route, handler);
// ✅ 正确:方括号 + 嵌套参数
const response = await client['unified-advertisements'][':id'].$put({
param: { id: 123 },
json: updateData
});
// 路由定义 - 嵌套+中线
const route = createRoute({
method: 'get',
path: '/admin/unified-advertisement-types',
// ...
});
app.openapi(route, handler);
// ✅ 正确:嵌套对象 + 方括号
const response = await client.admin['unified-advertisement-types'].$get();
重要: 前端 UI 包使用 rpcClient 创建的 RPC 客户端与 testClient 使用完全相同的路径映射规则。
// 前端 API 客户端管理器(UI包标准模式)
// src/api/<module>Client.ts
import { <module>Routes } from '@d8d/<module-name>-module';
import { rpcClient } from '@d8d/shared-ui-components/utils/hc'
export class <Module>ClientManager {
private client: ReturnType<typeof rpcClient<typeof <module>Routes>> | null = null;
public init(baseUrl: string = '/'): ReturnType<typeof rpcClient<typeof <module>Routes>> {
return this.client = rpcClient<typeof <module>Routes>(baseUrl);
}
public get(): ReturnType<typeof rpcClient<typeof <module>Routes>> {
if (!this.client) {
return this.init();
}
return this.client;
}
}
// 使用示例
const <module>ClientManager = <Module>ClientManager.getInstance();
const client = <module>ClientManager.get();
// 路由定义: path: '/' (在 server 包注册为 '/api/v1/admin/<module>s')
const response = await client.index.$get({
query: { page: 1, pageSize: 10 }
});
// 路由定义: path: '/:id'
const response = await client[':id'].$put({
param: { id: 123 },
json: updateData
});
关键点:
adminClient.$get(),前端也用 client.index.$get()rpcClient 会配置 baseURL,调用时只需要使用路由的相对路径路由名称对应关系:
| 模块内路由定义 | Server注册 | rpcClient调用 |
|--------------|-----------|--------------|
| path: '/' | .route('/api/v1/admin/<module>s', routes) | client.index.$get() |
| path: '/:id' | .route('/api/v1/admin/<module>s', routes) | client[':id'].$get() |
| path: '/search' | 自定义路由 | client.search.$get() |
参考实现: UI包开发规范 - RPC客户端实现规范
// ❌ 错误:使用完整路径作为属性
const response = await client['/api/v1/users'].$get();
// ✅ 正确:去掉前缀,使用相对路径映射
const response = await client.users.$get();
// 模块路由定义为 /:id
const route = createRoute({
path: '/:id',
// ...
});
// ❌ 错误:使用了完整路径
const response = await client['/api/v1/:id'].$get();
// ✅ 正确:使用模块内的相对路径
const response = await client[':id'].$get();
:id 索引// 路由定义: path: '/users/:id'
// ❌ 错误:直接访问id属性
const response = await client.users[id].$get(); // 编译错误
// ✅ 正确:使用字符串索引
const response = await client.users[':id'].$get({
param: { id }
});
// 路由定义: path: '/admin/users/:id'
// ❌ 错误:用斜杠分隔
const response = await client['admin/users/:id'].$get();
// ❌ 错误:用点号分隔参数
const response = await client.admin.users.:id.$get();
// ✅ 正确:用嵌套对象+字符串索引
const response = await client.admin.users[':id'].$get();
// 路由定义: path: '/admin-users' 或 path: '/unified-advertisements'
// ❌ 错误:JavaScript不允许用点号访问带连字符的属性
const response = await client.admin-users.$get(); // 语法错误
const response = await client['unified-advertisements'].$get(); // ✅ 正确:必须用方括号
// ❌ 错误:混合使用点号和方括号
const response = await client.admin['unified-advertisements'].$get(); // 如果路由是 /admin/unified-advertisements
// ✅ 正确:全部使用方括号访问中线路径
const response = await client['admin-users'].$get();
const response = await client['unified-advertisements'][':id'].$put();
模块内部路由应使用相对路径(不以 / 开头的完整路径):
// ✅ 正确:模块内使用相对路径
const listRoute = createRoute({
method: 'get',
path: '/', // 或 '/:id'
middleware: [authMiddleware],
// ...
});
// ❌ 错误:模块内使用完整API路径
const listRoute = createRoute({
method: 'get',
path: '/api/v1/admin/users', // 不应在模块内包含前缀
middleware: [authMiddleware],
// ...
});
Server包注册时添加完整前缀:
// packages/server/src/index.ts
import { unifiedAdvertisementAdminRoutes } from '@d8d/unified-advertisements-module';
// ✅ 正确:Server包注册时添加完整前缀
app.route('/api/v1/admin/unified-advertisements', unifiedAdvertisementAdminRoutes);
// ✅ 正确:测试时使用模块的相对路径
const adminClient = testClient(unifiedAdvertisementAdminRoutes);
// 路由定义: path: '/'
const response = await adminClient.$get({
query: { page: 1 }
});
// 路由定义: path: '/:id'
const response = await adminClient[':id'].$put({
param: { id: 123 },
json: data
});
| 路由定义 (模块内) | Server注册 | 测试调用方式 |
|---|---|---|
path: '/' |
.route('/api/v1/users', routes) |
client.$get() |
path: '/users' |
.route('/api/v1/users', routes) |
client.users.$get() |
path: '/:id' |
.route('/api/v1/users', routes) |
client[':id'].$get() |
path: '/users/:id' |
.route('/api/v1/users', routes) |
client.users[':id'].$get() |
path: '/admin/users/:id' |
.route('/api/v1/admin', routes) |
client.admin.users[':id'].$get() |
path: '/admin-users' |
.route('/api/v1', routes) |
client['admin-users'].$get() |
path: '/unified-advertisements/:id' |
.route('/api/v1/admin', routes) |
client['unified-advertisements'][':id'].$get() |
path: '/admin/ad-types' |
.route('/api/v1/admin', routes) |
client.admin['ad-types'].$get() |
const client = testClient<typeof userRoutes>(userRoutes);
// IDE会自动补全可用的方法和路径
client.$get() // 如果路由有GET /
client.users.$get() // 如果路由有GET /users
client[':id'].$get() // 如果路由有GET /:id
// 查看路由文件中的 path 定义
// src/routes/users.ts
const listRoute = createRoute({
method: 'get',
path: '/', // ← 这里决定了测试调用方式
// ...
});
// 根据path确定测试调用
await client.$get() // path: '/' → .$get()
// 如果看到类似错误:
// "Cannot read property '$get' of undefined"
// 说明路径映射不正确
// 检查:
// 1. 路由 path 是否正确定义
// 2. testClient 是否使用了正确的路由实例
// 3. 是否有多余的路径前缀
| 路由 path | testClient 调用 |
|---|---|
/ |
.$get(), .$post(), etc. |
/:id |
[':id'].$get(), [':id'].$put(), etc. |
/users |
.users.$get(), .users.$post(), etc. |
/users/:id |
.users[':id'].$get(), .users[':id'].$put(), etc. |
/admin/users/:id |
.admin.users[':id'].$get(), etc. |
/admin-users |
['admin-users'].$get(), etc. |
/unified-advertisements/:id |
['unified-advertisements'][':id'].$get(), etc. |
/admin/ad-types |
.admin['ad-types'].$get(), etc. |
path: '/' 或 path: '/:id'.route('/api/v1/xxx', routes)/ → .$method(), /:id → [':id'].$method()/admin/users/:id → .admin.users[':id'].$method()/admin-users → ['admin-users'].$method()文档状态: 正式版 创建原因: 故事010.003修复过程中发现缺乏testClient调用规范 适用范围: 所有使用 Hono RPC 客户端的地方 覆盖范围:
hono/testing 的 testClient)@d8d/shared-ui-components/utils/hc 的 rpcClient)