1. 为什么需要封装uni-app的request请求
在uni-app开发中,网络请求是最基础也最频繁使用的功能之一。原生提供的uni.request()虽然功能完善,但在实际项目开发中直接使用会面临几个典型问题:
- 代码冗余:每个请求都需要重复编写url、header、错误处理等逻辑
- 维护困难:当需要修改请求基础配置时,需要逐个修改所有请求
- 缺乏统一处理:无法集中管理请求拦截、响应拦截、错误处理等逻辑
- 可读性差:业务代码中混杂大量网络请求细节,影响代码整洁度
以一个典型场景为例,假设我们需要在多个页面调用用户信息接口:
// 页面A uni.request({ url: 'https://api.example.com/user/info', method: 'GET', header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, success(res) { // 处理数据 }, fail(err) { console.error(err) uni.showToast({ title: '网络错误' }) } }) // 页面B又得重复写一遍这种写法在中小型项目中很快就会变得难以维护。而通过封装request,我们可以实现:
// 页面A api.getUserInfo().then(res => { // 处理数据 }).catch(err => { // 错误处理 }) // 页面B api.getUserInfo().then(...)2. 基础封装方案设计
2.1 核心封装思路
一个完整的request封装应该包含以下核心功能:
- 基础配置:统一设置baseURL、超时时间等
- 请求拦截:统一添加token、设备信息等header
- 响应拦截:统一处理响应数据、错误码
- 错误处理:网络错误、业务错误统一处理
- 便捷方法:提供get/post等快捷方法
2.2 基础实现代码
创建request.js文件:
const BASE_URL = 'https://api.example.com' const request = (options) => { // 合并配置 options.url = BASE_URL + options.url options.header = options.header || {} options.timeout = options.timeout || 60000 // 添加公共header options.header['Content-Type'] = 'application/json' const token = uni.getStorageSync('token') if (token) { options.header['Authorization'] = 'Bearer ' + token } return new Promise((resolve, reject) => { options.success = (res) => { // 响应拦截处理 if (res.statusCode === 200) { resolve(res.data) } else { reject(res) } } options.fail = (err) => { // 错误处理 uni.showToast({ title: '网络错误', icon: 'none' }) reject(err) } uni.request(options) }) } // 快捷方法 const get = (url, data, options = {}) => { return request({ url, data, method: 'GET', ...options }) } const post = (url, data, options = {}) => { return request({ url, data, method: 'POST', ...options }) } export default { request, get, post }3. 高级功能实现
3.1 请求拦截器实现
在实际项目中,我们通常需要在请求发出前做一些统一处理:
// 请求拦截器队列 const requestInterceptors = [] // 添加拦截器 const addRequestInterceptor = (interceptor) => { requestInterceptors.push(interceptor) } // 修改request函数 const request = (options) => { // ...原有代码... // 执行请求拦截器 requestInterceptors.forEach(interceptor => { options = interceptor(options) || options }) // ...剩余代码... }使用示例:
// 添加设备信息拦截器 addRequestInterceptor((config) => { config.header['Device-Id'] = uni.getSystemInfoSync().deviceId return config })3.2 响应拦截器实现
同样地,我们可以实现响应拦截:
const responseInterceptors = [] const addResponseInterceptor = (interceptor) => { responseInterceptors.push(interceptor) } // 修改request的success处理 options.success = (res) => { let response = res responseInterceptors.forEach(interceptor => { response = interceptor(response) || response }) if (response.statusCode === 200) { resolve(response.data) } else { reject(response) } }使用示例:
// 添加业务错误码拦截器 addResponseInterceptor((response) => { if (response.data.code === 401) { // token过期处理 uni.navigateTo({ url: '/pages/login/login' }) return null } return response })3.3 取消请求功能
在某些场景下(如页面卸载时),我们需要取消未完成的请求:
const request = (options) => { let requestTask = null const promise = new Promise((resolve, reject) => { // ...其他代码... requestTask = uni.request({ ...options, success: (res) => { // ...响应处理... }, fail: (err) => { // ...错误处理... } }) }) // 添加abort方法 promise.abort = () => { requestTask && requestTask.abort() } return promise }使用示例:
const req = api.getUserInfo() req.then(...).catch(...) // 需要取消时 req.abort()4. 完整实现与最佳实践
4.1 完整封装代码
结合上述所有功能,完整的request封装如下:
class Request { constructor() { this.BASE_URL = 'https://api.example.com' this.requestInterceptors = [] this.responseInterceptors = [] } request(options) { let requestTask = null // 合并配置 options.url = this.BASE_URL + options.url options.header = options.header || {} options.timeout = options.timeout || 60000 // 请求拦截 this.requestInterceptors.forEach(interceptor => { options = interceptor(options) || options }) const promise = new Promise((resolve, reject) => { options.success = (res) => { // 响应拦截 let response = res this.responseInterceptors.forEach(interceptor => { response = interceptor(response) || response }) if (response && response.statusCode === 200) { resolve(response.data) } else if (response) { reject(response) } } options.fail = (err) => { uni.showToast({ title: '网络错误', icon: 'none' }) reject(err) } requestTask = uni.request(options) }) promise.abort = () => { requestTask && requestTask.abort() } return promise } get(url, data, options = {}) { return this.request({ url, data, method: 'GET', ...options }) } post(url, data, options = {}) { return this.request({ url, data, method: 'POST', ...options }) } addRequestInterceptor(interceptor) { this.requestInterceptors.push(interceptor) } addResponseInterceptor(interceptor) { this.responseInterceptors.push(interceptor) } } const api = new Request() // 添加默认拦截器 api.addRequestInterceptor((config) => { config.header['Content-Type'] = 'application/json' const token = uni.getStorageSync('token') if (token) { config.header['Authorization'] = 'Bearer ' + token } return config }) api.addResponseInterceptor((response) => { if (response.data.code === 401) { uni.navigateTo({ url: '/pages/login/login' }) return null } return response }) export default api4.2 使用示例
import api from '@/utils/request' // 获取用户信息 api.get('/user/info').then(res => { console.log(res) }).catch(err => { console.error(err) }) // 提交表单 api.post('/form/submit', { name: '张三', age: 25 }).then(...) // 添加自定义拦截器 api.addRequestInterceptor((config) => { console.log('请求发出:', config.url) return config })5. 常见问题与解决方案
5.1 跨域问题处理
在H5端开发时,可能会遇到跨域问题。解决方案:
- 开发环境:
- 配置devServer代理
- 在manifest.json中配置:
{ "h5": { "devServer": { "proxy": { "/api": { "target": "https://api.example.com", "changeOrigin": true, "pathRewrite": { "^/api": "" } } } } } }- 生产环境:
- 使用Nginx反向代理
- 后端配置CORS
5.2 请求重试机制
对于不稳定的网络环境,可以实现自动重试:
const requestWithRetry = async (options, retryCount = 3) => { try { return await request(options) } catch (err) { if (retryCount > 0) { await new Promise(resolve => setTimeout(resolve, 1000)) return requestWithRetry(options, retryCount - 1) } throw err } }5.3 文件上传下载
对于文件操作,可以单独封装:
const uploadFile = (url, filePath, formData = {}) => { return new Promise((resolve, reject) => { uni.uploadFile({ url: BASE_URL + url, filePath, name: 'file', formData, success: resolve, fail: reject }) }) } const downloadFile = (url) => { return new Promise((resolve, reject) => { uni.downloadFile({ url: BASE_URL + url, success: resolve, fail: reject }) }) }5.4 性能优化建议
- 合理设置缓存:对于不常变的数据,可以添加缓存机制
- 请求合并:对于同页面多个请求,可以考虑合并请求
- 取消无用请求:在页面卸载时取消未完成的请求
- 合理设置超时时间:根据接口特点设置不同的超时时间
// 缓存示例 const cache = new Map() const getWithCache = async (url) => { if (cache.has(url)) { return cache.get(url) } const res = await get(url) cache.set(url, res) return res }6. 实际项目中的扩展
6.1 TypeScript支持
对于使用TypeScript的项目,可以添加类型定义:
interface RequestConfig { url: string method?: 'GET' | 'POST' | 'PUT' | 'DELETE' data?: any header?: Record<string, string> timeout?: number } interface ResponseData<T = any> { code: number message: string data: T } class Request { // ...其他代码... async request<T = any>(options: RequestConfig): Promise<ResponseData<T>> { // ...实现... } }6.2 多环境配置
在实际项目中,通常会有多个环境:
const env = process.env.NODE_ENV || 'development' const BASE_URL_MAP = { development: 'https://dev.api.example.com', test: 'https://test.api.example.com', production: 'https://api.example.com' } class Request { constructor() { this.BASE_URL = BASE_URL_MAP[env] // ...其他初始化... } // ...其他代码... }6.3 监控与日志
可以添加请求监控和日志记录:
api.addRequestInterceptor((config) => { console.log('[Request]', config.method, config.url) return config }) api.addResponseInterceptor((response) => { if (response) { console.log('[Response]', response.statusCode, response.data) } return response })对于线上环境,可以将日志上报到服务器:
api.addResponseInterceptor((response) => { if (response.statusCode >= 500) { reportError({ url: response.config.url, status: response.statusCode, data: response.data }) } return response })7. 与其他方案的对比
7.1 与直接使用uni.request对比
| 特性 | 封装方案 | 直接使用uni.request |
|---|---|---|
| 代码复用性 | 高 | 低 |
| 统一配置 | 支持 | 不支持 |
| 拦截器机制 | 支持 | 不支持 |
| 类型支持(TypeScript) | 容易 | 困难 |
| 学习成本 | 略高 | 低 |
7.2 与第三方库(flyio/axios)对比
uni-app生态中有一些第三方请求库,如flyio,它们的主要特点:
flyio:
- 支持请求/响应拦截
- 支持Promise
- 体积小巧
- 但更新维护不够活跃
axios:
- 功能强大
- 但在uni-app中需要适配
- 部分功能在小程序中不可用
自封装方案的优势:
- 完全可控,可以根据项目需求定制
- 不依赖第三方库,减少潜在风险
- 更贴合uni-app的特性
8. 测试与调试技巧
8.1 Mock数据方案
在开发阶段,可以使用Mock数据:
// 添加Mock拦截器 if (process.env.NODE_ENV === 'development') { api.addRequestInterceptor((config) => { if (config.url.startsWith('/mock/')) { config.url = config.url.replace('/mock/', '/') config.baseURL = '' // 清除baseURL return { ...config, success: (res) => { // 直接返回Mock数据 res.data = require(`@/mock${config.url}.json`) config.success(res) } } } return config }) }8.2 调试技巧
- 查看完整请求信息:
api.addRequestInterceptor((config) => { console.log('Request Config:', JSON.stringify(config, null, 2)) return config })- 性能分析:
api.addRequestInterceptor((config) => { config._startTime = Date.now() return config }) api.addResponseInterceptor((response) => { if (response) { const duration = Date.now() - response.config._startTime console.log(`请求 ${response.config.url} 耗时: ${duration}ms`) } return response })- 错误追踪:
api.addResponseInterceptor((response) => { if (response && response.statusCode >= 400) { console.error('请求错误:', { url: response.config.url, status: response.statusCode, data: response.data }) } return response })9. 安全注意事项
Token安全:
- 不要将token存储在全局变量中
- 使用uni.setStorageSync加密存储
- 设置合理的过期时间
HTTPS:
- 生产环境必须使用HTTPS
- 在manifest.json中配置:
{ "networkTimeout": { "request": 60000, "uploadFile": 60000, "downloadFile": 60000 }, "app-plus": { "sslVerify": true } }- 输入验证:
- 对所有接口返回数据进行验证
- 使用TypeScript接口定义帮助验证
interface UserInfo { id: number name: string avatar?: string } const getUserInfo = async (): Promise<UserInfo> => { const res = await api.get('/user/info') // TypeScript会帮助检查返回数据是否符合接口定义 return res.data }- 防重放攻击:
- 对重要请求添加nonce/timestamp
- 在请求拦截器中实现:
api.addRequestInterceptor((config) => { if (config.method === 'POST') { config.data = config.data || {} config.data.timestamp = Date.now() config.data.nonce = Math.random().toString(36).substring(2) } return config })10. 项目结构建议
对于大型项目,建议的请求相关代码结构:
src/ api/ modules/ user.js # 用户相关接口 product.js # 产品相关接口 order.js # 订单相关接口 request.js # 封装的request类 index.js # 统一导出所有apiuser.js示例:
import api from '../request' export default { getInfo() { return api.get('/user/info') }, updateInfo(data) { return api.post('/user/update', data) } }index.js示例:
import user from './modules/user' import product from './modules/product' import order from './modules/order' export default { user, product, order }使用方式:
import api from '@/api' api.user.getInfo().then(...)这种结构的好处:
- 接口按业务模块划分,清晰明了
- 便于团队协作,不同开发者负责不同模块
- 便于维护和查找接口
- 便于统一修改和重构
11. 性能优化进阶
11.1 请求优先级管理
对于复杂页面,可以实施请求优先级策略:
class Request { constructor() { this.pendingRequests = [] this.maxConcurrent = 5 // 最大并发数 this.currentConcurrent = 0 } async request(options) { if (this.currentConcurrent >= this.maxConcurrent) { await new Promise(resolve => { this.pendingRequests.push(resolve) }) } this.currentConcurrent++ try { // ...执行请求... } finally { this.currentConcurrent-- if (this.pendingRequests.length) { this.pendingRequests.shift()() } } } }11.2 请求去重
对于相同请求,可以避免重复发送:
const requestCache = new Map() const getCacheKey = (config) => { return `${config.method}:${config.url}:${JSON.stringify(config.data)}` } class Request { async request(options) { const cacheKey = getCacheKey(options) if (requestCache.has(cacheKey)) { return requestCache.get(cacheKey) } const promise = this._request(options) requestCache.set(cacheKey, promise) // 请求完成后清除缓存 promise.finally(() => { requestCache.delete(cacheKey) }) return promise } _request(options) { // 实际请求逻辑 } }11.3 预加载策略
对于关键数据,可以在页面加载前预加载:
// 在App.vue中 export default { onLaunch() { // 预加载用户信息 api.user.getInfo().then(res => { store.commit('setUserInfo', res.data) }).catch(() => {}) } }12. 错误监控与上报
完善的错误监控体系应包括:
- 前端错误捕获:
api.addResponseInterceptor((response) => { if (!response) return if (response.statusCode >= 500) { trackError({ type: 'API_ERROR', url: response.config.url, status: response.statusCode, message: response.data?.message || 'Unknown error' }) } return response })- 网络错误监控:
options.fail = (err) => { trackError({ type: 'NETWORK_ERROR', url: options.url, message: err.errMsg || 'Network error' }) reject(err) }- 性能监控:
api.addRequestInterceptor((config) => { config._startTime = performance.now() return config }) api.addResponseInterceptor((response) => { if (response) { const duration = performance.now() - response.config._startTime trackPerformance({ url: response.config.url, duration: Math.round(duration) }) } return response })13. 国际化支持
对于多语言项目,可以在请求头中添加语言信息:
api.addRequestInterceptor((config) => { const locale = uni.getLocale() config.header['Accept-Language'] = locale return config })对于错误消息,可以在响应拦截器中统一处理:
api.addResponseInterceptor((response) => { if (response && response.data && response.data.message) { response.data.message = i18n.t(response.data.message) } return response })14. 版本兼容性处理
随着uni-app版本更新,需要注意:
- API兼容性:
- 检查使用的uni.request参数在不同平台的兼容性
- 必要时添加平台判断:
const isWeapp = uni.getSystemInfoSync().platform === 'devtools' if (isWeapp) { // 微信小程序特有配置 options.enableHttp2 = true }- 新特性检测:
- 对于新版本添加的功能,先检测是否可用:
if (uni.canIUse('request.onHeadersReceived')) { // 使用新特性 }- 降级方案:
- 对于不支持的功能,提供降级方案
try { const requestTask = uni.request(options) // 新版本功能 if (requestTask.onHeadersReceived) { requestTask.onHeadersReceived(...) } } catch (e) { // 降级处理 }15. 单元测试建议
对于封装的request模块,建议编写单元测试验证:
describe('Request', () => { let api beforeEach(() => { api = new Request() api.BASE_URL = 'https://test.api' }) it('should add base url', async () => { const mockRequest = jest.spyOn(uni, 'request') mockRequest.mockImplementation(({success}) => { success({ statusCode: 200, data: {} }) }) await api.get('/test') expect(mockRequest).toBeCalledWith( expect.objectContaining({ url: 'https://test.api/test' }) ) }) it('should handle errors', async () => { const mockRequest = jest.spyOn(uni, 'request') mockRequest.mockImplementation(({fail}) => { fail({ errMsg: 'request:fail' }) }) await expect(api.get('/test')).rejects.toEqual({ errMsg: 'request:fail' }) }) })测试要点应包括:
- 基础功能测试(GET/POST等)
- 拦截器测试
- 错误处理测试
- 取消请求测试
- 超时处理测试
16. 未来扩展方向
随着业务发展,request封装可以进一步扩展:
GraphQL支持:
- 添加GraphQL客户端功能
- 统一处理GraphQL错误
WebSocket封装:
- 提供更易用的WebSocket API
- 自动重连、心跳检测等
离线缓存:
- 对于重要数据实现离线优先策略
- 使用IndexedDB或本地存储
请求优先级:
- 实现请求优先级队列
- 关键请求优先发送
数据压缩:
- 对大请求体进行压缩
- 减少数据传输量
17. 个人实践经验分享
在实际项目中,我总结了以下几点经验:
合理设置超时时间:
- 普通接口:5-10秒
- 文件上传:30-60秒
- 关键接口:适当延长
token刷新策略:
- 检测到token过期时自动刷新
- 避免多个并发请求同时触发刷新
let isRefreshing = false let refreshSubscribers = [] api.addResponseInterceptor(async (response) => { if (response.data.code === 401 && !isRefreshing) { isRefreshing = true try { const newToken = await refreshToken() uni.setStorageSync('token', newToken) refreshSubscribers.forEach(cb => cb(newToken)) refreshSubscribers = [] return api.request(response.config) // 重试原请求 } finally { isRefreshing = false } } else if (isRefreshing && response.config.url !== '/refreshToken') { // 将请求加入队列,等待token刷新完成后重试 return new Promise((resolve) => { refreshSubscribers.push((token) => { response.config.header.Authorization = `Bearer ${token}` resolve(api.request(response.config)) }) }) } return response })- 请求取消策略:
- 页面卸载时取消未完成请求
- 路由切换时取消非必要请求
// 在页面onUnload中 onUnload() { this._requests.forEach(req => req.abort()) this._requests = [] }- 性能监控:
- 记录关键接口响应时间
- 设置性能阈值报警
api.addResponseInterceptor((response) => { const duration = Date.now() - response.config._startTime if (duration > 2000) { trackSlowRequest({ url: response.config.url, duration }) } return response })- 调试技巧:
- 开发环境打印完整请求/响应信息
- 使用vConsole等工具查看网络请求
if (process.env.NODE_ENV === 'development') { api.addRequestInterceptor((config) => { console.groupCollapsed(`Request: ${config.method} ${config.url}`) console.log('Config:', config) return config }) api.addResponseInterceptor((response) => { if (response) { console.log('Response:', response) console.groupEnd() } return response }) }18. 总结
通过本文的封装方案,我们实现了:
- 统一管理:所有API请求集中管理,便于维护
- 功能完善:支持拦截器、取消请求、错误处理等高级功能
- 开发高效:简化业务代码,提高开发效率
- 安全可靠:内置安全措施,减少潜在风险
- 扩展性强:易于添加新功能和适配业务变化
在实际项目中,建议根据团队和项目特点调整封装方案,找到最适合的实现方式。一个好的请求封装应该随着项目发展而不断演进,持续满足业务需求。