小程序请求封装wx.request

4,223 阅读7分钟

1 问题

在使用小程序小程序原生的请求方法wx.request时,发现以下几个问题,不利于统一管理

  • 1 不支持async/await,不太好处理请求
  • 2 请求的头每次都得重新写一遍,登录使用的token注入麻烦
  • 3 请求的异常状态码散乱,因为请求异常,后台都是传递code加上英文说明,而前端为了友好呈现,一般需要进行中文的映射

2 解决

async/await支持

之前都是手动引入第三方的库regenerator实现的,后面微信开发者工具增强编译中增加的自动引入async/await的支持,无需自己引入第三方库

  • 勾选增强编译

请求封装

  • 目录结构
  utils
    |---- wx-request.js 封装请求
    |---- api.js 所有请求的地址
    |---- expectionalError.js 请求异常映射
    |---- storageSyncTool.js 本地存储工具类

封装请求wx-request.js

这里主要做了一下几件事 1.引入请求异常的映射,方便出现已知的异常时,对异常信息统一翻译 2.引入本地存储封装 3.指定服务器请求根地址 4.在封装的请求中,接收传递传递过来的自定义请求参数,将自定义请求参数、默认参数进行合并,将token注入请求头中 5.发起请求时,校验token是否存在,如果存在,发起请求,如果不存在,直接拒绝请求 6.请求完成后,判断请求码code

  • 请求正常code &&code === 200(这里根据实际情况做判断),将数据返回
  • 请求失败
    • 进行异常处理中,使用异常状态码换区错误提示信息
    • 异常状态码是406,提示用户重新登录
import getErrorMessage from "./expectionalError"
import {
  getLoginToken,
  clearLoginToken
} from "./storageSyncTool.js.js"

// host地址
const host = "https://www.abc.com"

// 合并请求参数: 将默认参数和实际请求的参数进行合并
const mergeRequestParmas = (perDefaults, params) => {
  return Object.assign({}, perDefaults, params)
}

// 请求封装
const wxRequest = async (subUrl, params = {}) => {
  const token = getLoginToken()
  const defaults = {
    header: {
      "Content-Type": "application/json",
      token: token || ""
    },
    method: "GET",
    data: {}
  }

  // 合并参数
  const options = mergeRequestParmas(defaults, params)
  let res = await new Promise((resolve, reject) => {
    if (!token) {
      reject("登录过期")
    }
    let url = host + subUrl
    const { header, method, data } = options
    wx.request({
      url,
      header,
      method,
      data,
      success: res => {
        const { code, data } = res.data
        if (res && code === 200) {
          resolve(data)
        } else {
          let err = {
            request: res.request,
            response: {
              data: { status: res.data.code, description: res.data.message }
            }
          }
          const handlerErr = responseErrorHandler(err)
          res.err = handlerErr
          reject(res)
        }
      },
      fail: err => {
        reject(err)
      },
      complete: e => {}
    })
  })
  return res
}

/**
 * 统一处理响应错误
 * @param {object} error
 */
const responseErrorHandler = error => {
  // 自定义错误
  let err = {
    title: "未知错误",
    description: "系统发生未知的错误"
  }
  if (error.response) {
    // 发送请求后,服务端有返回
    // 1. HTTP返回的响应码不是 2xx
    // 2. 服务端自定义错误,服务端响应码不是 2xx
    // 3. 客户端自定义错误,返回的数据被定义为错误状态
    err = getErrorMessage(error)
    if (err.code === 406) {
      const token = getLoginToken()
      if (token) {
        clearLoginToken()
        //之前有token,token过期
        loginDialog()
      } else {
        loginDialog()
      }
      return false
    }
  } else if (error.request) {
    // 发送请求但是没有响应返回
    err.title = "服务器忙"
    err.description = "服务器繁忙,请稍后重试"
  }
  return err
}

export { wxRequest }

请求异常映射expectionalError.js

主要分为三大块: 1.常见请求的基本错误码 2.业务相关的错误码 3.未识别的错误码

//异常信息统一处理
const HTTP_ERROR = {
  HTTP_DEFALUT_ERROR: new Map([
    [400, '请求参数错误'],
    [401, '请求要求用户的身份认证'],
    [403, '服务器拒绝执行此请求'],
    [404, '请求的资源无法找到'],
    [405, '请求中的方法被禁止'],
    [406, '服务器无法根据客户端请求的内容特性完成请求'],
    [407, '请求要求代理的身份认证'],
    [408, '请求超时'],
    [409, '请求存在冲突'],
    [410, '客户端请求的资源已经不存在'],
    [411, '服务器无法处理不带Content-Length的请求信息'],
    [412, '请求信息的先决条件错误'],
    [413, '请求的实体过大'],
    [414, '请求的URI过长'],
    [415, '无法处理请求附带的媒体格式'],
    [416, '无效的请求范围'],
    [417, '无法满足Expect的请求头信息'],
    [500, '服务器内部错误'],
    [501, '服务器不支持该请求功能'],
    [502, '请求失效'],
    [503, '服务器暂时的无法处理请求'],
    [504, '无法获取请求'],
    [505, 'http协议版本错误']
  ]),

  CUSTOM_ERROR: new Map([
    [404, '请求数据不存在'],
    [406, '登陆过期,请重登'],
    [200201, '乘客登录验证码发送失败'],
    [200202, '验证码已过期,请重新获取'],
    [200203, '短信验证码错误'],
    [200501, '临时code为空'],
    [200502, '参数配置错误'],

    [301001, '资源不可达'],
    [301002, '未查询出匹配数据'],
    [301003, '输入的城市在有效范围中'],
    [301004, '短信验证码错误'],
    [301003, '高德查询主机解析错误']
    // ....
  ])
}

const getErrorMessage = function (error) {
  let response = error.response.data
  let err = {
    title: '未知错误',
    description: '系统发生未知的错误'
  }
  if (typeof response === 'string') {
    err.description = '服务器异常,请稍后重试'
  } else if (error.response.status >= 400 && error.response.status < 600) {
    err.title = (error.response.status < 500) ? '请求错误' : '服务器错误'
    err.description = HTTP_ERROR.HTTP_DEFALUT_ERROR.get(error.response.status)
  } else {
    err.code = response.status
    err.title = '后台系统错误'
    err.description = (response && 'status' in response && HTTP_ERROR.CUSTOM_ERROR.has(response.status)) ? HTTP_ERROR.CUSTOM_ERROR.get(response.status) : '操作失败,请稍后重试'
  }

  return err
}

export default getErrorMessage

请求api

所有业务请求的api接口,包含h5链接,在wx-request.js请求模块中通过host+api接口拼接的方式组成完整的请求地址

const api = {
  // 获取证码
  'SMS_CODE': '/usr/passenger/captcha',
  // 乘客登录
  'PASSENGER_LOGIN': '/usr/passenger/wechat-login',
  // 意见反馈
  'FEEDBACK': '/usr/setting/feedback',
  // 根据乘客输入的城市和上下车地点查询地点的相关信息(模糊查询
  'QUERY_SITE': '/usr/amap/obscureQuerySite',
  // 订单列表
  'ORDER_LIST': '/my/order/list',
  // 收费说明
  'CHARGE_DESCRIPTION': '/h5/charge-description.html',
  // 免责协议
  'DISCLAIMER_AGREEMENT': '/h5/disclaimer-agreement.html'
}

export default api

本地存储storageSyncTool.js

这个模块中主要做2件事,

  • 1.定义登录后token的键名,定义存储/获取登录token的方法getLoginToken/setLoginToken,请除登录token的方法clearLoginToken
  • 2.定义通用的存储/获取本地存储的方法getStorage/setStorage,清除方法clearStorage
// 定义登录token的key(键名),在获取/存储的时候,通过这个key获取和存储
const LOGIN_TOKEN_KEY = 'customerTokenName'

const setStorage = (key, value) => {
  try {
    wx.setStorageSync(key, value)
    return true
  } catch (e) {
    return false
  }
}

const getStorage = (key) => {
  try {
    const value = wx.getStorageSync(key)
    return value
  } catch (e) {
    return false
  }
}

const clearStorage = key => {
  try {
    wx.clearStorageSync(key)
    return true
  } catch (e) {
    return false
  }
}

const setLoginToken = (value) => {
  return setStorage(LOGIN_TOKEN_KEY, value)
}

const getLoginToken = () => {
  return getStorage(LOGIN_TOKEN_KEY)
}

const clearLoginToken = () => {
  return clearStorage(LOGIN_TOKEN_KEY)
}

module.exports = {
  setLoginToken,
  getLoginToken,
  getStorage,
  setStorage,
  clearLoginToken,
  clearStorage
}

使用封装请求的简单示例

  • 1 GET请求

导入api模块和请求模块,取出对应的api接口,准备请求数据,最后发送请求

import API from '../../utils/api.js';
import { wxRequest } from '../../utils/wx-request.js';

Page({
  //....
  getList() {
    const that = this
    // 获取请求api
    const url = API.ORDER_LIST
    const { page, size } = that.data
    const params = {
      page,
      size
    }
    wxRequest(url, {
      data: params
    }).then(data => {
      if (data && data.length > 0) {
        that.setData({
          list: data
        })
      }
    }).catch(res => {
      const { description } = res.err
      console.log(description)
    })
  }
})

  • 2 POST请求
import API from '../../utils/api.js';
import { wxRequest } from '../../utils/wx-request.js';

Page({
  submitFeedback() {
    const that = this
    const { suggestion, phoneNum } = that.data
    const url = API.FEEDBACK

    const params = {
      suggestion,
      phoneNum
    }

    wxRequest(url, {
      method: 'POST',
      data: params
    }).then(res => {
      wx.showToast({
        title: '提交反馈成功',
        mask: true,
        icon: 'success',
        duration: 3000,
        success: function() {
          setTimeout(() => {
            wx.navigateBack({
              delta: 1
            })
          }, 3000)
        }
      })
    }).catch(res => {
      const { description } = res.err
      console.log(description)
    })
  }
})

3 其它问题

通过上面的封装,可以比较好的将api,请求,请求调用和异常信息处理分模块处理,方便后面的维护,但是还有一些问题还是可以优化的 比如,环境的切换(一般都有开发、测试、生产环境,都对应不同的地址)

环境管理

在上面的请求封装中,host地址只有一个,在使用的时候将host+api接口的方式组成完整的地址每次切换环境都得修改host变量,比较麻烦,并且实际生产中,不同环境除了host不同外,h5地址,wss,第三方key都是不同的,一套环境同时维持不同请求内容.所有需要一个父级进行包裹它们,切换环境,只需要更改父级即可.

定义环境管理env.js

// 运行环境配置文件

// 开发环境
const dev = {
  host: 'https://dev.qq.cn',
  ws: 'wss://dev.qq.cn',
  h5: 'https://dev.qq.cn',
  label: 'dev',
}

// 测试环境
const test = {
  host: 'https://test.qq.cn',
  ws: 'wss://test.qq.cn',
  h5: 'https://test.qq.cn',
  label: 'test'
}

// 生产环境
const prod = {
  host: 'https://prod.qq.cn',
  ws: 'wss://prod.qq.cn',
  h5: 'https://prod.qq.cn',
  label: 'prod'
}

module.exports = {
  domain: prod
}

在请求模块中使用

  • 导入env.js
  • 拼接完整地址使用let url = env.domain.host + subUrl方式,代替之前的host+subUrl方式
import env from './env.js';

// 请求封装
const wxRequest = async (subUrl, params = {}) => {
  const token = getLoginToken()
  const defaults = {
    header: {
      "Content-Type": "application/json",
      token: token || ""
    },
    method: "GET",
    data: {}
  }

  // 合并参数
  const options = mergeRequestParmas(defaults, params)
  let res = await new Promise((resolve, reject) => {
    if (!token) {
      reject("登录过期")
    }
    
    let url = env.domain.host + subUrl
    const { header, method, data } = options
    wx.request({
      url,
      header,
      method,
      data,
      success: res => {
        const { code, data } = res.data
        if (res && code === 200) {
          resolve(data)
        } else {
          let err = {
            request: res.request,
            response: {
              data: { status: res.data.code, description: res.data.message }
            }
          }
          const handlerErr = responseErrorHandler(err)
          res.err = handlerErr
          reject(res)
        }
      },
      fail: err => {
        reject(err)
      },
      complete: e => {}
    })
  })
  return res
}
//.......

export {
  wxRequest
}