一个好用的http请求类必须支持拦截器、Loading管理、Token自动注入、请求取消等常用功能。
1、核心代码,保存为utils/request.js
/**
* 微信小程序网络请求工具类
* 支持拦截器、Loading管理、Token自动注入、请求取消等功能
*
* @class Request
* @example
* const request = new Request({
* baseURL: 'https://api.example.com',
* timeout: 60000,
* showLoading: false
* })
*
* // 添加请求拦截器
* request.interceptors.request.use(config => {
* config.header['X-Custom'] = 'value'
* return config
* })
*
* // 添加响应拦截器
* request.interceptors.response.use(response => {
* if (response.data.code !== 0) throw new Error(response.data.message)
* return response.data.data
* })
*
* // 发起请求
* request.get('/user/info', { id: 1 }).then(res => console.log(res))
*/
/**
* 拦截器管理器
* 用于管理请求/响应拦截器函数队列
*/
class InterceptorManager {
constructor() {
this.handlers = []
}
/**
* 添加拦截器
* @param {Function} fulfilled 成功回调
* @param {Function} rejected 失败回调(可选)
* @returns {number} 拦截器ID,可用于 eject 移除
*/
use(fulfilled, rejected) {
this.handlers.push({
fulfilled,
rejected: rejected || null
})
return this.handlers.length - 1
}
/**
* 移除拦截器
* @param {number} id 拦截器ID
*/
eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null
}
}
/**
* 清空所有拦截器
*/
clear() {
this.handlers = []
}
/**
* 遍历执行拦截器
* @param {Object} params 初始参数
* @param {boolean} isRequest true:请求拦截器 false:响应拦截器
* @returns {Promise} 处理后的结果
*/
async run(params, isRequest = true) {
let result = params
for (const handler of this.handlers) {
if (!handler) continue
try {
if (handler.fulfilled) {
result = await handler.fulfilled(result)
}
} catch (error) {
if (handler.rejected) {
result = await handler.rejected(error)
} else {
throw error
}
}
}
return result
}
}
/**
* 网络请求主类
*/
class Request {
/**
* 构造函数
* @param {Object} config 全局配置
* @param {string} config.baseURL 基础URL
* @param {number} config.timeout 超时时间(ms),默认60000
* @param {Object} config.header 默认请求头
* @param {boolean} config.showLoading 是否显示Loading,默认false
* @param {string} config.loadingText Loading文案,默认'加载中...'
* @param {boolean} config.showError 是否自动显示错误提示,默认true
* @param {Function} config.getToken 获取Token的函数,需返回string
* @param {Function} config.onError 全局错误回调
* @param {Function} config.validateStatus 验证HTTP状态码是否成功,默认 status >= 200 && status < 300
*/
constructor(config = {}) {
// 全局配置
this.config = {
baseURL: config.baseURL || '',
timeout: config.timeout || 60000,
header: {
'Content-Type': 'application/json',
...(config.header || {})
},
showLoading: config.showLoading || false,
loadingText: config.loadingText || '加载中...',
showError: config.showError !== undefined ? config.showError : true,
getToken: config.getToken || null,
onError: config.onError || null,
validateStatus: config.validateStatus || (status => status >= 200 && status < 300)
}
// 拦截器实例
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
}
// Loading计数器(用于多个请求同时显示时只关闭一次)
this._loadingCount = 0
// 存储进行中的请求 { requestId: requestTask }
this._pendingRequests = new Map()
this._nextRequestId = 0
}
/**
* 更新全局配置
* @param {Object} config 配置项
*/
setConfig(config) {
Object.assign(this.config, config)
}
/**
* 显示Loading(支持计数)
* @param {string} text Loading文案
*/
_showLoading(text) {
if (!this.config.showLoading) return
if (this._loadingCount === 0) {
wx.showLoading({
title: text || this.config.loadingText,
mask: true
})
}
this._loadingCount++
}
/**
* 隐藏Loading(计数归零时真正关闭)
*/
_hideLoading() {
if (!this.config.showLoading) return
this._loadingCount--
if (this._loadingCount <= 0) {
this._loadingCount = 0
wx.hideLoading()
}
}
/**
* 显示错误提示
* @param {string} message 错误信息
* @param {Object} config 请求配置
*/
_showError(message, config) {
// 如果本次请求配置了不显示错误,则跳过
if (config && config.showError === false) return
if (this.config.showError) {
wx.showToast({
title: message || '请求失败',
icon: 'none',
duration: 2000
})
}
// 调用全局错误回调
if (this.config.onError && typeof this.config.onError === 'function') {
this.config.onError({ message, config })
}
}
/**
* 拼接URL参数
* @param {string} url 原始URL
* @param {Object} params 参数对象
* @returns {string}
*/
_buildUrl(url, params) {
if (!params || Object.keys(params).length === 0) return url
const queryString = Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&')
return url.includes('?') ? `${url}&${queryString}` : `${url}?${queryString}`
}
/**
* 获取完整URL
* @param {string} url 请求路径
* @returns {string}
*/
_getFullUrl(url) {
if (/^(https?:)?\/\//.test(url)) return url
return `${this.config.baseURL}${url.startsWith('/') ? url : '/' + url}`
}
/**
* 合并请求头
* @param {Object} header 自定义请求头
* @returns {Object}
*/
async _mergeHeader(customHeader = {}) {
const header = { ...this.config.header, ...customHeader }
// 自动注入Token
if (this.config.getToken && typeof this.config.getToken === 'function') {
try {
const token = await this.config.getToken()
if (token) {
header['Authorization'] = `Bearer ${token}`
}
} catch (e) {
console.warn('[Request] 获取Token失败:', e)
}
}
return header
}
/**
* 生成唯一请求ID
*/
_generateRequestId() {
return this._nextRequestId++
}
/**
* 存储请求任务
* @param {number} id 请求ID
* @param {Object} task requestTask
*/
_addPendingRequest(id, task) {
this._pendingRequests.set(id, task)
}
/**
* 移除请求任务
* @param {number} id 请求ID
*/
_removePendingRequest(id) {
this._pendingRequests.delete(id)
}
/**
* 取消指定请求
* @param {number} id 请求ID
*/
cancelRequest(id) {
const task = this._pendingRequests.get(id)
if (task && typeof task.abort === 'function') {
task.abort()
this._removePendingRequest(id)
}
}
/**
* 取消所有进行中的请求
*/
cancelAllRequests() {
this._pendingRequests.forEach((task, id) => {
if (task && typeof task.abort === 'function') {
task.abort()
}
})
this._pendingRequests.clear()
}
/**
* 核心请求方法
* @param {Object} options 请求配置
* @returns {Promise} 返回Promise,附加cancel方法
*/
async request(options) {
const requestId = this._generateRequestId()
let cancel = null
// 返回Promise并附加cancel方法
const promise = new Promise(async (resolve, reject) => {
try {
// 1. 合并配置
let requestConfig = {
url: options.url,
method: options.method || 'GET',
data: options.data || null,
params: options.params || null,
header: options.header || {},
timeout: options.timeout || this.config.timeout,
showLoading: options.showLoading !== undefined ? options.showLoading : this.config.showLoading,
loadingText: options.loadingText || this.config.loadingText,
showError: options.showError !== undefined ? options.showError : this.config.showError,
validateStatus: options.validateStatus || this.config.validateStatus
}
// 处理Loading显示(需要在请求拦截器之前显示,因为拦截器可能耗时)
if (requestConfig.showLoading) {
this._showLoading(requestConfig.loadingText)
}
// 2. 执行请求拦截器
requestConfig = await this.interceptors.request.run(requestConfig, true)
// 3. 构建完整URL
let fullUrl = this._getFullUrl(requestConfig.url)
// 处理params参数
if (requestConfig.params) {
fullUrl = this._buildUrl(fullUrl, requestConfig.params)
}
// 4. 合并请求头
const finalHeader = await this._mergeHeader(requestConfig.header)
// 5. 发起wx.request
const requestTask = wx.request({
url: fullUrl,
method: requestConfig.method,
data: requestConfig.data,
header: finalHeader,
timeout: requestConfig.timeout,
success: async (res) => {
// 移除pending记录
this._removePendingRequest(requestId)
// 判断HTTP状态码是否成功
const isSuccess = requestConfig.validateStatus(res.statusCode)
// 构造响应对象
const response = {
data: res.data,
statusCode: res.statusCode,
header: res.header,
config: requestConfig,
originalRes: res
}
try {
// 6. 执行响应拦截器
const finalResponse = await this.interceptors.response.run(response, false)
// 隐藏Loading
if (requestConfig.showLoading) this._hideLoading()
resolve(finalResponse)
} catch (interceptorError) {
// 响应拦截器抛出错误
if (requestConfig.showLoading) this._hideLoading()
if (requestConfig.showError !== false) {
const errMsg = interceptorError.message || interceptorError.errMsg || '请求处理失败'
this._showError(errMsg, requestConfig)
}
reject(interceptorError)
}
},
fail: async (err) => {
// 移除pending记录
this._removePendingRequest(requestId)
// 隐藏Loading
if (requestConfig.showLoading) this._hideLoading()
// 处理错误信息
let errorMsg = ''
if (err.errMsg === 'request:fail abort') {
errorMsg = '请求已取消'
} else if (err.errMsg.includes('timeout')) {
errorMsg = '请求超时'
} else if (err.errMsg.includes('fail')) {
errorMsg = '网络异常,请检查网络连接'
} else {
errorMsg = err.errMsg || '请求失败'
}
const error = { ...err, message: errorMsg, config: requestConfig }
// 显示错误提示
if (requestConfig.showError !== false) {
this._showError(errorMsg, requestConfig)
}
reject(error)
}
})
// 存储requestTask以便取消
this._addPendingRequest(requestId, requestTask)
// 定义cancel函数
cancel = () => {
this.cancelRequest(requestId)
const cancelError = new Error('Request canceled')
cancelError.type = 'cancel'
reject(cancelError)
}
} catch (error) {
// 请求拦截器执行失败
if (options.showLoading !== false) this._hideLoading()
if (options.showError !== false) {
this._showError(error.message || '请求配置错误', options)
}
reject(error)
}
})
// 为Promise附加cancel方法
promise.cancel = cancel
return promise
}
/**
* GET请求
* @param {string} url 请求地址
* @param {Object} params 查询参数
* @param {Object} config 其他配置
* @returns {Promise}
*/
get(url, params = null, config = {}) {
return this.request({
url,
method: 'GET',
params,
...config
})
}
/**
* POST请求
* @param {string} url 请求地址
* @param {Object} data 请求体数据
* @param {Object} config 其他配置
* @returns {Promise}
*/
post(url, data = null, config = {}) {
return this.request({
url,
method: 'POST',
data,
...config
})
}
/**
* PUT请求
* @param {string} url 请求地址
* @param {Object} data 请求体数据
* @param {Object} config 其他配置
* @returns {Promise}
*/
put(url, data = null, config = {}) {
return this.request({
url,
method: 'PUT',
data,
...config
})
}
/**
* DELETE请求
* @param {string} url 请求地址
* @param {Object} params 查询参数
* @param {Object} config 其他配置
* @returns {Promise}
*/
delete(url, params = null, config = {}) {
return this.request({
url,
method: 'DELETE',
params,
...config
})
}
/**
* 文件上传
* @param {Object} options 上传配置
* @param {string} options.url 上传地址
* @param {string} options.filePath 本地文件路径
* @param {string} options.name 文件对应的key
* @param {Object} options.formData 额外的表单数据
* @param {Object} options.header 请求头
* @param {boolean} options.showLoading 是否显示Loading
* @param {Function} options.onProgress 进度回调
* @returns {Promise}
*/
upload(options) {
return new Promise(async (resolve, reject) => {
const {
url,
filePath,
name = 'file',
formData = {},
header = {},
showLoading = this.config.showLoading,
loadingText = this.config.loadingText,
showError = this.config.showError,
onProgress = null
} = options
let cancelUpload = null
const requestId = this._generateRequestId()
try {
if (showLoading) this._showLoading(loadingText)
// 构建完整URL
const fullUrl = this._getFullUrl(url)
// 合并请求头(自动添加Token)
const finalHeader = await this._mergeHeader(header)
const uploadTask = wx.uploadFile({
url: fullUrl,
filePath,
name,
formData,
header: finalHeader,
success: async (res) => {
this._removePendingRequest(requestId)
if (showLoading) this._hideLoading()
// 尝试解析返回数据
let data = res.data
try {
data = JSON.parse(res.data)
} catch (e) {
// 非JSON格式保持不变
}
const response = {
data,
statusCode: res.statusCode,
header: res.header,
config: options
}
// 应用响应拦截器
try {
const finalResponse = await this.interceptors.response.run(response, false)
resolve(finalResponse)
} catch (interceptorError) {
if (showError !== false) {
this._showError(interceptorError.message || '上传处理失败', options)
}
reject(interceptorError)
}
},
fail: (err) => {
this._removePendingRequest(requestId)
if (showLoading) this._hideLoading()
let errorMsg = ''
if (err.errMsg === 'uploadFile:fail abort') {
errorMsg = '上传已取消'
} else {
errorMsg = err.errMsg || '上传失败'
}
if (showError !== false) {
this._showError(errorMsg, options)
}
reject({ ...err, message: errorMsg })
}
})
this._addPendingRequest(requestId, uploadTask)
cancelUpload = () => this.cancelRequest(requestId)
// 监听上传进度
if (onProgress && typeof onProgress === 'function') {
uploadTask.onProgressUpdate(onProgress)
}
} catch (error) {
if (showLoading) this._hideLoading()
if (showError !== false) {
this._showError(error.message || '上传配置错误', options)
}
reject(error)
}
// 返回的Promise附加cancel方法
const promiseWithCancel = Promise.resolve().then(() => {})
promiseWithCancel.cancel = cancelUpload
return promiseWithCancel
})
}
}
// 导出类(支持CommonJS和ES Module)
module.exports = Request
2、app.js中初始化
// app.js
const Request = require('./utils/request.js') // 引入工具类
// app.js
const Request = require('./utils/request2.js')
App({
// app实例属性,全局:getApp().request
request: null,
globalData: {
userInfo: null,
loginCallback: null
},
// 封装:创建axios/request实例的私有函数
createHttpInstance() {
// 这里所有配置、拦截器、请求头统一写在这里
const http = new Request({
baseURL: 'https://your-api-domain.com', // 替换成您的真实接口域名
timeout: 30000, // 超时时间(毫秒)
showLoading: true, // 是否自动显示 loading
loadingText: '加载中...', // loading 文案
showError: true, // 是否自动弹出错误提示
// 可选:获取 token 的函数(如果需要登录认证)
getToken: () => {
return wx.getStorageSync('token') || null // 从本地存储中读取 token
},
// 可选:全局错误处理函数
onError: (error) => {
console.error('请求出错:', error.message)
}
})
// 请求拦截器
http.interceptors.request.use(config => {
// 统一token、请求头
const token = wx.getStorageSync('token')
if (token) {
config.header.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器
http.interceptors.response.use(res => {
// 统一错误处理、状态码判断
return res.data
}, err => {
// 报错统一处理:弹窗、401跳登录
wx.showToast({ title: '接口异常', icon: 'none' })
return Promise.reject(err)
})
return http
},
onLaunch() {
// 一行调用,代码清爽
this.request = this.createHttpInstance()
}
})
3、其它页面使用
const app = getApp()
Page({
async onLoad() {
const request = app.request
try {
const posts = await request.get('/posts/1')
console.log('获取到的数据:', posts)
wx.showModal({ title: '成功', content: JSON.stringify(posts) })
} catch (err) {
console.error(err)
}
}
})
4、页面发起请求完整版
// pages/user/user.js
const app = getApp() // 获取小程序实例
Page({
data: {
userInfo: null,
loading: false
},
onLoad() {
this.getUserInfo()
},
async getUserInfo() {
// 通过 app.globalData.request 拿到我们之前创建的实例
const request = app.request
this.setData({ loading: true })
try {
// 发起 GET 请求,路径会自动拼上 baseURL
// 返回的结果是经过响应拦截器处理后的 data 数据(假设我们上一步做了)
const user = await request.get('/user/info', { userId: 123 })
this.setData({ userInfo: user, loading: false })
} catch (err) {
// 错误会自动被工具类弹窗提示(因为 showError: true)
// 如果想自己处理,可以在 catch 里做额外的事情
console.error('获取用户信息失败', err)
this.setData({ loading: false })
}
}
})
5、POST请求示例
// 提交登录表单
async handleLogin() {
const request = app.request
try {
const result = await request.post('/login', {
username: 'test',
password: '123456'
})
// 假设登录成功后返回 token
wx.setStorageSync('token', result.token)
wx.showToast({ title: '登录成功' })
} catch (err) {
// 错误提示已自动显示,这里可以不做额外处理
}
}
6、文件上传示例
// 选择图片并上传
async uploadAvatar() {
const request = app.globalData.request
wx.chooseImage({
success: async (res) => {
const tempFilePath = res.tempFilePaths[0]
try {
const result = await request.upload({
url: '/upload/avatar',
filePath: tempFilePath,
name: 'avatar',
formData: { userId: 123 },
onProgress: (progressEvent) => {
console.log('上传进度:', progressEvent.progress)
}
})
console.log('上传成功', result)
wx.showToast({ title: '上传成功' })
} catch (err) {
console.error('上传失败', err)
}
}
})
}
7、取消请求
有时候用户切换页面或点击取消按钮,我们希望中止正在进行的请求。
Page({
data: {
requestPromise: null // 保存请求返回的 Promise
},
async search() {
const request = app.request
// 发起请求,保存返回的 Promise(它带有一个 cancel 方法)
const promise = request.get('/search', { keyword: '小程序' })
this.setData({ requestPromise: promise })
try {
const result = await promise
console.log('搜索结果', result)
} catch (err) {
if (err.type === 'cancel') {
console.log('请求已取消')
} else {
console.error('请求失败', err)
}
}
},
// 点击取消按钮时调用
cancelSearch() {
if (this.data.requestPromise && this.data.requestPromise.cancel) {
this.data.requestPromise.cancel() // 取消请求
this.setData({ requestPromise: null })
}
}
})
8、常见问题解答
8.1、如果关闭某个请求的自动loading?
// 在请求参数中传入 showLoading: false
request.get('/list', null, { showLoading: false })
8.2、如何单独处理某个请求的错误,不让它自动弹窗?
javascript
request.get('/some/api', null, { showError: false })
.catch(err => {
// 自己处理错误,不会自动弹 toast
console.log('自己处理:', err)
})
8.3、token过期后怎么让请求自动刷新token?
可以在响应拦截器中判断特定错误码(比如 401),然后调用刷新 token 的接口,再重新发起原请求。这部分逻辑稍微复杂,如果您需要完整代码示例,可以告诉我,我会为您补充。