# Hono RPC Client 调用规范 ## 版本信息 | 版本 | 日期 | 描述 | 作者 | |------|------|------|------| | 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`) - 前端 UI 包使用的 `rpcClient`(来自 `@d8d/shared-ui-components/utils/hc`) ### 核心映射规则 1. **路径分隔符 `/` 转换为嵌套对象** 2. **路径参数 `:param` 转换为 `[':param']` 索引** 3. **开头的 `/` 被移除** 4. **连续的 `/` 被压缩为单层嵌套** 5. **中线 `-` (kebab-case) 必须使用方括号 `['path-with-hyphen']`** ## 调用方式详解 ### 1. 简单路由(无参数) ```typescript // 路由定义 const route = createRoute({ method: 'get', path: '/', // ✅ 根路径 // ... }); app.openapi(route, handler); // 测试调用 const client = testClient(app); const response = await client.$get({ // ✅ 使用 .$get() query: { page: 1 } }); ``` ```typescript // 路由定义 const route = createRoute({ method: 'post', path: '/users', // 单层路径 // ... }); app.openapi(route, handler); // 测试调用 const response = await client.users.$post({ // ✅ client.users.$post() json: userData }); ``` ### 2. 路由带参数 ```typescript // 路由定义 const route = createRoute({ method: 'get', path: '/:id', // 参数路径 // ... }); app.openapi(route, handler); // 测试调用 const response = await client[':id'].$get({ // ✅ client[':id'].$get() param: { id: 123 } }); ``` ```typescript // 路由定义 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 }); ``` ### 3. 嵌套路由 ```typescript // 路由定义 const route = createRoute({ method: 'get', path: '/admin/users/:id', // 多层嵌套+参数 // ... }); app.openapi(route, handler); // 测试调用 const response = await client.admin.users[':id'].$get({ // ✅ 嵌套调用 param: { id: 123 } }); ``` ### 4. 路由带中线(kebab-case) **重要**: JavaScript中带连字符的属性名无法用点号访问,必须使用方括号表示法。 ```typescript // 路由定义 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(); ``` ```typescript // 路由定义 - 中线+参数 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 }); ``` ```typescript // 路由定义 - 嵌套+中线 const route = createRoute({ method: 'get', path: '/admin/unified-advertisement-types', // ... }); app.openapi(route, handler); // ✅ 正确:嵌套对象 + 方括号 const response = await client.admin['unified-advertisement-types'].$get(); ``` ### 5. 前端 UI 包 RPC Client 使用 **重要**: 前端 UI 包使用 `rpcClient` 创建的 RPC 客户端与 `testClient` 使用**完全相同的路径映射规则**。 ```typescript // 前端 API 客户端管理器(UI包标准模式) // src/api/Client.ts import { Routes } from '@d8d/-module'; import { rpcClient } from '@d8d/shared-ui-components/utils/hc' export class ClientManager { private client: ReturnTypeRoutes>> | null = null; public init(baseUrl: string = '/'): ReturnTypeRoutes>> { return this.client = rpcClientRoutes>(baseUrl); } public get(): ReturnTypeRoutes>> { if (!this.client) { return this.init(); } return this.client; } } // 使用示例 const ClientManager = ClientManager.getInstance(); const client = ClientManager.get(); // 路由定义: path: '/' (在 server 包注册为 '/api/v1/admin/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 }); ``` **关键点**: 1. **rpcClient 调用路径与 testClient 完全一致**: 如果测试用 `adminClient.$get()`,前端也用 `client.index.$get()` 2. **不需要重复前缀**: `rpcClient` 会配置 baseURL,调用时只需要使用路由的相对路径 3. **类型安全**: TypeScript 会自动推断正确的路径和方法 **路由名称对应关系**: | 模块内路由定义 | Server注册 | rpcClient调用 | |--------------|-----------|--------------| | `path: '/'` | `.route('/api/v1/admin/s', routes)` | `client.index.$get()` | | `path: '/:id'` | `.route('/api/v1/admin/s', routes)` | `client[':id'].$get()` | | `path: '/search'` | 自定义路由 | `client.search.$get()` | **参考实现**: UI包开发规范 - RPC客户端实现规范 ## 常见错误 ❌ ### 错误1: 使用完整路径字符串 ```typescript // ❌ 错误:使用完整路径作为属性 const response = await client['/api/v1/users'].$get(); // ✅ 正确:去掉前缀,使用相对路径映射 const response = await client.users.$get(); ``` ### 错误2: 路径与模块定义不匹配 ```typescript // 模块路由定义为 /:id const route = createRoute({ path: '/:id', // ... }); // ❌ 错误:使用了完整路径 const response = await client['/api/v1/:id'].$get(); // ✅ 正确:使用模块内的相对路径 const response = await client[':id'].$get(); ``` ### 错误3: 忘记使用 `:id` 索引 ```typescript // 路由定义: path: '/users/:id' // ❌ 错误:直接访问id属性 const response = await client.users[id].$get(); // 编译错误 // ✅ 正确:使用字符串索引 const response = await client.users[':id'].$get({ param: { id } }); ``` ### 错误4: 嵌套路由使用错误的分隔方式 ```typescript // 路由定义: 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(); ``` ### 错误5: 中线路径使用点号访问 ```typescript // 路由定义: 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(); ``` ## 后端模块开发规范关联 ### 路由定义规范 **模块内部路由应使用相对路径**(不以 `/` 开头的完整路径): ```typescript // ✅ 正确:模块内使用相对路径 const listRoute = createRoute({ method: 'get', path: '/', // 或 '/:id' middleware: [authMiddleware], // ... }); // ❌ 错误:模块内使用完整API路径 const listRoute = createRoute({ method: 'get', path: '/api/v1/admin/users', // 不应在模块内包含前缀 middleware: [authMiddleware], // ... }); ``` **Server包注册时添加完整前缀**: ```typescript // packages/server/src/index.ts import { unifiedAdvertisementAdminRoutes } from '@d8d/unified-advertisements-module'; // ✅ 正确:Server包注册时添加完整前缀 app.route('/api/v1/admin/unified-advertisements', unifiedAdvertisementAdminRoutes); ``` ### 测试调用规范 ```typescript // ✅ 正确:测试时使用模块的相对路径 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()` | ## 调用技巧 ### 1. 使用 TypeScript 类型提示 ```typescript const client = testClient(userRoutes); // IDE会自动补全可用的方法和路径 client.$get() // 如果路由有GET / client.users.$get() // 如果路由有GET /users client[':id'].$get() // 如果路由有GET /:id ``` ### 2. 查看路由定义确认调用方式 ```typescript // 查看路由文件中的 path 定义 // src/routes/users.ts const listRoute = createRoute({ method: 'get', path: '/', // ← 这里决定了测试调用方式 // ... }); // 根据path确定测试调用 await client.$get() // path: '/' → .$get() ``` ### 3. 调试错误信息 ```typescript // 如果看到类似错误: // "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. | ### 核心原则 1. **模块内使用相对路径**:`path: '/'` 或 `path: '/:id'` 2. **Server注册时添加前缀**:`.route('/api/v1/xxx', routes)` 3. **测试时映射路径结构**:`/` → `.$method()`, `/:id` → `[':id'].$method()` 4. **嵌套路径反映为嵌套对象**:`/admin/users/:id` → `.admin.users[':id'].$method()` 5. **中线路径必须用方括号**:`/admin-users` → `['admin-users'].$method()` ## 相关文档 - [后端模块包开发规范](./backend-module-package-standards.md) - 路由定义规范 - [后端模块包测试规范](./backend-module-testing-standards.md) - 测试框架和集成测试 - [UI包开发规范](./ui-package-standards.md) - RPC客户端实现规范 - [编码标准](./coding-standards.md) - RPC API测试规范章节 --- **文档状态**: 正式版 **创建原因**: 故事010.003修复过程中发现缺乏testClient调用规范 **适用范围**: 所有使用 Hono RPC 客户端的地方 **覆盖范围**: - 后端集成测试 (`hono/testing` 的 `testClient`) - 前端 UI 包 RPC 调用 (`@d8d/shared-ui-components/utils/hc` 的 `rpcClient`)