|
@@ -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)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|