UniApp请求封装实战:从拦截器陷阱到TS类型安全的全链路解决方案
第一次在UniApp+Vue3+TS项目中封装网络请求层时,我天真地以为这不过是把axios那套逻辑移植过来。直到拦截器莫名失效、TypeScript类型检查全线飘红、异步请求状态管理失控——这些坑让我在深夜调试时差点把键盘摔了。如果你也正在经历这种痛苦,不妨看看这份用头发换来的实战指南。
1. 拦截器失效的真相:UniApp的异步陷阱
很多人习惯在main.ts里直接初始化拦截器,但在UniApp中这可能导致拦截器"隐形"。我遇到过最诡异的情况是:拦截器代码明明执行了,但请求就是不走拦截逻辑。根本原因在于UniApp的运行时加载机制。
1.1 正确的拦截器注册时机
// 错误示范:直接放在main.ts顶层 uni.addInterceptor('request', {...}) // 正确姿势:确保在应用ready后注册 onLaunch(() => { initHttpInterceptor() // 封装好的拦截器初始化函数 })关键发现:UniApp的API模块是动态加载的,过早注册会导致拦截器绑定失败。实测发现,在onLaunch或页面onLoad阶段注册最可靠。
1.2 拦截器参数传递的黑盒现象
当你的拦截器接收到的options总是缺少某些字段时,这不是幻觉。UniApp的请求参数处理有个隐藏特性:
uni.addInterceptor('request', { invoke(args) { // args会被深拷贝,直接修改可能不生效 const newArgs = { ...args, url: processUrl(args.url), // 必须返回新对象 header: mergeHeaders(args.header) } return newArgs // 关键!必须返回完整配置 } })对比传统axios拦截器,UniApp的这个设计差异常容易踩坑:
| 特性 | Axios拦截器 | UniApp拦截器 |
|---|---|---|
| 参数修改方式 | 直接修改config | 必须返回新对象 |
| 异步支持 | 完全支持 | 有限支持 |
| 错误捕获范围 | 全局 | 可能遗漏部分错误 |
2. TS类型体操:从Any到类型安全
初期我的请求封装返回的都是Promise<any>,直到项目大了才发现类型检查形同虚设。经过三次重构,最终定型这套类型方案:
2.1 三层泛型设计
// 基础响应结构 type BaseResponse<T = any> = { code: number message: string data: T timestamp: number } // 请求配置扩展 type RequestConfig<T = any> = UniApp.RequestOptions & { mock?: boolean transformResponse?: (res: T) => any } // 核心请求方法 function request<T = any, R = BaseResponse<T>>( config: RequestConfig<R> ): Promise<R> { // 实现逻辑... }使用示例:
// 获取用户信息 interface UserProfile { name: string age: number avatar: string } const fetchUser = () => request<UserProfile>({ url: '/api/user' }) // 此时response.data自动推断为UserProfile类型 fetchUser().then(res => { console.log(res.data.age) // 完美类型提示! })2.2 类型守卫的妙用
针对后端可能返回的各种异常数据结构,我们可以在类型层面做安全防护:
function isSuccessResponse(res: any): res is BaseResponse { return ( typeof res === 'object' && 'code' in res && 'data' in res && res.code === 200 ) } // 在拦截器中 if (isSuccessResponse(response)) { // 此处response自动获得正确类型 return response.data } else { throw new Error('Invalid response structure') }3. 异常处理的艺术:不只是Toast那么简单
见过太多项目用uni.showToast处理所有异常,直到我们的测试人员发现:连续快速触发多个请求失败时,Toast会形成"弹窗风暴"。这是我们的改进方案:
3.1 异常分级处理机制
const ERROR_STRATEGY = { NETWORK_ERROR: { level: 'blocking', handler: () => navigateTo('/network-error') }, AUTH_EXPIRED: { level: 'critical', handler: () => { clearToken() showModal({ title: '登录过期', content: '请重新登录' }) } }, DEFAULT: { level: 'notice', handler: (msg) => showToast(msg) } } // 使用示例 function handleError(error) { const strategy = ERROR_STRATEGY[error.code] || ERROR_STRATEGY.DEFAULT // 防抖处理 if (!this.debouncing) { strategy.handler(error.message) this.debouncing = true setTimeout(() => this.debouncing = false, 2000) } }3.2 请求重试的智能策略
对于网络波动导致的失败,我们实现了指数退避重试:
async function requestWithRetry( config: RequestConfig, retries = 3, delay = 1000 ): Promise<any> { try { return await request(config) } catch (error) { if (shouldRetry(error) && retries > 0) { await new Promise(resolve => setTimeout(resolve, delay) ) return requestWithRetry( config, retries - 1, delay * 2 // 每次延迟时间翻倍 ) } throw error } } function shouldRetry(error) { return [ 'NETWORK_ERROR', 'TIMEOUT', 'SERVER_UNAVAILABLE' ].includes(error.code) }4. 性能优化:看不见的细节处理
当我们的用户列表页达到100+并发请求时,发现了严重的性能瓶颈。以下是关键的优化点:
4.1 请求去重与缓存
const pendingRequests = new Map() function getRequestKey(config) { return `${config.method}-${config.url}-${JSON.stringify(config.data)}` } async function deduplicatedRequest(config) { const key = getRequestKey(config) if (pendingRequests.has(key)) { return pendingRequests.get(key) } const promise = request(config).finally(() => { pendingRequests.delete(key) }) pendingRequests.set(key, promise) return promise }4.2 智能超时设置
根据网络环境动态调整超时时间:
function getDynamicTimeout() { const { networkType } = uni.getNetworkTypeSync() const timeouts = { wifi: 10000, '4g': 15000, '3g': 20000, '2g': 30000, unknown: 25000 } return timeouts[networkType] || 15000 } // 在请求配置中 uni.request({ timeout: getDynamicTimeout() })5. 实战中的那些"骚操作"
项目上线后我们收集了一些特殊场景的解决方案,这几个技巧可能会救你一命:
5.1 文件上传的进度劫持
UniApp的upload API进度事件有时不触发,这是我们找到的替代方案:
uni.uploadFile({ // ...其他配置 formData: { _progress_monitor: '1' // 后端配合返回进度数据 }, success(res) { if (res.data) { const { progress } = JSON.parse(res.data) // 使用假进度数据 } } })5.2 请求取消的另类实现
由于UniApp没有真正的AbortController,我们通过标记法模拟:
let cancelToken = { cancelled: false } function cancelableRequest(config) { if (config.cancelToken) { config.cancelToken.cancel = () => { config.cancelToken.cancelled = true } } return new Promise((resolve, reject) => { uni.request({ ...config, success: (res) => { if (!config.cancelToken?.cancelled) { resolve(res) } }, fail: reject }) }) }在最近一次项目迭代中,这套请求封装经受住了单日百万级请求的考验。最让我欣慰的不是零崩溃的记录,而是新同事能在完全不看文档的情况下,凭借类型提示就能正确调用所有API。