Skip to content

网络请求

掌握小程序网络请求的核心技术,包括HTTP请求、WebSocket连接、文件上传下载等功能的实现。

🌐 HTTP 请求

wx.request 基础用法

javascript
// GET 请求
wx.request({
  url: 'https://api.example.com/users',
  method: 'GET',
  data: {
    page: 1,
    limit: 20,
    keyword: '搜索关键词'
  },
  header: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + wx.getStorageSync('token')
  },
  success: (res) => {
    console.log('请求成功', res)
    if (res.statusCode === 200) {
      this.setData({
        userList: res.data.list,
        total: res.data.total
      })
    } else {
      wx.showToast({
        title: '请求失败',
        icon: 'none'
      })
    }
  },
  fail: (err) => {
    console.error('请求失败', err)
    wx.showToast({
      title: '网络错误',
      icon: 'none'
    })
  },
  complete: () => {
    wx.hideLoading()
  }
})

// POST 请求
wx.request({
  url: 'https://api.example.com/users',
  method: 'POST',
  data: {
    name: '张三',
    email: 'zhangsan@example.com',
    phone: '13800138000'
  },
  header: {
    'Content-Type': 'application/json'
  },
  success: (res) => {
    if (res.statusCode === 201) {
      wx.showToast({
        title: '创建成功',
        icon: 'success'
      })
      // 刷新列表
      this.loadUserList()
    }
  }
})

// PUT 请求(更新)
wx.request({
  url: `https://api.example.com/users/${userId}`,
  method: 'PUT',
  data: {
    name: '李四',
    email: 'lisi@example.com'
  },
  header: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + wx.getStorageSync('token')
  },
  success: (res) => {
    if (res.statusCode === 200) {
      wx.showToast({
        title: '更新成功',
        icon: 'success'
      })
    }
  }
})

// DELETE 请求
wx.request({
  url: `https://api.example.com/users/${userId}`,
  method: 'DELETE',
  header: {
    'Authorization': 'Bearer ' + wx.getStorageSync('token')
  },
  success: (res) => {
    if (res.statusCode === 204) {
      wx.showToast({
        title: '删除成功',
        icon: 'success'
      })
      // 从列表中移除
      this.removeUserFromList(userId)
    }
  }
})

请求封装类

javascript
// utils/http.js
class HttpClient {
  constructor(options = {}) {
    this.baseURL = options.baseURL || ''
    this.timeout = options.timeout || 10000
    this.defaultHeaders = options.headers || {}
    
    // 请求和响应拦截器
    this.interceptors = {
      request: [],
      response: []
    }
    
    // 请求队列(用于取消请求)
    this.requestQueue = new Map()
  }

  // 添加请求拦截器
  addRequestInterceptor(interceptor) {
    this.interceptors.request.push(interceptor)
  }

  // 添加响应拦截器
  addResponseInterceptor(interceptor) {
    this.interceptors.response.push(interceptor)
  }

  // 核心请求方法
  request(config) {
    return new Promise((resolve, reject) => {
      // 生成请求ID
      const requestId = this.generateRequestId()
      
      // 合并配置
      let finalConfig = {
        url: this.baseURL + config.url,
        method: config.method || 'GET',
        data: config.data || {},
        header: {
          ...this.defaultHeaders,
          ...config.header
        },
        timeout: config.timeout || this.timeout,
        dataType: config.dataType || 'json',
        responseType: config.responseType || 'text'
      }

      // 执行请求拦截器
      this.interceptors.request.forEach(interceptor => {
        finalConfig = interceptor(finalConfig) || finalConfig
      })

      // 显示加载提示
      if (config.loading !== false) {
        wx.showLoading({
          title: config.loadingText || '请求中...',
          mask: true
        })
      }

      // 发起请求
      const requestTask = wx.request({
        ...finalConfig,
        success: (response) => {
          // 执行响应拦截器
          let finalResponse = response
          this.interceptors.response.forEach(interceptor => {
            finalResponse = interceptor(finalResponse) || finalResponse
          })

          // 处理响应
          if (this.isSuccess(finalResponse)) {
            resolve(finalResponse.data)
          } else {
            reject(this.createError(finalResponse))
          }
        },
        fail: (error) => {
          console.error('请求失败', error)
          reject(this.createError(null, error))
        },
        complete: () => {
          // 从请求队列中移除
          this.requestQueue.delete(requestId)
          
          // 隐藏加载提示
          if (config.loading !== false) {
            wx.hideLoading()
          }
        }
      })

      // 添加到请求队列
      this.requestQueue.set(requestId, {
        task: requestTask,
        config: finalConfig
      })
    })
  }

  // 判断请求是否成功
  isSuccess(response) {
    return response.statusCode >= 200 && response.statusCode < 300
  }

  // 创建错误对象
  createError(response, error) {
    if (error) {
      return {
        type: 'network',
        message: '网络请求失败',
        error
      }
    }
    
    return {
      type: 'http',
      message: `请求失败 (${response.statusCode})`,
      statusCode: response.statusCode,
      data: response.data
    }
  }

  // 生成请求ID
  generateRequestId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2)
  }

  // GET 请求
  get(url, params, config = {}) {
    return this.request({
      url,
      method: 'GET',
      data: params,
      ...config
    })
  }

  // POST 请求
  post(url, data, config = {}) {
    return this.request({
      url,
      method: 'POST',
      data,
      ...config
    })
  }

  // PUT 请求
  put(url, data, config = {}) {
    return this.request({
      url,
      method: 'PUT',
      data,
      ...config
    })
  }

  // DELETE 请求
  delete(url, config = {}) {
    return this.request({
      url,
      method: 'DELETE',
      ...config
    })
  }

  // 取消所有请求
  cancelAllRequests() {
    this.requestQueue.forEach(({ task }) => {
      task.abort()
    })
    this.requestQueue.clear()
    wx.hideLoading()
  }

  // 取消特定请求
  cancelRequest(requestId) {
    const request = this.requestQueue.get(requestId)
    if (request) {
      request.task.abort()
      this.requestQueue.delete(requestId)
    }
  }
}

// 创建默认实例
const http = new HttpClient({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 添加请求拦截器
http.addRequestInterceptor((config) => {
  // 添加认证token
  const token = wx.getStorageSync('token')
  if (token) {
    config.header.Authorization = `Bearer ${token}`
  }
  
  // 添加时间戳防止缓存
  if (config.method === 'GET') {
    config.data._t = Date.now()
  }
  
  console.log('发起请求', config)
  return config
})

// 添加响应拦截器
http.addResponseInterceptor((response) => {
  console.log('收到响应', response)
  
  // 统一错误处理
  if (response.data && response.data.code !== undefined) {
    if (response.data.code !== 0) {
      // 显示错误信息
      wx.showToast({
        title: response.data.message || '请求失败',
        icon: 'none'
      })
      
      // token过期处理
      if (response.data.code === 401) {
        wx.removeStorageSync('token')
        wx.redirectTo({
          url: '/pages/login/login'
        })
      }
    }
  }
  
  return response
})

module.exports = http

API 接口管理

javascript
// api/index.js
const http = require('../utils/http')

// 用户相关API
const userAPI = {
  // 用户登录
  login(data) {
    return http.post('/auth/login', data)
  },

  // 用户注册
  register(data) {
    return http.post('/auth/register', data)
  },

  // 获取用户信息
  getUserInfo(userId) {
    return http.get(`/users/${userId}`)
  },

  // 更新用户信息
  updateUserInfo(userId, data) {
    return http.put(`/users/${userId}`, data)
  },

  // 获取用户列表
  getUserList(params) {
    return http.get('/users', params)
  },

  // 删除用户
  deleteUser(userId) {
    return http.delete(`/users/${userId}`)
  },

  // 修改密码
  changePassword(data) {
    return http.post('/auth/change-password', data)
  },

  // 忘记密码
  forgotPassword(email) {
    return http.post('/auth/forgot-password', { email })
  }
}

// 文章相关API
const articleAPI = {
  // 获取文章列表
  getArticleList(params) {
    return http.get('/articles', params)
  },

  // 获取文章详情
  getArticleDetail(articleId) {
    return http.get(`/articles/${articleId}`)
  },

  // 创建文章
  createArticle(data) {
    return http.post('/articles', data)
  },

  // 更新文章
  updateArticle(articleId, data) {
    return http.put(`/articles/${articleId}`, data)
  },

  // 删除文章
  deleteArticle(articleId) {
    return http.delete(`/articles/${articleId}`)
  },

  // 点赞文章
  likeArticle(articleId) {
    return http.post(`/articles/${articleId}/like`)
  },

  // 收藏文章
  favoriteArticle(articleId) {
    return http.post(`/articles/${articleId}/favorite`)
  }
}

// 评论相关API
const commentAPI = {
  // 获取评论列表
  getCommentList(articleId, params) {
    return http.get(`/articles/${articleId}/comments`, params)
  },

  // 添加评论
  addComment(articleId, data) {
    return http.post(`/articles/${articleId}/comments`, data)
  },

  // 删除评论
  deleteComment(commentId) {
    return http.delete(`/comments/${commentId}`)
  },

  // 回复评论
  replyComment(commentId, data) {
    return http.post(`/comments/${commentId}/reply`, data)
  }
}

module.exports = {
  userAPI,
  articleAPI,
  commentAPI
}

📡 WebSocket 连接

WebSocket 基础用法

javascript
// utils/websocket.js
class WebSocketManager {
  constructor(url, options = {}) {
    this.url = url
    this.options = {
      reconnectInterval: 5000, // 重连间隔
      maxReconnectAttempts: 5, // 最大重连次数
      heartbeatInterval: 30000, // 心跳间隔
      ...options
    }
    
    this.socket = null
    this.reconnectAttempts = 0
    this.heartbeatTimer = null
    this.isConnected = false
    this.messageQueue = [] // 消息队列
    
    // 事件监听器
    this.listeners = {
      open: [],
      message: [],
      error: [],
      close: []
    }
  }

  // 连接WebSocket
  connect() {
    if (this.socket) {
      this.close()
    }

    console.log('连接WebSocket', this.url)
    
    this.socket = wx.connectSocket({
      url: this.url,
      protocols: this.options.protocols
    })

    // 监听连接打开
    this.socket.onOpen(() => {
      console.log('WebSocket连接已打开')
      this.isConnected = true
      this.reconnectAttempts = 0
      
      // 发送队列中的消息
      this.flushMessageQueue()
      
      // 开始心跳
      this.startHeartbeat()
      
      // 触发open事件
      this.emit('open')
    })

    // 监听消息
    this.socket.onMessage((res) => {
      console.log('收到WebSocket消息', res.data)
      
      try {
        const data = JSON.parse(res.data)
        this.handleMessage(data)
      } catch (error) {
        console.error('解析WebSocket消息失败', error)
      }
    })

    // 监听错误
    this.socket.onError((error) => {
      console.error('WebSocket错误', error)
      this.isConnected = false
      this.emit('error', error)
    })

    // 监听连接关闭
    this.socket.onClose((res) => {
      console.log('WebSocket连接已关闭', res)
      this.isConnected = false
      this.stopHeartbeat()
      
      this.emit('close', res)
      
      // 自动重连
      if (this.reconnectAttempts < this.options.maxReconnectAttempts) {
        this.reconnect()
      }
    })
  }

  // 发送消息
  send(data) {
    const message = typeof data === 'string' ? data : JSON.stringify(data)
    
    if (this.isConnected) {
      this.socket.send({
        data: message,
        success: () => {
          console.log('消息发送成功', message)
        },
        fail: (error) => {
          console.error('消息发送失败', error)
          // 将消息加入队列,等待重连后发送
          this.messageQueue.push(message)
        }
      })
    } else {
      // 连接未建立,将消息加入队列
      this.messageQueue.push(message)
      console.log('连接未建立,消息已加入队列')
    }
  }

  // 关闭连接
  close() {
    if (this.socket) {
      this.socket.close()
      this.socket = null
    }
    this.isConnected = false
    this.stopHeartbeat()
  }

  // 重连
  reconnect() {
    this.reconnectAttempts++
    console.log(`尝试重连 (${this.reconnectAttempts}/${this.options.maxReconnectAttempts})`)
    
    setTimeout(() => {
      this.connect()
    }, this.options.reconnectInterval)
  }

  // 开始心跳
  startHeartbeat() {
    if (this.options.heartbeatInterval > 0) {
      this.heartbeatTimer = setInterval(() => {
        this.send({
          type: 'heartbeat',
          timestamp: Date.now()
        })
      }, this.options.heartbeatInterval)
    }
  }

  // 停止心跳
  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer)
      this.heartbeatTimer = null
    }
  }

  // 发送队列中的消息
  flushMessageQueue() {
    while (this.messageQueue.length > 0) {
      const message = this.messageQueue.shift()
      this.send(message)
    }
  }

  // 处理收到的消息
  handleMessage(data) {
    switch (data.type) {
      case 'heartbeat':
        // 心跳响应
        console.log('收到心跳响应')
        break
      case 'notification':
        // 通知消息
        this.handleNotification(data)
        break
      case 'chat':
        // 聊天消息
        this.handleChatMessage(data)
        break
      default:
        // 其他消息
        this.emit('message', data)
    }
  }

  // 处理通知消息
  handleNotification(data) {
    wx.showToast({
      title: data.message,
      icon: 'none'
    })
    
    this.emit('notification', data)
  }

  // 处理聊天消息
  handleChatMessage(data) {
    this.emit('chat', data)
  }

  // 添加事件监听器
  on(event, callback) {
    if (this.listeners[event]) {
      this.listeners[event].push(callback)
    }
  }

  // 移除事件监听器
  off(event, callback) {
    if (this.listeners[event]) {
      const index = this.listeners[event].indexOf(callback)
      if (index > -1) {
        this.listeners[event].splice(index, 1)
      }
    }
  }

  // 触发事件
  emit(event, data) {
    if (this.listeners[event]) {
      this.listeners[event].forEach(callback => {
        try {
          callback(data)
        } catch (error) {
          console.error('事件回调执行失败', error)
        }
      })
    }
  }

  // 获取连接状态
  getReadyState() {
    return this.socket ? this.socket.readyState : -1
  }

  // 是否已连接
  isConnected() {
    return this.isConnected
  }
}

module.exports = WebSocketManager

WebSocket 使用示例

javascript
// pages/chat/chat.js
const WebSocketManager = require('../../utils/websocket')

Page({
  data: {
    messages: [],
    inputText: '',
    connected: false
  },

  onLoad() {
    this.initWebSocket()
  },

  onUnload() {
    if (this.ws) {
      this.ws.close()
    }
  },

  // 初始化WebSocket
  initWebSocket() {
    const token = wx.getStorageSync('token')
    const wsUrl = `wss://api.example.com/ws?token=${token}`
    
    this.ws = new WebSocketManager(wsUrl, {
      reconnectInterval: 3000,
      maxReconnectAttempts: 5,
      heartbeatInterval: 30000
    })

    // 监听连接打开
    this.ws.on('open', () => {
      console.log('WebSocket连接成功')
      this.setData({ connected: true })
      
      wx.showToast({
        title: '连接成功',
        icon: 'success'
      })
    })

    // 监听聊天消息
    this.ws.on('chat', (data) => {
      console.log('收到聊天消息', data)
      this.addMessage(data)
    })

    // 监听通知消息
    this.ws.on('notification', (data) => {
      console.log('收到通知', data)
      // 可以在这里处理系统通知
    })

    // 监听连接关闭
    this.ws.on('close', () => {
      console.log('WebSocket连接关闭')
      this.setData({ connected: false })
    })

    // 监听错误
    this.ws.on('error', (error) => {
      console.error('WebSocket错误', error)
      wx.showToast({
        title: '连接错误',
        icon: 'none'
      })
    })

    // 开始连接
    this.ws.connect()
  },

  // 发送消息
  sendMessage() {
    const text = this.data.inputText.trim()
    if (!text) return

    const message = {
      type: 'chat',
      content: text,
      timestamp: Date.now(),
      sender: wx.getStorageSync('userInfo').id
    }

    this.ws.send(message)
    
    // 添加到本地消息列表
    this.addMessage({
      ...message,
      isSelf: true
    })

    // 清空输入框
    this.setData({ inputText: '' })
  },

  // 添加消息到列表
  addMessage(message) {
    const messages = [...this.data.messages, message]
    this.setData({ messages })
    
    // 滚动到底部
    this.scrollToBottom()
  },

  // 滚动到底部
  scrollToBottom() {
    wx.createSelectorQuery()
      .select('#message-list')
      .boundingClientRect((rect) => {
        if (rect) {
          wx.pageScrollTo({
            scrollTop: rect.bottom
          })
        }
      })
      .exec()
  },

  // 输入框内容变化
  onInputChange(e) {
    this.setData({
      inputText: e.detail.value
    })
  },

  // 重新连接
  reconnect() {
    if (this.ws) {
      this.ws.connect()
    }
  }
})

📁 文件上传下载

文件上传

javascript
// utils/upload.js
class UploadManager {
  constructor(options = {}) {
    this.baseURL = options.baseURL || ''
    this.defaultHeaders = options.headers || {}
    this.maxConcurrent = options.maxConcurrent || 3 // 最大并发数
    this.chunkSize = options.chunkSize || 1024 * 1024 // 分片大小 1MB
    
    this.uploadQueue = []
    this.activeUploads = new Map()
  }

  // 上传单个文件
  uploadFile(filePath, options = {}) {
    return new Promise((resolve, reject) => {
      const {
        url = '/upload',
        name = 'file',
        formData = {},
        header = {},
        onProgress = null
      } = options

      const uploadTask = wx.uploadFile({
        url: this.baseURL + url,
        filePath: filePath,
        name: name,
        formData: formData,
        header: {
          ...this.defaultHeaders,
          ...header
        },
        success: (res) => {
          console.log('文件上传成功', res)
          
          try {
            const data = JSON.parse(res.data)
            resolve(data)
          } catch (error) {
            resolve(res.data)
          }
        },
        fail: (error) => {
          console.error('文件上传失败', error)
          reject(error)
        }
      })

      // 监听上传进度
      if (onProgress) {
        uploadTask.onProgressUpdate((res) => {
          onProgress({
            progress: res.progress,
            totalBytesSent: res.totalBytesSent,
            totalBytesExpectedToSend: res.totalBytesExpectedToSend
          })
        })
      }
    })
  }

  // 批量上传文件
  async uploadFiles(filePaths, options = {}) {
    const results = []
    const errors = []

    // 显示总体进度
    wx.showLoading({
      title: `上传中 0/${filePaths.length}`,
      mask: true
    })

    for (let i = 0; i < filePaths.length; i++) {
      try {
        const result = await this.uploadFile(filePaths[i], {
          ...options,
          onProgress: (progress) => {
            // 更新进度显示
            wx.showLoading({
              title: `上传中 ${i + 1}/${filePaths.length} (${progress.progress}%)`,
              mask: true
            })
            
            if (options.onProgress) {
              options.onProgress({
                fileIndex: i,
                fileProgress: progress,
                totalProgress: ((i + progress.progress / 100) / filePaths.length * 100).toFixed(1)
              })
            }
          }
        })
        
        results.push(result)
      } catch (error) {
        errors.push({ index: i, error })
      }
    }

    wx.hideLoading()

    return {
      results,
      errors,
      success: errors.length === 0
    }
  }

  // 选择并上传图片
  async chooseAndUploadImages(options = {}) {
    const {
      count = 9,
      sizeType = ['original', 'compressed'],
      sourceType = ['album', 'camera'],
      uploadOptions = {}
    } = options

    try {
      // 选择图片
      const chooseResult = await new Promise((resolve, reject) => {
        wx.chooseImage({
          count,
          sizeType,
          sourceType,
          success: resolve,
          fail: reject
        })
      })

      // 上传图片
      const uploadResult = await this.uploadFiles(
        chooseResult.tempFilePaths,
        uploadOptions
      )

      return {
        chooseResult,
        uploadResult
      }
    } catch (error) {
      console.error('选择或上传图片失败', error)
      throw error
    }
  }

  // 大文件分片上传
  async uploadLargeFile(filePath, options = {}) {
    const {
      url = '/upload/chunk',
      chunkSize = this.chunkSize,
      onProgress = null
    } = options

    try {
      // 获取文件信息
      const fileInfo = await this.getFileInfo(filePath)
      const totalChunks = Math.ceil(fileInfo.size / chunkSize)
      const fileId = this.generateFileId()

      console.log(`开始分片上传,文件大小: ${fileInfo.size}, 分片数: ${totalChunks}`)

      // 上传各个分片
      for (let i = 0; i < totalChunks; i++) {
        const start = i * chunkSize
        const end = Math.min(start + chunkSize, fileInfo.size)
        
        await this.uploadChunk(filePath, {
          fileId,
          chunkIndex: i,
          totalChunks,
          start,
          end,
          url
        })

        // 更新进度
        if (onProgress) {
          onProgress({
            progress: ((i + 1) / totalChunks * 100).toFixed(1),
            chunkIndex: i,
            totalChunks
          })
        }
      }

      // 合并分片
      const result = await this.mergeChunks(fileId, totalChunks)
      console.log('分片上传完成', result)
      
      return result
    } catch (error) {
      console.error('分片上传失败', error)
      throw error
    }
  }

  // 上传单个分片
  uploadChunk(filePath, options) {
    const {
      fileId,
      chunkIndex,
      totalChunks,
      start,
      end,
      url
    } = options

    return new Promise((resolve, reject) => {
      wx.uploadFile({
        url: this.baseURL + url,
        filePath: filePath,
        name: 'chunk',
        formData: {
          fileId,
          chunkIndex,
          totalChunks,
          start,
          end
        },
        success: (res) => {
          try {
            const data = JSON.parse(res.data)
            resolve(data)
          } catch (error) {
            resolve(res.data)
          }
        },
        fail: reject
      })
    })
  }

  // 合并分片
  mergeChunks(fileId, totalChunks) {
    return new Promise((resolve, reject) => {
      wx.request({
        url: this.baseURL + '/upload/merge',
        method: 'POST',
        data: {
          fileId,
          totalChunks
        },
        success: (res) => {
          if (res.statusCode === 200) {
            resolve(res.data)
          } else {
            reject(res)
          }
        },
        fail: reject
      })
    })
  }

  // 获取文件信息
  getFileInfo(filePath) {
    return new Promise((resolve, reject) => {
      wx.getFileInfo({
        filePath,
        success: resolve,
        fail: reject
      })
    })
  }

  // 生成文件ID
  generateFileId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2)
  }
}

module.exports = new UploadManager({
  baseURL: 'https://api.example.com',
  headers: {
    'Authorization': 'Bearer ' + wx.getStorageSync('token')
  }
})

文件下载

javascript
// utils/download.js
class DownloadManager {
  constructor() {
    this.downloadQueue = []
    this.activeDownloads = new Map()
    this.maxConcurrent = 3
  }

  // 下载单个文件
  downloadFile(url, options = {}) {
    return new Promise((resolve, reject) => {
      const {
        filePath = null,
        header = {},
        onProgress = null
      } = options

      const downloadTask = wx.downloadFile({
        url: url,
        filePath: filePath,
        header: header,
        success: (res) => {
          console.log('文件下载成功', res)
          
          if (res.statusCode === 200) {
            resolve({
              tempFilePath: res.tempFilePath,
              filePath: res.filePath,
              statusCode: res.statusCode
            })
          } else {
            reject({
              statusCode: res.statusCode,
              message: '下载失败'
            })
          }
        },
        fail: (error) => {
          console.error('文件下载失败', error)
          reject(error)
        }
      })

      // 监听下载进度
      if (onProgress) {
        downloadTask.onProgressUpdate((res) => {
          onProgress({
            progress: res.progress,
            totalBytesWritten: res.totalBytesWritten,
            totalBytesExpectedToWrite: res.totalBytesExpectedToWrite
          })
        })
      }
    })
  }

  // 批量下载文件
  async downloadFiles(urls, options = {}) {
    const results = []
    const errors = []

    wx.showLoading({
      title: `下载中 0/${urls.length}`,
      mask: true
    })

    for (let i = 0; i < urls.length; i++) {
      try {
        const result = await this.downloadFile(urls[i], {
          ...options,
          onProgress: (progress) => {
            wx.showLoading({
              title: `下载中 ${i + 1}/${urls.length} (${progress.progress}%)`,
              mask: true
            })
            
            if (options.onProgress) {
              options.onProgress({
                fileIndex: i,
                fileProgress: progress,
                totalProgress: ((i + progress.progress / 100) / urls.length * 100).toFixed(1)
              })
            }
          }
        })
        
        results.push(result)
      } catch (error) {
        errors.push({ index: i, error })
      }
    }

    wx.hideLoading()

    return {
      results,
      errors,
      success: errors.length === 0
    }
  }

  // 下载并保存到相册
  async downloadAndSaveToAlbum(url) {
    try {
      // 下载文件
      const downloadResult = await this.downloadFile(url)
      
      // 保存到相册
      await new Promise((resolve, reject) => {
        wx.saveImageToPhotosAlbum({
          filePath: downloadResult.tempFilePath,
          success: resolve,
          fail: reject
        })
      })

      wx.showToast({
        title: '保存成功',
        icon: 'success'
      })

      return downloadResult
    } catch (error) {
      console.error('下载并保存失败', error)
      
      if (error.errMsg && error.errMsg.includes('auth deny')) {
        wx.showModal({
          title: '保存失败',
          content: '需要获取保存到相册的权限',
          confirmText: '去设置',
          success: (res) => {
            if (res.confirm) {
              wx.openSetting()
            }
          }
        })
      } else {
        wx.showToast({
          title: '保存失败',
          icon: 'none'
        })
      }
      
      throw error
    }
  }
}

module.exports = new DownloadManager()

🔄 请求重试和错误处理

自动重试机制

javascript
// utils/retry.js
class RetryManager {
  constructor(options = {}) {
    this.maxRetries = options.maxRetries || 3
    this.retryDelay = options.retryDelay || 1000
    this.retryDelayMultiplier = options.retryDelayMultiplier || 2
    this.retryCondition = options.retryCondition || this.defaultRetryCondition
  }

  // 默认重试条件
  defaultRetryCondition(error) {
    // 网络错误或5xx服务器错误时重试
    return error.type === 'network' || 
           (error.statusCode >= 500 && error.statusCode < 600)
  }

  // 执行带重试的请求
  async executeWithRetry(requestFn, options = {}) {
    const {
      maxRetries = this.maxRetries,
      retryDelay = this.retryDelay,
      retryDelayMultiplier = this.retryDelayMultiplier,
      retryCondition = this.retryCondition,
      onRetry = null
    } = options

    let lastError = null
    let currentDelay = retryDelay

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        const result = await requestFn()
        return result
      } catch (error) {
        lastError = error
        
        // 检查是否应该重试
        if (attempt < maxRetries && retryCondition(error)) {
          console.log(`请求失败,${currentDelay}ms后进行第${attempt + 1}次重试`, error)
          
          // 触发重试回调
          if (onRetry) {
            onRetry(error, attempt + 1, maxRetries)
          }
          
          // 等待后重试
          await this.delay(currentDelay)
          currentDelay *= retryDelayMultiplier
        } else {
          // 不再重试,抛出最后的错误
          throw lastError
        }
      }
    }
  }

  // 延迟函数
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
  }
}

// 使用示例
const http = require('./http')
const retryManager = new RetryManager({
  maxRetries: 3,
  retryDelay: 1000,
  retryDelayMultiplier: 2
})

// 带重试的API调用
async function fetchUserDataWithRetry(userId) {
  return retryManager.executeWithRetry(
    () => http.get(`/users/${userId}`),
    {
      onRetry: (error, attempt, maxRetries) => {
        wx.showToast({
          title: `重试中 ${attempt}/${maxRetries}`,
          icon: 'loading'
        })
      }
    }
  )
}

错误处理策略

javascript
// utils/errorHandler.js
class ErrorHandler {
  constructor() {
    this.errorHandlers = new Map()
    this.globalErrorHandler = null
    
    // 注册默认错误处理器
    this.registerDefaultHandlers()
  }

  // 注册错误处理器
  register(errorType, handler) {
    this.errorHandlers.set(errorType, handler)
  }

  // 设置全局错误处理器
  setGlobalHandler(handler) {
    this.globalErrorHandler = handler
  }

  // 处理错误
  handle(error, context = {}) {
    console.error('处理错误', error, context)
    
    // 获取错误类型
    const errorType = this.getErrorType(error)
    
    // 查找对应的错误处理器
    const handler = this.errorHandlers.get(errorType)
    
    if (handler) {
      return handler(error, context)
    } else if (this.globalErrorHandler) {
      return this.globalErrorHandler(error, context)
    } else {
      // 默认错误处理
      this.defaultErrorHandler(error, context)
    }
  }

  // 获取错误类型
  getErrorType(error) {
    if (error.type) {
      return error.type
    }
    
    if (error.statusCode) {
      if (error.statusCode >= 400 && error.statusCode < 500) {
        return 'client_error'
      } else if (error.statusCode >= 500) {
        return 'server_error'
      }
    }
    
    if (error.errMsg) {
      if (error.errMsg.includes('timeout')) {
        return 'timeout'
      } else if (error.errMsg.includes('network')) {
        return 'network'
      }
    }
    
    return 'unknown'
  }

  // 注册默认错误处理器
  registerDefaultHandlers() {
    // 网络错误
    this.register('network', (error, context) => {
      wx.showToast({
        title: '网络连接失败',
        icon: 'none'
      })
    })

    // 超时错误
    this.register('timeout', (error, context) => {
      wx.showToast({
        title: '请求超时',
        icon: 'none'
      })
    })

    // 401 未授权
    this.register('unauthorized', (error, context) => {
      wx.showModal({
        title: '登录过期',
        content: '请重新登录',
        showCancel: false,
        success: () => {
          wx.removeStorageSync('token')
          wx.redirectTo({
            url: '/pages/login/login'
          })
        }
      })
    })

    // 403 禁止访问
    this.register('forbidden', (error, context) => {
      wx.showToast({
        title: '没有访问权限',
        icon: 'none'
      })
    })

    // 404 未找到
    this.register('not_found', (error, context) => {
      wx.showToast({
        title: '请求的资源不存在',
        icon: 'none'
      })
    })

    // 服务器错误
    this.register('server_error', (error, context) => {
      wx.showToast({
        title: '服务器错误,请稍后重试',
        icon: 'none'
      })
    })

    // 客户端错误
    this.register('client_error', (error, context) => {
      const message = error.data?.message || '请求参数错误'
      wx.showToast({
        title: message,
        icon: 'none'
      })
    })
  }

  // 默认错误处理器
  defaultErrorHandler(error, context) {
    wx.showToast({
      title: '操作失败,请重试',
      icon: 'none'
    })
  }
}

module.exports = new ErrorHandler()

📊 请求监控和统计

请求统计

javascript
// utils/requestMonitor.js
class RequestMonitor {
  constructor() {
    this.stats = {
      totalRequests: 0,
      successRequests: 0,
      failedRequests: 0,
      totalResponseTime: 0,
      averageResponseTime: 0
    }
    
    this.requestHistory = []
    this.maxHistorySize = 100
    
    this.loadStats()
  }

  // 记录请求开始
  recordRequestStart(config) {
    const requestId = this.generateRequestId()
    const startTime = Date.now()
    
    this.requestHistory.push({
      id: requestId,
      url: config.url,
      method: config.method,
      startTime,
      status: 'pending'
    })
    
    return requestId
  }

  // 记录请求成功
  recordRequestSuccess(requestId, response) {
    const request = this.findRequest(requestId)
    if (request) {
      const endTime = Date.now()
      const responseTime = endTime - request.startTime
      
      request.status = 'success'
      request.endTime = endTime
      request.responseTime = responseTime
      request.statusCode = response.statusCode
      
      // 更新统计信息
      this.stats.totalRequests++
      this.stats.successRequests++
      this.stats.totalResponseTime += responseTime
      this.stats.averageResponseTime = this.stats.totalResponseTime / this.stats.totalRequests
      
      this.saveStats()
    }
  }

  // 记录请求失败
  recordRequestFailure(requestId, error) {
    const request = this.findRequest(requestId)
    if (request) {
      const endTime = Date.now()
      const responseTime = endTime - request.startTime
      
      request.status = 'failed'
      request.endTime = endTime
      request.responseTime = responseTime
      request.error = error
      
      // 更新统计信息
      this.stats.totalRequests++
      this.stats.failedRequests++
      this.stats.totalResponseTime += responseTime
      this.stats.averageResponseTime = this.stats.totalResponseTime / this.stats.totalRequests
      
      this.saveStats()
    }
  }

  // 查找请求记录
  findRequest(requestId) {
    return this.requestHistory.find(req => req.id === requestId)
  }

  // 生成请求ID
  generateRequestId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2)
  }

  // 获取统计信息
  getStats() {
    const successRate = this.stats.totalRequests > 0 ? 
      (this.stats.successRequests / this.stats.totalRequests * 100).toFixed(2) + '%' : '0%'
    
    return {
      ...this.stats,
      successRate
    }
  }

  // 获取请求历史
  getRequestHistory(limit = 50) {
    return this.requestHistory
      .slice(-limit)
      .reverse()
      .map(req => ({
        ...req,
        responseTimeFormatted: req.responseTime ? `${req.responseTime}ms` : '-'
      }))
  }

  // 分析慢请求
  getSlowRequests(threshold = 3000) {
    return this.requestHistory
      .filter(req => req.responseTime && req.responseTime > threshold)
      .sort((a, b) => b.responseTime - a.responseTime)
      .slice(0, 10)
  }

  // 分析错误请求
  getErrorRequests() {
    return this.requestHistory
      .filter(req => req.status === 'failed')
      .slice(-20)
      .reverse()
  }

  // 保存统计信息
  saveStats() {
    try {
      wx.setStorageSync('request_stats', this.stats)
      
      // 限制历史记录大小
      if (this.requestHistory.length > this.maxHistorySize) {
        this.requestHistory = this.requestHistory.slice(-this.maxHistorySize)
      }
      
      wx.setStorageSync('request_history', this.requestHistory)
    } catch (error) {
      console.error('保存请求统计失败', error)
    }
  }

  // 加载统计信息
  loadStats() {
    try {
      const savedStats = wx.getStorageSync('request_stats')
      if (savedStats) {
        this.stats = { ...this.stats, ...savedStats }
      }
      
      const savedHistory = wx.getStorageSync('request_history')
      if (savedHistory && Array.isArray(savedHistory)) {
        this.requestHistory = savedHistory
      }
    } catch (error) {
      console.error('加载请求统计失败', error)
    }
  }

  // 重置统计信息
  resetStats() {
    this.stats = {
      totalRequests: 0,
      successRequests: 0,
      failedRequests: 0,
      totalResponseTime: 0,
      averageResponseTime: 0
    }
    
    this.requestHistory = []
    
    wx.removeStorageSync('request_stats')
    wx.removeStorageSync('request_history')
  }

  // 生成报告
  generateReport() {
    const stats = this.getStats()
    const slowRequests = this.getSlowRequests()
    const errorRequests = this.getErrorRequests()
    
    return {
      summary: stats,
      slowRequests,
      errorRequests,
      generatedAt: new Date().toISOString()
    }
  }
}

module.exports = new RequestMonitor()

🔧 请求缓存

HTTP 缓存实现

javascript
// utils/httpCache.js
class HttpCache {
  constructor(options = {}) {
    this.storage = require('./storage')
    this.defaultTTL = options.defaultTTL || 300 // 默认缓存5分钟
    this.maxCacheSize = options.maxCacheSize || 50 // 最大缓存条目数
    this.cachePrefix = 'http_cache_'
  }

  // 生成缓存key
  generateCacheKey(config) {
    const { url, method = 'GET', data = {} } = config
    const key = `${method}:${url}:${JSON.stringify(data)}`
    return this.cachePrefix + this.hashCode(key)
  }

  // 简单hash函数
  hashCode(str) {
    let hash = 0
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i)
      hash = ((hash << 5) - hash) + char
      hash = hash & hash // 转换为32位整数
    }
    return Math.abs(hash).toString(36)
  }

  // 获取缓存
  get(config) {
    const cacheKey = this.generateCacheKey(config)
    const cached = this.storage.get(cacheKey)
    
    if (!cached) {
      return null
    }

    // 检查是否过期
    const now = Date.now()
    if (cached.expireTime && now > cached.expireTime) {
      this.storage.remove(cacheKey)
      return null
    }

    console.log('使用缓存数据', cacheKey)
    return cached.data
  }

  // 设置缓存
  set(config, data, ttl = this.defaultTTL) {
    const cacheKey = this.generateCacheKey(config)
    const expireTime = ttl > 0 ? Date.now() + ttl * 1000 : null
    
    const cacheData = {
      data,
      timestamp: Date.now(),
      expireTime,
      url: config.url,
      method: config.method
    }

    this.storage.set(cacheKey, cacheData)
    
    // 检查缓存大小
    this.checkCacheSize()
  }

  // 删除缓存
  delete(config) {
    const cacheKey = this.generateCacheKey(config)
    this.storage.remove(cacheKey)
  }

  // 清空所有缓存
  clear() {
    const keys = this.storage.keys()
    keys.forEach(key => {
      if (key.startsWith(this.cachePrefix)) {
        this.storage.remove(key.replace(this.cachePrefix, ''))
      }
    })
  }

  // 检查缓存大小
  checkCacheSize() {
    const keys = this.getCacheKeys()
    
    if (keys.length > this.maxCacheSize) {
      // 按时间戳排序,删除最旧的缓存
      const cacheItems = keys.map(key => {
        const data = this.storage.get(key.replace(this.cachePrefix, ''))
        return { key, timestamp: data?.timestamp || 0 }
      }).sort((a, b) => a.timestamp - b.timestamp)
      
      // 删除多余的缓存
      const toDelete = cacheItems.slice(0, keys.length - this.maxCacheSize)
      toDelete.forEach(item => {
        this.storage.remove(item.key.replace(this.cachePrefix, ''))
      })
      
      console.log(`清理了 ${toDelete.length} 个过期缓存`)
    }
  }

  // 获取所有缓存keys
  getCacheKeys() {
    const keys = this.storage.keys()
    return keys.filter(key => key.startsWith(this.cachePrefix))
  }

  // 清理过期缓存
  cleanExpired() {
    const keys = this.getCacheKeys()
    const now = Date.now()
    let cleanedCount = 0
    
    keys.forEach(key => {
      const data = this.storage.get(key.replace(this.cachePrefix, ''))
      if (data && data.expireTime && now > data.expireTime) {
        this.storage.remove(key.replace(this.cachePrefix, ''))
        cleanedCount++
      }
    })
    
    console.log(`清理了 ${cleanedCount} 个过期缓存`)
    return cleanedCount
  }

  // 获取缓存统计
  getStats() {
    const keys = this.getCacheKeys()
    const now = Date.now()
    let validCount = 0
    let expiredCount = 0
    let totalSize = 0
    
    keys.forEach(key => {
      const data = this.storage.get(key.replace(this.cachePrefix, ''))
      if (data) {
        if (data.expireTime && now > data.expireTime) {
          expiredCount++
        } else {
          validCount++
        }
        totalSize += JSON.stringify(data).length
      }
    })
    
    return {
      total: keys.length,
      valid: validCount,
      expired: expiredCount,
      size: totalSize,
      maxSize: this.maxCacheSize
    }
  }
}

module.exports = new HttpCache()

📚 相关文档


掌握网络请求的核心技术,构建高效稳定的小程序网络层!🚀

连接多端,赋能创新