基于 wx.request 封装类似axios请求库

1,907 阅读5分钟

前言

在H5项目中,我们可以利用axios来发送ajax请求。其中,ajax的最大优势在于其拦截器功能,通过拦截器可以对请求和响应进行拦截,并根据业务需求进行进一步处理。而在小程序开发中,我们可以通过对wx.request进行二次封装,从而实现类似于axios的功能。

源码地址git

npm地址npm

封装后支持以下功能:

  • 拦截器
  • 取消请求
  • 使用方式axios(url[, config]),axios.request(config),axios.get(url[, config])...

定义构造函数

默认配置

  • 默认使用get请求
  • 在发起请求时,会自动将baseUrl和url进行拼接。
// defaults.js
export default {
  baseUrl: '',
  method: 'get'
}

Axios

合并默认配置和实例配置,并将合并后的结果赋值给this.defaults,使得在Axios实例中可以方便地访问配置项。

// Axios.js
import defaults from '../defaults/index'

function Axios(config) {
  // 合并默认配置和实例配置
  this.defaults = { ...defaults, ...config }
}

添加请求

添加请求方法到Axios原型

  • 定义Axios.prototype.request方法,该方法暂未实现功能,目前只是一个占位符。

  • 遍历所有请求方式添加到Axios的原型上,其内部通过调用this.request方法发送请求。

// Axios.js
// 公共请求
Axios.prototype.request = function(){
  
}
// 其他请求方式
const methods = ['OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT']

// 遍历请求方式添加到原型
methods.forEach(method => {
  Axios.prototype[method.toLowerCase()] = function(config) {
    return this.request({ ...config, method: method.toLowerCase() })
  }
})

封装公共请求

  • 支持axios(config) 或 axios(url, config)调用方式
  • axios(config),参数为配置信息,配置信息和wx.request一致
  • axios(url, config),参数一为请求url, 参数二为配置信息

请求适配器

  • 在发起请求时,如果配置了 baseUrl,将会自动拼接 baseUrl 和 url,然后发送请求。如果未配置 baseUrl,则直接使用 url 发起请求。
  • rest 是一些其他的配置信息,和 wx.request 的配置信息是一致的。
  • 如果状态码在 200 到 299 的范围内,则意味着请求成功。此时会返回响应结果和配置信息,并且 Promise 的状态将会被置为已满足(fulfilled)状态。
  • 如果状态码不在 200 到 299 的范围内,则意味着请求失败。此时会返回错误信息和配置信息,并且 Promise 的状态将会被置为已满足(rejected)状态。
  • 该函数最终会返回一个 Promise 对象,以便调用者可以使用 then 和 catch 方法来处理请求的结果。
// utils.js
export function adapter(config) {
  const { baseUrl, url, ...rest } = config
  return new Promise((resolve, reject) => {
    wx.request({
      ...rest,
      // 拼接请求url
      url: baseUrl ? baseUrl + url : url,
      success(response) {
        if (response.statusCode >= 200 && response.statusCode < 300) {
          // 请求成功, 状态码2xx
          resolve({ ...config, ...response})
        } else {
          // 请求成功,非2xx的htt状态码
          reject({ ...config, ...response})
        }
      },
      fail(error) {
        // 请求发送失败,断网或超时
        reject({ ...config, ...error})
      }
    })
  })
}

使用配置的适配器将请求分派到服务器

// utils.js
export function dispatchRequest(config) {
  return adapter(config).then((response) => {
    return response
  }).catch((error) => {
    return Promise.reject(error)
  })
}

实现公共请求函数

  • 该方法接收两个参数。当第一个参数是字符串时,被视为请求的 URL,而第二个参数则被视为请求的配置信息。如果第一个参数不是字符串,则直接将其作为配置信息。
  • 将 Axios 的默认配置信息和本次请求的配置信息合并起来,作为本次请求的配置信息。
  • 通过循环遍历 chains 数组,创建一个处理链。
// Axios.js
import { dispatchRequest } from '../utils'

// 公共请求方法
Axios.prototype.request = function(configOrUrl, config){
  if (typeof configOrUrl === 'string') {
    config = config || {}
    config.url = configOrUrl
  } else {
    config = configOrUrl || {}
  }
  // 合并配置信息
  config = { ...this.defaults, ...config }
  
  const chains = [ dispatchRequest, undefined ]
  let promise = Promise.resolve(config)

  // 循环
  while(chains.length) {
    promise = promise.then(chains.shift(), chains.shift())
  }

  return promise
}

创建实例函数

复制Axios属性和方法,挂载到实例对象

// Axios.js
export function createInstance(config) {
  // 创建实例
  const context = new Axios(config)
  // 通过bind改变request方法的this指向,以支持调用instance(config)
  const instance = Axios.prototype.request.bind(context)

  // 遍历Axios原型上的所有方法,挂载到实例
  Object.keys(Axios.prototype).forEach((key) => {
    instance[key] = Axios.prototype[key]
  })

  // 遍历Axios所有属性,挂载到实例
  Object.keys(context).forEach((key) => {
    instance[key] = context[key]
  })

  return instance
}

使用

// 创建实例
const axios = createInstance({ baseUrl: 'http://localhost:3000' })
// 方式1
axios({ 
  url: '/posts', data: { id: 2 }, method: 'get'
})
  .then(result => {
    console.log(result)
  })
  .catch(error => {
    console.log(error)
  })

// 方式2
axios.request('/posts', { data: { id: 1 }})
  .then(result => {
    console.log(result)
  })
  .catch(error => {
    console.log(error)
  })

// 方式3
axios.post({
  url: '/posts', data: { id: 2 }
})
  .then(result => {
    console.log(result)
  })
  .catch(error => {
    console.log(error)
  })
// ...

添加拦截器功能

InterceptorManager

在定义 InterceptorManager 类时,我们使用 use 方法来接受两个参数,fulfilled 作为 Promise 成功回调,reject 作为 Promise 失败回调(可选)。然后,我们将 fulfilled 和 reject 作为对象 { fulfilled, reject } 的属性添加到 handles 数组中。

// InterceptorManager.js
class InterceptorManager {
  constructor() {
    this.handles = []
  }
  use(fulfilled, reject) {
    this.handles.push({ fulfilled, reject })
  }
}

export default InterceptorManager

Axios添加拦截器

Axios构造函数中添加了拦截器属性interceptors,其中包括了请求拦截器interceptors.request和响应拦截器interceptors.response。

import { dispatchRequest } from '../utils'
import defaults from '../defaults/index'
// +
import InterceptorManager from './InterceptorManager'

function Axios(config) {
  this.defaults = { ...defaults, ...config }
  // + 拦截器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  }
}

公共请求处理拦截器

通过遍历请求拦截器数组response.handles,使用unshift方法将handle.fulfilled和handle.reject依次添加到chains数组的头部。
通过遍历响应拦截器数组response.handles,使用push方法将handle.fulfilled和handle.reject依次添加到chains数组的末尾。
在循环 chains 数组时,通过请求拦截器逐个处理请求配置信息,然后将配置信息传递给 dispatchRequest 来发送请求,并将响应结果交由响应拦截器处理。

// Axios.js
Axios.prototype.request = function(configOrUrl, config){
  if (typeof configOrUrl === 'string') {
    config = config || {}
    config.url = configOrUrl
  } else {
    config = configOrUrl || {}
  }
  // 合并配置信息
  config = { ...this.defaults, ...config }
  
  const chains = [ dispatchRequest, undefined ]
  let promise = Promise.resolve(config)

  // + 遍历请求拦截器,使用unshift添加到chains数组前面
  this.interceptors.request.handles.forEach(handle => {
    chains.unshift(handle.fulfilled, handle.reject)
  })

  // + 遍历响应拦截器,使用push追加到chains数组
  this.interceptors.response.handles.forEach(handle => {
    chains.push(handle.fulfilled, handle.reject)
  })

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

  return promise
}

实例使用拦截器

// 创建实例
const axios = createInstance({ baseUrl: 'http://localhost:3000' })

// 添加请求拦截器
axios.interceptors.request.use((config) => {
  config.header = {
    token: 'xxx'
  }
  return config
})

// 添加响应拦截器
axios.interceptors.response.use((response) => {
  return response.data
}, (error) => {
  return Promise.reject(error)
})

取消请求

CancelToken

// utils.js
export function CancelToken(execute) {
  let cancel;
  this.promise = new Promise(resolve => {
    cancel = resolve
  })
  execute(() => {
    cancel()
  })
}

为请求适配器添加取消方法

// utils.js
export function adapter(config) {
  // + cancelToken
  const { baseUrl, cancelToken, url, ...rest } = config
  return new Promise((resolve, reject) => {
    // + requestTask 
    const requestTask = wx.request({
      ...rest,
      url: baseUrl ? baseUrl + url : url,
      success(response) {
        if (response.statusCode >= 200 && response.statusCode < 300) {
          // 请求成功, 状态码2xx
          resolve({ ...config, ...response})
        } else {
          // 请求成功,非2xx的htt状态码
          reject({ ...config, ...response})
        }
      },
      fail(error) {
        // 请求发送失败,断网或请求超时了
        reject({ ...config, ...error})
      }
    })
    // +
    if (cancelToken) {
      cancelToken.promise.then(function() {
        requestTask.abort()
      })
    }
  })
}

使用取消请求

const axios = createInstance({ baseUrl: 'http://localhost:3000' })

let cancel;
const cancelToken = new CancelToken(function(_cancel) {
  cancel = _cancel
})

axios({
  url: '/posts', data: { id: 2 }, method: 'get', cancelToken
})
  .then(result => {
    console.log(result)
  })
  .catch(error => {
    console.log(error)
  })

// 取消请求
cancel()