|
|
@@ -0,0 +1,222 @@
|
|
|
+import Taro from '@tarojs/taro'
|
|
|
+import { hc } from 'hono/client'
|
|
|
+import { ResponsePolyfill } from './response-polyfill'
|
|
|
+
|
|
|
+export interface RpcClientConfig {
|
|
|
+ /** API基础URL,例如: http://localhost:3000 */
|
|
|
+ apiBaseUrl: string
|
|
|
+ /** token存储键名,默认: 'token' */
|
|
|
+ tokenStorageKey?: string
|
|
|
+ /** 刷新token存储键名,默认: 'refresh_token' */
|
|
|
+ refreshTokenStorageKey?: string
|
|
|
+ /** 刷新token的API路径,默认: '/api/v1/auth/refresh-token' */
|
|
|
+ refreshTokenPath?: string
|
|
|
+ /** 登录页面路径,token刷新失败时跳转,默认: '/pages/login/index' */
|
|
|
+ loginPagePath?: string
|
|
|
+ /** 用户信息存储键名,默认: 'userInfo' */
|
|
|
+ userInfoStorageKey?: string
|
|
|
+ /** 认证类型前缀,默认: 'Bearer ' */
|
|
|
+ authPrefix?: string
|
|
|
+}
|
|
|
+
|
|
|
+// 默认配置
|
|
|
+const defaultConfig: RpcClientConfig = {
|
|
|
+ apiBaseUrl: process.env.TARO_APP_API_BASE_URL || 'http://localhost:3000',
|
|
|
+ tokenStorageKey: 'token',
|
|
|
+ refreshTokenStorageKey: 'refresh_token',
|
|
|
+ refreshTokenPath: '/api/v1/auth/refresh-token',
|
|
|
+ loginPagePath: '/pages/login/index',
|
|
|
+ userInfoStorageKey: 'userInfo',
|
|
|
+ authPrefix: 'Bearer '
|
|
|
+}
|
|
|
+
|
|
|
+// 刷新token的管理器
|
|
|
+class TokenRefreshManager {
|
|
|
+ private isRefreshing = false
|
|
|
+ private refreshSubscribers: ((token: string | null) => void)[] = []
|
|
|
+
|
|
|
+ constructor(private config: RpcClientConfig) {}
|
|
|
+
|
|
|
+ async refreshToken(): Promise<string | null> {
|
|
|
+ if (this.isRefreshing) {
|
|
|
+ // 如果已经在刷新,等待结果
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ this.refreshSubscribers.push((token) => {
|
|
|
+ resolve(token)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ this.isRefreshing = true
|
|
|
+ try {
|
|
|
+ const refreshToken = Taro.getStorageSync(this.config.refreshTokenStorageKey!)
|
|
|
+ if (!refreshToken) {
|
|
|
+ throw new Error('未找到刷新token')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用刷新token接口
|
|
|
+ const response = await Taro.request({
|
|
|
+ url: `${this.config.apiBaseUrl}${this.config.refreshTokenPath}`,
|
|
|
+ method: 'POST',
|
|
|
+ header: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'Authorization': `${this.config.authPrefix}${refreshToken}`
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.statusCode === 200) {
|
|
|
+ const { token, refresh_token: newRefreshToken } = response.data
|
|
|
+ Taro.setStorageSync(this.config.tokenStorageKey!, token)
|
|
|
+ if (newRefreshToken) {
|
|
|
+ Taro.setStorageSync(this.config.refreshTokenStorageKey!, newRefreshToken)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 通知所有等待的请求
|
|
|
+ this.refreshSubscribers.forEach(callback => callback(token))
|
|
|
+ this.refreshSubscribers = []
|
|
|
+ return token
|
|
|
+ } else {
|
|
|
+ throw new Error('刷新token失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('刷新token失败:', error)
|
|
|
+ // 清除token,跳转到登录页
|
|
|
+ this.clearAuthData()
|
|
|
+
|
|
|
+ // 跳转到登录页
|
|
|
+ Taro.showToast({
|
|
|
+ title: '登录已过期,请重新登录',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ setTimeout(() => {
|
|
|
+ Taro.redirectTo({
|
|
|
+ url: this.config.loginPagePath!
|
|
|
+ })
|
|
|
+ }, 1500)
|
|
|
+
|
|
|
+ // 通知所有等待的请求刷新失败
|
|
|
+ this.refreshSubscribers.forEach(callback => callback(null))
|
|
|
+ this.refreshSubscribers = []
|
|
|
+ return null
|
|
|
+ } finally {
|
|
|
+ this.isRefreshing = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ clearAuthData(): void {
|
|
|
+ Taro.removeStorageSync(this.config.tokenStorageKey!)
|
|
|
+ Taro.removeStorageSync(this.config.refreshTokenStorageKey!)
|
|
|
+ Taro.removeStorageSync(this.config.userInfoStorageKey!)
|
|
|
+ }
|
|
|
+
|
|
|
+ getToken(): string | null {
|
|
|
+ return Taro.getStorageSync(this.config.tokenStorageKey!)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 创建自定义fetch函数,适配Taro.request,支持token自动刷新
|
|
|
+const createTaroFetch = (config: RpcClientConfig, tokenManager: TokenRefreshManager) => {
|
|
|
+ return async (input: RequestInfo | URL, init?: RequestInit): Promise<any> => {
|
|
|
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
|
|
|
+ const method = init?.method || 'GET'
|
|
|
+ const requestHeaders: Record<string, string> = { ...init?.headers } as Record<string, string>
|
|
|
+
|
|
|
+ // 确保有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
|
|
|
+ let token = tokenManager.getToken()
|
|
|
+ if (token) {
|
|
|
+ options.header = {
|
|
|
+ ...options.header,
|
|
|
+ 'Authorization': `${config.authPrefix}${token}`
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 发送请求
|
|
|
+ const sendRequest = async (): Promise<any> => {
|
|
|
+ try {
|
|
|
+ console.log('API请求:', options.url)
|
|
|
+ const response = await Taro.request(options)
|
|
|
+
|
|
|
+ const responseHeaders = response.header;
|
|
|
+
|
|
|
+ // 处理204 No Content响应,不设置body
|
|
|
+ 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)
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ let response = await sendRequest()
|
|
|
+
|
|
|
+ // 检查是否为401错误,尝试刷新token
|
|
|
+ if (response.status === 401 && token) {
|
|
|
+ console.log('检测到401错误,尝试刷新token...')
|
|
|
+ const newToken = await tokenManager.refreshToken()
|
|
|
+
|
|
|
+ if (newToken) {
|
|
|
+ // 更新请求header中的token
|
|
|
+ options.header = {
|
|
|
+ ...options.header,
|
|
|
+ 'Authorization': `${config.authPrefix}${newToken}`
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重试原始请求
|
|
|
+ response = await sendRequest()
|
|
|
+ } else {
|
|
|
+ // 刷新失败,返回原始401响应
|
|
|
+ return response
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return response
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('API请求失败:', error)
|
|
|
+ Taro.showToast({
|
|
|
+ title: error.message || '网络错误',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 创建Hono RPC客户端
|
|
|
+export const createRpcClient = <T extends any>(customConfig?: Partial<RpcClientConfig>) => {
|
|
|
+ const config = { ...defaultConfig, ...customConfig }
|
|
|
+ const tokenManager = new TokenRefreshManager(config)
|
|
|
+ const fetch = createTaroFetch(config, tokenManager)
|
|
|
+
|
|
|
+ // @ts-ignore
|
|
|
+ return hc<T>(`${config.apiBaseUrl}`, { fetch })
|
|
|
+}
|
|
|
+
|
|
|
+// 导出类型
|
|
|
+export type RpcClient<T> = ReturnType<typeof createRpcClient<T>>
|