Browse Source

📝 docs(mini): add mini-rpc development specification

- document the architecture of Taro + Hono RPC client
- define RPC client configuration and initialization process
- specify API calling methods and response handling standards
- provide WeChat mini-program login process examples
- establish file structure and naming conventions
- add best practices for error handling and loading state management
yourname 4 months ago
parent
commit
179fd66649
1 changed files with 370 additions and 0 deletions
  1. 370 0
      .roo/commands/mini-rpc.md

+ 370 - 0
.roo/commands/mini-rpc.md

@@ -0,0 +1,370 @@
+---
+description: "小程序RPC客户端开发规范 - 基于Taro + Hono RPC的完整实现指南"
+---
+
+# 小程序RPC开发规范
+
+## 概述
+
+本文档定义了小程序端使用Taro框架结合Hono RPC客户端的标准开发规范。基于现有的`mini/src/api.ts`、`mini/src/utils/rpc-client.ts`和`mini/src/pages/login/wechat-login.tsx`中的最佳实践。
+
+## 核心架构
+
+### 1. RPC客户端配置
+
+#### 1.1 客户端初始化 (`mini/src/utils/rpc-client.ts`)
+
+```typescript
+// 环境配置
+const API_BASE_URL = process.env.TARO_APP_API_BASE_URL || 'http://localhost:3000'
+
+// 自定义fetch适配Taro.request
+const taroFetch: any = async (input, init) => {
+  const url = typeof input === 'string' ? input : input.url
+  const method = init.method || 'GET'
+  
+  const requestHeaders: Record<string, string> = init.headers;
+
+  // 自动设置content-type
+  const keyOfContentType = Object.keys(requestHeaders).find(item => item.toLowerCase() === 'content-type')
+  if (!keyOfContentType) {
+    requestHeaders['content-type'] = 'application/json'
+  }
+
+  // 构建Taro请求选项
+  const options: Taro.request.Option = {
+    url,
+    method: method as any,
+    data: init.body,
+    header: requestHeaders
+  }
+
+  // 自动添加token认证
+  const token = Taro.getStorageSync('mini_token')
+  if (token) {
+    options.header = {
+      ...options.header,
+      'Authorization': `Bearer ${token}`
+    }
+  }
+
+  try {
+    const response = await Taro.request(options)
+    
+    // 处理响应数据
+    const body = response.statusCode === 204
+      ? null
+      : responseHeaders['content-type']!.includes('application/json')
+        ? JSON.stringify(response.data)
+        : response.data;
+
+    return new ResponsePolyfill(
+      body,
+      {
+        status: response.statusCode,
+        statusText: response.errMsg || 'OK',
+        headers: responseHeaders
+      }
+    )
+  } catch (error) {
+    console.error('API Error:', error)
+    Taro.showToast({
+      title: error.message || '网络错误',
+      icon: 'none'
+    })
+    throw error
+  }
+}
+
+// 创建Hono RPC客户端工厂函数
+export const rpcClient = <T extends Hono>() => {
+  return hc<T>(`${API_BASE_URL}`, {
+    fetch: taroFetch
+  })
+}
+```
+
+#### 1.2 客户端API定义 (`mini/src/api.ts`)
+
+```typescript
+import type { AuthRoutes, UserRoutes, RoleRoutes, FileRoutes } from '@/server/api'
+import { rpcClient } from './utils/rpc-client'
+
+// 创建各个模块的RPC客户端
+export const authClient = rpcClient<AuthRoutes>().api.v1.auth
+export const userClient = rpcClient<UserRoutes>().api.v1.users
+export const roleClient = rpcClient<RoleRoutes>().api.v1.roles
+export const fileClient = rpcClient<FileRoutes>().api.v1.files
+```
+
+## 使用规范
+
+### 2.1 调用方式
+
+#### 标准GET请求
+```typescript
+const response = await userClient.$get({
+  query: {
+    page: 1,
+    pageSize: 10
+  }
+})
+```
+
+#### POST请求(带请求体)
+```typescript
+const response = await authClient['mini-login'].$post({
+  json: {
+    code: loginRes.code,
+    userInfo: userProfile.userInfo
+  }
+})
+```
+
+#### 带路径参数的请求
+```typescript
+const response = await userClient[':id'].$get({
+  param: {
+    id: userId
+  }
+})
+```
+
+### 2.2 响应处理规范
+
+#### 成功响应处理
+```typescript
+if (response.status === 200) {
+  const { token, user, isNewUser } = await response.json()
+  
+  // 保存token到本地存储
+  Taro.setStorageSync('mini_token', token)
+  Taro.setStorageSync('userInfo', user)
+  
+  // 显示成功提示
+  Taro.showToast({
+    title: isNewUser ? '注册成功' : '登录成功',
+    icon: 'success',
+    duration: 1500
+  })
+}
+```
+
+#### 错误响应处理
+```typescript
+try {
+  const response = await authClient['mini-login'].$post({
+    json: { code, userInfo }
+  })
+  
+  if (response.status !== 200) {
+    const errorData = await response.json()
+    throw new Error(errorData.message || '操作失败')
+  }
+} catch (error: any) {
+  const errorMessage = error.message || '网络错误'
+  
+  // 分类处理错误
+  if (errorMessage.includes('用户拒绝授权')) {
+    Taro.showModal({
+      title: '提示',
+      content: '需要授权才能使用小程序的全部功能',
+      showCancel: false
+    })
+  } else {
+    Taro.showToast({
+      title: errorMessage,
+      icon: 'none',
+      duration: 3000
+    })
+  }
+}
+```
+
+## 微信小程序特殊场景
+
+### 3.1 微信登录流程
+
+基于`mini/src/pages/login/wechat-login.tsx`的最佳实践:
+
+```typescript
+const handleWechatLogin = async () => {
+  try {
+    Taro.showLoading({
+      title: '登录中...',
+      mask: true
+    })
+
+    // 1. 获取用户信息授权
+    const userProfile = await Taro.getUserProfile({
+      desc: '用于完善用户资料'
+    })
+
+    // 2. 获取登录code
+    const loginRes = await Taro.login()
+    
+    if (!loginRes.code) {
+      throw new Error('获取登录凭证失败')
+    }
+
+    // 3. 调用RPC接口
+    const response = await authClient['mini-login'].$post({
+      json: {
+        code: loginRes.code,
+        userInfo: userProfile.userInfo
+      }
+    })
+
+    Taro.hideLoading()
+
+    if (response.status === 200) {
+      const { token, user, isNewUser } = await response.json()
+      
+      // 4. 保存登录态
+      Taro.setStorageSync('userInfo', user)
+      Taro.setStorageSync('mini_token', token)
+      
+      // 5. 跳转页面
+      Taro.switchTab({ url: '/pages/index/index' })
+    }
+  } catch (error) {
+    Taro.hideLoading()
+    // 错误处理...
+  }
+}
+```
+
+### 3.2 平台检测
+
+```typescript
+import { isWeapp } from '@/utils/platform'
+
+// 检查是否为微信小程序环境
+const wechatEnv = isWeapp()
+if (!wechatEnv) {
+  Taro.showModal({
+    title: '提示',
+    content: '微信登录功能仅支持在微信小程序中使用',
+    showCancel: false
+  })
+}
+```
+
+## 开发规范
+
+### 4.1 文件结构
+
+```
+mini/
+├── src/
+│   ├── api.ts              # RPC客户端定义
+│   ├── utils/
+│   │   └── rpc-client.ts   # RPC客户端工厂
+│   └── pages/
+│       └── [功能页面]/
+│           └── index.tsx   # 页面逻辑
+```
+
+### 4.2 命名规范
+
+- **客户端命名**:`[模块名]Client`(如`authClient`、`userClient`)
+- **方法命名**:遵循RESTful规范(如`$get`、`$post`、`$put`、`$delete`)
+- **路径命名**:使用小写字母和连字符(如`mini-login`)
+
+### 4.3 类型安全
+
+```typescript
+// 使用InferResponseType提取响应类型
+import type { InferResponseType } from 'hono/client'
+type LoginResponse = InferResponseType<typeof authClient['mini-login']['$post'], 200>
+
+// 使用InferRequestType提取请求类型
+import type { InferRequestType } from 'hono/client'
+type LoginRequest = InferRequestType<typeof authClient['mini-login']['$post']>['json']
+```
+
+### 4.4 环境配置
+
+在`mini/.env`中配置API地址:
+
+```bash
+TARO_APP_API_BASE_URL=https://your-api-domain.com
+```
+
+## 最佳实践
+
+### 5.1 请求封装
+
+```typescript
+// 创建通用请求hook
+const useApiRequest = () => {
+  const [loading, setLoading] = useState(false)
+  
+  const request = async <T>(
+    apiCall: () => Promise<Response>,
+    successCallback?: (data: T) => void,
+    errorCallback?: (error: Error) => void
+  ) => {
+    setLoading(true)
+    try {
+      const response = await apiCall()
+      const data = await response.json()
+      
+      if (response.status === 200) {
+        successCallback?.(data)
+      } else {
+        throw new Error(data.message || '请求失败')
+      }
+    } catch (error) {
+      errorCallback?.(error)
+    } finally {
+      setLoading(false)
+    }
+  }
+  
+  return { loading, request }
+}
+```
+
+### 5.2 错误处理
+
+```typescript
+const handleApiError = (error: any) => {
+  const message = error.message || '网络错误'
+  
+  // 网络错误
+  if (message.includes('Network') || message.includes('网络')) {
+    Taro.showModal({
+      title: '网络错误',
+      content: '请检查网络连接后重试',
+      showCancel: false
+    })
+    return
+  }
+  
+  // 业务错误
+  Taro.showToast({
+    title: message,
+    icon: 'none',
+    duration: 3000
+  })
+}
+```
+
+### 5.3 加载状态管理
+
+```typescript
+const [loading, setLoading] = useState(false)
+
+const handleRequest = async () => {
+  setLoading(true)
+  Taro.showLoading({ title: '加载中...' })
+  
+  try {
+    const response = await apiClient.method.$post({ json: data })
+    // 处理响应...
+  } finally {
+    Taro.hideLoading()
+    setLoading(false)
+  }
+}