微信小程序网络请求工具类request.js

所属分类:HtmlCssJs | 浏览:49 | 发布于 2026-06-09

一个好用的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 的接口,再重新发起原请求。这部分逻辑稍微复杂,如果您需要完整代码示例,可以告诉我,我会为您补充。

 

海涛博客(https://haitaoblog.com)属于海涛个人博客,欢迎浏览使用

联系方式:qq:52292959 邮箱:52292959@qq.com W26

备案号:粤ICP备18108585号 友情链接