uni-app 系列(一) 请求封装

3,088 阅读4分钟

请求封装

目前有一年多没有使用 uni-app 了,现在因为业务需求,重新拾起。

后面我会出一系列关于 uni-app 文章

下面讲的请求封装是借鉴 axios 最基本的

本人喜欢代码加注释讲解,下面就开始今天的主题

// 默认配置
const instanceConfig = {
  baseURL: '',
  header: {
    'content-type': 'application/json'
  },
  method: 'GET',
  dataType: 'json',
  responseType: 'text'
}

// 拦截器
class InterceptorManager {
  handlers = []

  use (fulfilled, rejected) {
    this.handlers.push({
      fulfilled: fulfilled,
      rejected: rejected
    })
    return this.handlers.length - 1
  }

  forEach (fn) {
    this.handlers.forEach(fn)
  }
}
class Request {
  /**
   * 默认配置
   */
  defaults = instanceConfig

  /**
   * 请求拦截器
   */
  interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  }

  constructor (config) {
    this.defaults = merge(this.defaults, config)
  }
}

基本构建就是上面的样子

下面就抽取一些工具作为辅助(为了合并参数)

// 对应 axios 项目路径 lib\utils
function isPlainObject(val) {
  if (toString.call(val) !== '[object Object]') {
    return false
  }

  var prototype = Object.getPrototypeOf(val);
  return prototype === null || prototype === Object.prototype;
}

function forEach(obj, fn) {
  if (obj === null || typeof obj === 'undefined') {
    return
  }

  if (typeof obj !== 'object') {
    obj = [obj]
  }

  if (Array.isArray(obj)) {
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj)
    }
  } else {
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj)
      }
    }
  }
}

function merge(...args) {
  var result = {}

  function assignValue(val, key) {
    if (isPlainObject(result[key]) && isPlainObject(val)) {
      result[key] = merge(result[key], val)
    } else if (isPlainObject(val)) {
      result[key] = merge({}, val)
    } else if (Array.isArray(val)) {
      result[key] = val.slice()
    } else {
      result[key] = val
    }
  }

  for (var i = 0, l = args.length; i < l; i++) {
    forEach(args[i], assignValue);
  }

  return result
}

我们都知道有个 baseURL 需要和 url 组合起来

// 组合 url
// 在 axios 是分了多个方法的,我这里为了简便,直接封成一个方法
// 对应 axios 项目路径 lib\core\buildFullPath
function buildFullPath (baseURL, relativeURL) {
  // 判断是否绝对地址
  if (/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(relativeURL)) return relativeURL
  // 不是绝对地址进行拼接
  return relativeURL
    ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
    : baseURL;
}

下面还有一个概念 promise chain 如下面图所示,(百度找的,如有侵权,请联系删除)

代码如下所示

request (config) {
  // 合并参数
  config = merge(this.defaults, config)
  // 至于这里为什么需要一个 undefined,是为了这条链
  const chain = [dispatchRequest, undefined]
  // 创建一个 promise 而且是成功状态的
  let promise = Promise.resolve(config)
  // 遍历请求拦截器
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected)
  })
  // 遍历响应拦截器
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected)
  })
  // 创建 promise 链
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift())
  }
  
  return promise
}

实现 dispatchRequest 方法

function dispatchRequest (config) {
  // 组合完整的 url
  const url = buildFullPath(config.baseURL, config.url)
  return new Promise((resolve, reject) => {
    config.success = function (res) {
      res.config = config
      resolve(res)
    }
    config.fail= function (err) {
      reject(err)
    }
    uni.request({
      ...config,
      url
    })
  })
}

到此就结束了,以下是完整代码

function isPlainObject(val) {
  if (toString.call(val) !== '[object Object]') {
    return false
  }

  var prototype = Object.getPrototypeOf(val);
  return prototype === null || prototype === Object.prototype;
}

function forEach(obj, fn) {
  if (obj === null || typeof obj === 'undefined') {
    return
  }

  if (typeof obj !== 'object') {
    obj = [obj]
  }

  if (Array.isArray(obj)) {
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj)
    }
  } else {
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj)
      }
    }
  }
}

function merge(...args) {
  var result = {}

  function assignValue(val, key) {
    if (isPlainObject(result[key]) && isPlainObject(val)) {
      result[key] = merge(result[key], val)
    } else if (isPlainObject(val)) {
      result[key] = merge({}, val)
    } else if (Array.isArray(val)) {
      result[key] = val.slice()
    } else {
      result[key] = val
    }
  }

  for (var i = 0, l = args.length; i < l; i++) {
    forEach(args[i], assignValue);
  }

  return result
}

function buildFullPath (baseURL, relativeURL) {
  
  if (/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(relativeURL)) return relativeURL

  return relativeURL
    ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
    : baseURL;
}

const instanceConfig = {
  baseURL: '',
  header: {
    'content-type': 'application/json'
  },
  method: 'GET',
  dataType: 'json',
  responseType: 'text'
}

function dispatchRequest (config) {
  const url = buildFullPath(config.baseURL, config.url)
  return new Promise((resolve, reject) => {
    config.success = function (res) {
      res.config = config
      resolve(res)
    }
    config.fail= function (err) {
      reject(err)
    }
    uni.request({
      ...config,
      url
    })
  })
}

class InterceptorManager {
  handlers = []

  use (fulfilled, rejected) {
    this.handlers.push({
      fulfilled: fulfilled,
      rejected: rejected
    })
    return this.handlers.length - 1
  }

  forEach (fn) {
    this.handlers.forEach(fn)
  }
}

export default class Request {
  defaults = instanceConfig

  interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  }

  constructor (config) {
    this.defaults = merge(this.defaults, config)
  }

  request (config) {
    config = merge(this.defaults, config)

    const chain = [dispatchRequest, undefined]

    let promise = Promise.resolve(config)

    this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
      chain.unshift(interceptor.fulfilled, interceptor.rejected)
    })

    this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
      chain.push(interceptor.fulfilled, interceptor.rejected)
    })

    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift())
    }
  
    return promise
  }
}

使用方式

// 创建请求对象
const request = new Request({
  baseURL: 'https://****'
})

// 请求拦截器
request.interceptors.request.use(config => {
  config.header.Authorization = '可以添加凭证 token'

  return config
})

// 响应拦截器
request.interceptors.response.use(response => {
  return response
}, err => {
  console.log(err.errMsg)
  return Promise.reject(err.errMsg)
})

// 发送请求
request.request({
  url: '/****',
  data: { wd: 'uni-app' }
}).then(res => {
  console.log(res)
}).cacth(err => {
  console.log(err)
})

后面会持续更新(业务怎么处理,请求失败了怎么重试,无感刷新 token,请求失败了是否弹框或者提示)

上面的代码仅供参考,目前项目在起步状态,没有使用到其他处理方式,后面项目用到了,会加上对应的处理方式

业务处理

注意下面的处理,每个公司的状态码 code 码是不一样的

为什么要这样处理了,这样处理后有什么好处,先看下面代码

const codeMessage = {
  400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
  401: '用户没有权限(令牌、用户名、密码错误)。',
  403: '用户得到授权,但是访问是被禁止的。',
  404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
  406: '请求的格式不可得。',
  410: '请求的资源被永久删除,且不会再得到的。',
  422: '当创建一个对象时,发生一个验证错误。',
  500: '服务器发生错误,请检查服务器。',
  502: '网关错误。',
  503: '服务不可用,服务器暂时过载或维护。',
  504: '网关超时。'
}

request.interceptors.response.use(response => {
  // 业务处理
  const { data, statusCode  } = response
  const { response: res, code, msg } = data

  // 状态码不是 200 说明出错了
  if (statusCode !== 200) {
    // 及时抛出错误
    // uni.showToast 自行处理 可以配置要不要弹框
    return Promise.reject(codeMessage[statusCode] || '请求失败')
  }

  // 成功了只要数据其他都不要
  if (code === 1) {
    return res
  }

  // 处理 code 不等于 1 的情况
  // uni.showToast 自行处理 可以配置要不要弹框
  return Promise.reject(msg)

}, err => {
  // uni.showToast 自行处理 可以配置要不要弹框
  return Promise.reject(err.errMsg)
})

// 经过上面处理后
// 调用请求我们就可以只关注成功的逻辑,屏蔽掉一些不必要的判断和错误处理
request.request({
  url: 'https://****',
}).then(res => {
  // 只有走到这里就是成功的
  // 不会在这里判断是否成功了
  console.log(res)
}).catch(err => {
  // 失败逻辑
  // 可以统一在响应拦截器处理
  console.log(err)
})

说明:

很多人问我为什么不添加以下方法

request#get
request#delete
request#head
request#options
request#post
request#put
request#patch

因为没有什么必要,上面的方法都是基于 request#request 方法的,而且提供的调用方式太多了会产生一定的负担

敬请期待(装饰器实现后端 CRUD 接口)

功能已经完善了,在后台项目已经稳定运行了一段时间

后续会讲源码实现

// 创建基本 Service
const BaseService = createBaseService('axios 请求实体')
// 和后端商议: 比如用户 user
// https://**/user/list
// https://**/user/detail
// https://**/user/create
// https://**/user/update
// https://**/user/delete

// 赋能(可以根据自行商议的结构定义)
class Service extends BaseService {
  list = (params?: any) => {
    return this.request({
      url: 'list',
      params
    })
  }

  detail = (params: any) => {
    return this.request({
      url: 'detail',
      params
    })
  }

  create = (data: any) => {
    return this.request({
      method: 'post',
      url: 'create',
      data
    })
  }

  update = (data: any) => {
    return this.request({
      method: 'post',
      url: 'update',
      data
    })
  }

  delete = (data: any) => {
    return this.request({
      method: 'post',
      url: 'delete',
      data
    })
  }
}

// 使用方式
@Controller('/user')
class User extends Service {}

const user = new User()

// 经过上面的定义就可以一下子生成五个接口
// url: /user/list
user.list
// url: /user/create
user.create
// url: /user/detail
user.detail
// url: /user/update
user.update
// url: /user/delete
user.delete

// 要是要扩展怎么办,比如需要一个 user 选项
@Controller('/user')
class User extends Service {
  @Get('options')
  getOptions () {}
  
  // 这个写法等同于上面,只是名称不一样
  // @Get()
  // options () {}
}
const user = new User()

// 这样就可以扩展了
// url: /user/options
user.getOptions

// 可能有人会问有一些公共的请求,并不需要 (CRUD),这时就需要一个纯粹的 Service
// 还记得一开始创建的 BaseService 吗,这个就是纯粹的
@Controller('/api')
class Common extends BaseService {
  @Get('user/info')
  getUserInfo () {}
  
  @Post()
  setUserInfo
}

const common = new Common()

// url: /api/user/info
common.getUserInfo
// url: /api/setUserInfo
common.setUserInfo