uni.request 是个原始 API:没有拦截器、不支持 Promise 链式调用(callback 风格)、错误处理要层层嵌套。任何像样的 uni-app 项目第一步都是封装请求层。本文从零实现一个生产级的 request 模块,覆盖拦截器、Token 无感刷新、并发请求控制与上传下载。
一、基础封装:Promise 化 + 统一错误处理
// utils/request.js
const BASE_URL = 'https://api.example.com'
const TIMEOUT = 10000
function request(options = {}) {
return new Promise((resolve, reject) => {
uni.request({
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: options.header || {},
timeout: TIMEOUT,
success: (res) => {
// HTTP 层成功
if (res.statusCode >= 200 && res.statusCode < 300) {
// 业务层判断
const body = res.data
if (body.code === 0) {
resolve(body.data)
} else {
handleBizError(body)
reject(body)
}
} else if (res.statusCode === 401) {
// 交给 Token 刷新流程处理
refreshTokenAndRetry(options).then(resolve).catch(reject)
} else {
uni.showToast({ title: `服务异常(${res.statusCode})`, icon: 'none' })
reject(res)
}
},
fail: (err) => {
// 网络层失败:断网、DNS、超时
uni.showToast({
title: err.errMsg.includes('timeout') ? '请求超时' : '网络连接失败',
icon: 'none'
})
reject(err)
}
})
})
}这里把三层错误分开处理:网络层(fail 回调)、HTTP 层(statusCode)、业务层(响应体 code),每层给用户不同的提示,排查问题时也能立刻定位层级。
二、请求拦截:自动携带 Token
// utils/auth.js
export function getToken() {
return uni.getStorageSync('access_token') || ''
}
export function setTokens(access, refresh) {
uni.setStorageSync('access_token', access)
uni.setStorageSync('refresh_token', refresh)
}
export function clearTokens() {
uni.removeStorageSync('access_token')
uni.removeStorageSync('refresh_token')
}在 request 内统一注入:
function request(options = {}) {
// 白名单接口不携带 Token
const whiteList = ['/auth/login', '/auth/captcha']
const needAuth = !whiteList.includes(options.url)
const header = {
'Content-Type': 'application/json',
...(options.header || {})
}
if (needAuth && getToken()) {
header.Authorization = `Bearer ${getToken()}`
}
return new Promise((resolve, reject) => {
uni.request({
url: BASE_URL + options.url,
header,
// ...同上
})
})
}
// 便捷方法
request.get = (url, data) => request({ url, method: 'GET', data })
request.post = (url, data) => request({ url, method: 'POST', data })
request.put = (url, data) => request({ url, method: 'PUT', data })
request.delete = (url, data) => request({ url, method: 'DELETE', data })
export default request业务代码立刻清爽:
import request from '@/utils/request'
// 页面里
async loadOrders() {
this.loading = true
try {
this.orders = await request.get('/orders', { page: this.page, size: 20 })
} finally {
this.loading = false
}
}三、Token 无感刷新(核心难点)
Access Token 过期时,理想体验是用户毫无感知地完成刷新并重放失败请求。难点在于:并发多个请求同时 401 时,只允许刷新一次,其余请求排队等待。
// utils/request.js
let isRefreshing = false
let pendingQueue = [] // 等待刷新完成的请求
function refreshTokenAndRetry(options) {
return new Promise((resolve, reject) => {
pendingQueue.push({ options, resolve, reject })
if (isRefreshing) return // 已有刷新任务在跑,只入队
isRefreshing = true
const refreshToken = uni.getStorageSync('refresh_token')
// 用原始 uni.request 刷新,避免递归
uni.request({
url: BASE_URL + '/auth/refresh',
method: 'POST',
data: { refreshToken },
success: (res) => {
if (res.statusCode === 200 && res.data.code === 0) {
const { accessToken, refreshToken: newRefresh } = res.data.data
setTokens(accessToken, newRefresh)
// 重放所有排队请求
pendingQueue.forEach(({ options, resolve, reject }) => {
request(options).then(resolve).catch(reject)
})
} else {
// 刷新失败:登录态彻底失效
clearTokens()
pendingQueue.forEach(({ reject }) => reject(new Error('登录已过期')))
uni.showToast({ title: '请重新登录', icon: 'none' })
setTimeout(() => uni.reLaunch({ url: '/pages/login/login' }), 1500)
}
},
fail: (err) => {
pendingQueue.forEach(({ reject }) => reject(err))
},
complete: () => {
isRefreshing = false
pendingQueue = []
}
})
})
}这套机制的关键点:
- isRefreshing 互斥锁:第一个 401 触发刷新,后续 401 只入队
- 排队重放:刷新成功后遍历队列,用新 Token 重发原请求
- 彻底失败兜底:refresh token 也过期时,清空凭证踢回登录页
- 用原生 uni.request 刷新:避免刷新接口自身 401 造成无限递归
四、并发控制与取消请求
取消请求
uni.request 返回 RequestTask,可以 abort:
// 页面搜索场景:新请求发出前取消上一个
export function requestWithAbort(options) {
if (requestWithAbort._task) {
requestWithAbort._task.abort()
}
const taskHolder = {}
const p = new Promise((resolve, reject) => {
taskHolder.task = uni.request({
...options,
success: resolve,
fail: reject
})
})
requestWithAbort._task = taskHolder.task
return p
}搜索防抖 + 取消请求组合,才能彻底解决"旧结果覆盖新结果"的竞态问题。
并发限制
小程序对同时进行的 wx.request 有 10 个上限(微信端),批量任务要做池化控制:
async function requestPool(tasks, limit = 8) {
const results = []
const executing = []
for (const task of tasks) {
const p = task().then(r => ({ status: 'ok', data: r }))
.catch(e => ({ status: 'fail', error: e }))
results.push(p)
if (limit <= tasks.length) {
executing.push(p)
p.finally(() => executing.splice(executing.indexOf(p), 1))
if (executing.length >= limit) {
await Promise.race(executing)
}
}
}
return Promise.all(results)
}
// 批量拉取 50 个商品详情,并发 8
const details = await requestPool(
goodsIds.map(id => () => request.get(`/goods/${id}`)),
8
)五、上传与下载
文件上传
function upload(filePath, formData = {}) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: BASE_URL + '/upload',
filePath,
name: 'file',
formData, // 额外表单字段
header: { Authorization: `Bearer ${getToken()}` },
success: (res) => {
// 注意:uploadFile 返回的是字符串,要手动 parse
const body = JSON.parse(res.data)
body.code === 0 ? resolve(body.data) : reject(body)
},
fail: reject
})
})
}uploadFile 响应体是字符串,不 像 request 自动按 content-type 解析,JSON.parse 是必经步骤,这是极高频的坑。
分片上传大文件
uni-app 端实现分片需要先读文件(uni.getFileSystemManager,仅小程序端可用),将 Blob 切片后逐片上传,最后通知后端合并:
// 伪代码框架
const chunkSize = 1024 * 512 // 512KB
const totalChunks = Math.ceil(fileSize / chunkSize)
for (let i = 0; i < totalChunks; i++) {
await request.post('/upload/chunk', {
uploadId,
index: i,
total: totalChunks,
chunk: chunkData
})
}
// 全部上传完成后合并
await request.post('/upload/merge', { uploadId, filename })六、环境切换与接口配置
// config/env.js
let baseUrl = ''
// #ifdef MP-WEIXIN
baseUrl = 'https://mp-api.example.com' // 小程序正式环境
// #endif
// #ifdef H5
// H5 开发环境走代理
baseUrl = process.env.NODE_ENV === 'development'
? '/api'
: 'https://h5-api.example.com'
// #endif
// #ifdef APP-PLUS
baseUrl = 'https://app-api.example.com'
// #endif
export { baseUrl }利用条件编译按平台切换域名,比运行时 process.env 判断更干净。
总结
- 三层错误模型:网络层 fail、HTTP 层 statusCode、业务层 code 分开处理
- Token 无感刷新 = 互斥锁 + 等待队列 + 重放,缺一不可
- 竞态问题 = 防抖 + abort 双保险
- uploadFile 响应是字符串,必须手动 JSON.parse
- 微信端并发请求上限 10,批量任务用请求池
一个健壮的请求层是项目的地基,值得花一个下午把它写好。下一篇讲条件编译与多端适配——uni-app "一次开发多端运行"的光鲜与代价。
评论 (0)