1. 为什么需要封装uni-app的request请求在uni-app开发中我们经常需要与后端API进行数据交互。原生的uni.request()虽然功能完善但在实际项目中使用时会遇到几个典型问题每个请求都需要重复编写基础配置如baseURL、超时时间缺乏统一的错误处理机制难以管理接口的版本变更需要手动处理loading状态缺少请求拦截和响应拦截能力我在多个uni-app项目中踩过这些坑后总结出了一套完整的request封装方案。下面分享具体实现方法和实战经验。2. 基础封装方案设计2.1 创建request实例首先在utils目录下创建request.jsconst BASE_URL https://api.yourdomain.com/v1 const request (options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) || , ...options.header }, success: (res) { if (res.statusCode 200) { resolve(res.data) } else { reject(res.data) } }, fail: (err) { reject(err) } }) }) }2.2 添加拦截器功能增强版的request应该支持拦截器const interceptors { request: [], response: [] } const addRequestInterceptor (callback) { interceptors.request.push(callback) } const addResponseInterceptor (callback) { interceptors.response.push(callback) }3. 高级功能实现3.1 自动处理loading状态在页面请求时自动显示loadinglet loadingCount 0 const showLoading () { if (loadingCount 0) { uni.showLoading({ title: 加载中, mask: true }) } loadingCount } const hideLoading () { loadingCount-- if (loadingCount 0) { uni.hideLoading() loadingCount 0 } }3.2 请求重试机制对于网络不稳定的情况const requestWithRetry async (options, retryCount 3) { try { return await request(options) } catch (err) { if (retryCount 0) { return requestWithRetry(options, retryCount - 1) } throw err } }4. 完整封装代码示例// utils/request.js const BASE_URL https://api.yourdomain.com/v1 const DEFAULT_TIMEOUT 15000 class Request { constructor() { this.interceptors { request: [], response: [] } this.loadingCount 0 } async request(options) { // 请求拦截 for (const interceptor of this.interceptors.request) { options await interceptor(options) } this.showLoading() try { const response await this._request(options) // 响应拦截 let processedResponse response for (const interceptor of this.interceptors.response) { processedResponse await interceptor(processedResponse) } return processedResponse } finally { this.hideLoading() } } _request(options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) || , ...options.header }, timeout: DEFAULT_TIMEOUT, success: (res) { if (res.statusCode 200) { resolve(res.data) } else { reject(this._formatError(res)) } }, fail: (err) { reject(this._formatError(err)) } }) }) } _formatError(error) { // 统一错误格式 return { code: error.statusCode || -1, message: error.errMsg || 网络错误, data: error.data || null } } showLoading() { if (this.loadingCount 0) { uni.showLoading({ title: 加载中, mask: true }) } this.loadingCount } hideLoading() { this.loadingCount-- if (this.loadingCount 0) { uni.hideLoading() this.loadingCount 0 } } addRequestInterceptor(interceptor) { this.interceptors.request.push(interceptor) } addResponseInterceptor(interceptor) { this.interceptors.response.push(interceptor) } } export default new Request()5. 实际使用示例5.1 基础使用import request from /utils/request // GET请求 request({ url: /user/info, method: GET }).then(res { console.log(res) }).catch(err { console.error(err) }) // POST请求 request({ url: /user/login, method: POST, data: { username: admin, password: 123456 } })5.2 添加拦截器// 请求拦截 - 添加时间戳 request.addRequestInterceptor((config) { config.url (config.url.includes(?) ? : ?) _t${Date.now()} return config }) // 响应拦截 - 处理错误码 request.addResponseInterceptor((response) { if (response.code 401) { uni.navigateTo({ url: /pages/login/login }) return Promise.reject(response) } return response })6. 常见问题与解决方案6.1 跨域问题处理在manifest.json中配置{ h5: { devServer: { proxy: { /api: { target: https://api.yourdomain.com, changeOrigin: true, pathRewrite: { ^/api: } } } } } }6.2 请求超时优化根据业务场景调整超时时间// 文件上传可以设置更长超时 request({ url: /upload, method: POST, timeout: 60000, data: file })6.3 取消请求实现使用AbortControllerH5端const controller new AbortController() request({ // ...其他配置 signal: controller.signal }) // 需要取消时调用 controller.abort()7. 性能优化建议合理设置缓存对不常变的数据启用缓存request({ url: /config, cache: true // 自定义缓存标记 })合并请求使用Promise.all处理并行请求Promise.all([ request({url: /user/info}), request({url: /user/settings}) ])减少请求体积使用gzip压缩按需加载接口拆分大型接口为多个小型接口8. TypeScript支持对于使用TypeScript的项目可以添加类型定义// types/request.d.ts declare module /utils/request { interface RequestOptions { url: string method?: GET | POST | PUT | DELETE data?: any header?: Recordstring, string timeout?: number } interface RequestResponseT any { code: number message: string data: T } const request: { T any(options: RequestOptions): PromiseRequestResponseT addRequestInterceptor: (interceptor: (config: RequestOptions) RequestOptions | PromiseRequestOptions) void addResponseInterceptor: T any(interceptor: (response: RequestResponseT) RequestResponseT | PromiseRequestResponseT) void } export default request }9. 单元测试方案使用jest进行单元测试// tests/request.test.js import request from /utils/request describe(Request, () { it(should make GET request, async () { const mockData { id: 1, name: test } global.uni.request jest.fn((options) { options.success({ statusCode: 200, data: mockData }) }) const result await request({ url: /test }) expect(result).toEqual(mockData) }) it(should handle error, async () { global.uni.request jest.fn((options) { options.fail({ errMsg: request:fail }) }) await expect(request({ url: /test })).rejects.toMatchObject({ code: -1, message: 网络错误 }) }) })10. 项目结构建议推荐的项目结构组织方式src/ ├── api/ │ ├── user.js # 用户相关接口 │ ├── product.js # 产品相关接口 │ └── index.js # 统一导出 ├── utils/ │ └── request.js # 封装的request └── stores/ └── user.js # 状态管理接口模块化示例api/user.jsimport request from /utils/request export const login (data) { return request({ url: /user/login, method: POST, data }) } export const getUserInfo () { return request({ url: /user/info, method: GET }) }11. 版本管理策略当API需要升级版本时// utils/request.js const API_VERSION v2 // 通过环境变量配置 const request (options) { let url options.url if (!url.startsWith(/http)) { url /${API_VERSION}${url} } // ...其他配置 }12. 安全最佳实践HTTPS强制生产环境必须使用HTTPS敏感信息保护不在URL中传递敏感参数防CSRF添加CSRF Tokenrequest.addRequestInterceptor((config) { config.header[X-CSRF-Token] getCSRFToken() return config })频率限制客户端实现简单节流const lastRequestTime 0 request.addRequestInterceptor((config) { const now Date.now() if (now - lastRequestTime 1000) { throw new Error(请求过于频繁) } lastRequestTime now return config })13. 调试技巧使用Charles抓包查看实际请求数据Mock数据开发阶段使用// 只在开发环境启用 if (process.env.NODE_ENV development) { request.addResponseInterceptor((response) { if (response.config.url /user/info) { return { code: 0, data: mockUserInfo } } return response }) }日志记录关键请求添加日志request.addRequestInterceptor((config) { console.log([Request], config.method, config.url) return config }) request.addResponseInterceptor((response) { console.log([Response], response.config.method, response.config.url, response) return response })14. 性能监控添加简单的性能统计request.addRequestInterceptor((config) { config.metadata { startTime: performance.now() } return config }) request.addResponseInterceptor((response) { const duration performance.now() - response.config.metadata.startTime console.log(请求 ${response.config.url} 耗时 ${duration.toFixed(2)}ms) // 可以上报到监控系统 if (duration 3000) { reportSlowRequest(response.config.url, duration) } return response })15. 移动端特殊处理网络状态检测request.addRequestInterceptor(async (config) { const networkType await getNetworkType() if (networkType none) { throw new Error(网络不可用) } return config })重连机制const requestWithReconnect async (options, retry 0) { try { return await request(options) } catch (err) { if (err.message.includes(network) retry 3) { await new Promise(resolve setTimeout(resolve, 1000)) return requestWithReconnect(options, retry 1) } throw err } }16. 小程序端优化域名配置在小程序后台配置合法域名图片上传特殊处理const uploadFile (filePath) { return new Promise((resolve, reject) { uni.uploadFile({ url: BASE_URL /upload, filePath, name: file, success: (res) { resolve(JSON.parse(res.data)) }, fail: reject }) }) }本地存储缓存常用接口数据17. 错误统一处理创建错误处理中心// utils/errorHandler.js export const handleError (error) { console.error(API Error:, error) if (error.code 401) { // 跳转到登录页 uni.navigateTo({ url: /pages/login/login }) } else if (error.code 403) { uni.showToast({ title: 无权限访问, icon: none }) } else { uni.showToast({ title: error.message || 请求失败, icon: none }) } // 上报错误 reportError(error) } // 在request封装中使用 request.addResponseInterceptor((response) { if (response.code ! 0) { handleError(response) return Promise.reject(response) } return response })18. 请求取消功能实现请求取消功能// utils/request.js class Request { constructor() { this.pendingRequests new Map() } request(options) { const cancelToken options.cancelToken const requestKey ${options.method}:${options.url} if (cancelToken) { cancelToken.promise.then(() { const requestTask this.pendingRequests.get(requestKey) if (requestTask) { requestTask.abort() this.pendingRequests.delete(requestKey) } }) } return new Promise((resolve, reject) { const requestTask uni.request({ ...options, success: (res) { this.pendingRequests.delete(requestKey) resolve(res) }, fail: (err) { this.pendingRequests.delete(requestKey) reject(err) } }) this.pendingRequests.set(requestKey, requestTask) }) } } // 使用示例 const cancelToken { promise: null, cancel: null } cancelToken.promise new Promise((resolve) { cancelToken.cancel resolve }) request({ url: /data, cancelToken }) // 需要取消时调用 cancelToken.cancel()19. 文件下载处理封装文件下载功能const downloadFile (url, name) { return new Promise((resolve, reject) { uni.downloadFile({ url: BASE_URL url, success: (res) { if (res.statusCode 200) { uni.saveFile({ tempFilePath: res.tempFilePath, success: (saveRes) { resolve(saveRes.savedFilePath) }, fail: reject }) } else { reject(new Error(下载失败)) } }, fail: reject }) }) }20. WebSocket封装对于需要实时通信的场景class Socket { constructor(url) { this.socket null this.url url this.listeners new Map() } connect() { this.socket uni.connectSocket({ url: this.url, success: () { console.log(WebSocket连接成功) } }) this.socket.onOpen(() { console.log(WebSocket已打开) }) this.socket.onMessage((res) { const data JSON.parse(res.data) const callbacks this.listeners.get(data.type) || [] callbacks.forEach(cb cb(data)) }) this.socket.onClose(() { console.log(WebSocket已关闭) }) this.socket.onError((err) { console.error(WebSocket错误:, err) }) } on(type, callback) { if (!this.listeners.has(type)) { this.listeners.set(type, []) } this.listeners.get(type).push(callback) } send(type, data) { this.socket.send({ data: JSON.stringify({ type, data }) }) } close() { this.socket.close() } } // 使用示例 const socket new Socket(wss://api.yourdomain.com/ws) socket.connect() socket.on(message, (data) { console.log(收到消息:, data) }) socket.send(login, { token: xxx })21. 分页请求封装对于列表分页场景class Pagination { constructor(url, params {}, pageSize 10) { this.url url this.params params this.pageSize pageSize this.currentPage 1 this.total 0 this.list [] this.loading false this.noMore false } async loadNextPage() { if (this.loading || this.noMore) return this.loading true try { const res await request({ url: this.url, method: GET, data: { ...this.params, page: this.currentPage, pageSize: this.pageSize } }) if (res.code 0) { this.list [...this.list, ...res.data.list] this.total res.data.total this.currentPage if (this.list.length this.total) { this.noMore true } } } finally { this.loading false } } async refresh() { this.currentPage 1 this.list [] this.noMore false await this.loadNextPage() } } // 使用示例 const userList new Pagination(/user/list, { status: 1 }, 20) // 加载第一页 await userList.loadNextPage() // 滚动加载更多 onReachBottom() { userList.loadNextPage() } // 下拉刷新 onPullDownRefresh() { await userList.refresh() uni.stopPullDownRefresh() }22. 数据缓存策略实现请求缓存功能const cache new Map() const requestWithCache async (options) { const cacheKey JSON.stringify(options) if (options.cache cache.has(cacheKey)) { return cache.get(cacheKey) } const result await request(options) if (options.cache) { cache.set(cacheKey, result) // 设置缓存过期时间 setTimeout(() { cache.delete(cacheKey) }, options.cacheTime || 60000) } return result }23. 上传进度处理对于大文件上传显示进度const uploadWithProgress (options) { return new Promise((resolve, reject) { const uploadTask uni.uploadFile({ url: BASE_URL options.url, filePath: options.filePath, name: options.name || file, formData: options.formData, success: resolve, fail: reject, complete: options.complete }) uploadTask.onProgressUpdate((res) { if (options.onProgress) { options.onProgress(res) } }) }) } // 使用示例 uploadWithProgress({ url: /upload, filePath: path/to/file, onProgress: (res) { console.log(上传进度: ${res.progress}%) } })24. 多环境配置根据不同环境配置不同参数// config.js const env process.env.NODE_ENV || development const configs { development: { baseURL: https://dev.api.yourdomain.com, debug: true }, production: { baseURL: https://api.yourdomain.com, debug: false } } export default configs[env] // request.js import config from ./config const request (options) { return uni.request({ url: config.baseURL options.url, // ...其他配置 }) }25. 请求队列管理对于需要控制并发数的场景class RequestQueue { constructor(maxConcurrent 5) { this.maxConcurrent maxConcurrent this.queue [] this.activeCount 0 } add(requestFn) { return new Promise((resolve, reject) { this.queue.push({ requestFn, resolve, reject }) this.run() }) } run() { if (this.activeCount this.maxConcurrent || this.queue.length 0) { return } this.activeCount const { requestFn, resolve, reject } this.queue.shift() requestFn() .then(resolve) .catch(reject) .finally(() { this.activeCount-- this.run() }) } } // 使用示例 const queue new RequestQueue(3) // 添加请求到队列 queue.add(() request({ url: /api1 })) queue.add(() request({ url: /api2 }))26. 请求优先级处理实现带优先级的请求队列class PriorityRequestQueue { constructor() { this.highPriorityQueue [] this.normalPriorityQueue [] this.activeCount 0 this.maxConcurrent 5 } add(requestFn, priority normal) { return new Promise((resolve, reject) { const task { requestFn, resolve, reject } if (priority high) { this.highPriorityQueue.push(task) } else { this.normalPriorityQueue.push(task) } this.run() }) } run() { while (this.activeCount this.maxConcurrent (this.highPriorityQueue.length 0 || this.normalPriorityQueue.length 0)) { const queue this.highPriorityQueue.length 0 ? this.highPriorityQueue : this.normalPriorityQueue const { requestFn, resolve, reject } queue.shift() this.activeCount requestFn() .then(resolve) .catch(reject) .finally(() { this.activeCount-- this.run() }) } } }27. 请求Mock方案开发阶段使用Mock数据// mock.js const mockData { /user/info: { code: 0, data: { id: 1, name: Mock User } } } export const mockRequest (options) { return new Promise((resolve) { setTimeout(() { const mockResponse mockData[options.url] || { code: 404, message: 接口未Mock } resolve(mockResponse) }, 300) // 模拟网络延迟 }) } // request.js import { mockRequest } from ./mock const realRequest (options) { // 实际请求逻辑 } export const request process.env.NODE_ENV development ? (options) { // 开发环境可以配置是否使用Mock if (options.useMock) { return mockRequest(options) } return realRequest(options) } : realRequest28. 请求日志记录记录请求日志用于调试const logRequest (options) { console.groupCollapsed(%c${options.method} ${options.url}, color: #4CAF50;) console.log(Request:, options) const startTime Date.now() return request(options) .then((response) { console.log(Response:, response) console.log(Time: ${Date.now() - startTime}ms) console.groupEnd() return response }) .catch((error) { console.error(Error:, error) console.groupEnd() throw error }) } // 使用示例 logRequest({ url: /user/info, method: GET })29. 请求性能统计收集请求性能数据const performanceStats { count: 0, totalTime: 0, success: 0, fail: 0 } const trackRequestPerformance (options) { const startTime performance.now() return request(options) .then((response) { const duration performance.now() - startTime performanceStats.count performanceStats.totalTime duration performanceStats.success if (duration 1000) { console.warn(慢请求: ${options.url} (${duration.toFixed(2)}ms)) } return response }) .catch((error) { performanceStats.count performanceStats.fail throw error }) } // 可以定期上报性能数据 setInterval(() { if (performanceStats.count 0) { const avgTime performanceStats.totalTime / performanceStats.count console.log(请求统计: 总数: ${performanceStats.count} 成功: ${performanceStats.success} 失败: ${performanceStats.fail} 平均耗时: ${avgTime.toFixed(2)}ms ) // 重置统计 performanceStats.count 0 performanceStats.totalTime 0 performanceStats.success 0 performanceStats.fail 0 } }, 60000)30. 最终建议与个人经验在实际项目中封装request时我有几点重要经验分享保持适度封装不要过度设计满足当前业务需求即可随着项目发展逐步完善文档很重要为封装的request编写使用文档特别是团队项目统一错误处理确保所有错误都经过统一格式化便于前端处理考虑离线场景移动端应用需要考虑网络不可用时的降级方案版本兼容当API升级时确保旧版本仍能正常工作性能监控在生产环境添加请求性能监控及时发现慢请求安全审计定期检查请求封装是否存在安全漏洞测试覆盖为request封装编写单元测试确保核心功能稳定我在实际项目中遇到过因为request封装不当导致的几个典型问题没有统一错误处理导致每个页面都要单独处理错误缺少取消请求功能页面切换时旧请求继续执行导致数据混乱没有考虑大文件上传后来需要重构早期没有做类型定义后期加TypeScript时工作量很大希望这些经验能帮助你少走弯路。一个好的request封装应该像空气一样存在 - 开发者几乎感觉不到它的存在但它默默地支撑着所有网络请求稳定可靠。