压箱底的 axios 封装类,了解一下?

3950

在前端项目中,我们通常使用的 axios 库和后端进行数据交互,它是基于 promisehttp库,可运行在 浏览器端node.js 中。因为有很多优秀的特性,例如请求和响应拦截、取消请求、转换 json、客户端防御 CSRF 等,所以封装一个符合自身业务场景的 axios 类是很有必要的,可以为日后的项目开发提供复用和统一接口请求规范化。

下面开始我们的 axios 封装之旅。

1、目录结构

api
  ├─ request                    // 文件夹,存放封装类的处理逻辑相关
  │  ├─ axiosApi.js              // 封装类导出文件
  │  ├─ config.js                // 接口 baseURL 的配置文件,开发或者生产环境
  │  ├─ HttpRequest.js           // 类逻辑处理封装文件
  │  └─ errorHandle.js           // 全局错误响应处理文件
  ├─ urls                       // 文件夹,存放所有接口请求 URL 路径
  │  ├─ users.js
  │  ├─ log.js
  │  └─ ...
  └─ index.js                   // 封装类引用入口文件

2、引入所需模块

HttpRequest.js 文件中,首先引入我们需要用到的模块,如下:

import axios from 'axios' // 引入 axios 库
import qs from 'qs' // qs 模块,用来系列化 post 类型的数据
import store from '@/store' // 状态管理,用于设置 token 或者调用自定义的接口方法
import errorHandle from './errorHandle' // 统一的错误响应处理
import { getToken } from '@/utils/auth' // 从 localStorage 中获取 token

3、HttpRequest 类封装

1)定义默认配置

默认配置中,我们可以根据是开发环境或者生产环境来定义对应的 baseURL,默认请求方式是 GETheader头信息设置接受的数据类型和请求发送的数据格式是 json,超时时间设定为10s,可根据不同接口请求的实际情况重新定义。

class HttpRequest {
  // 设置默认值为空方便使用 devServer 代理
  constructor (baseURL = '') {
    this.defaultConfig = { // 默认配置
      baseURL,
      method: 'get',
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json; charset=UTF-8'
      },
      timeout: 1000 * 10, // 请求超时时间
      isErrorHandle: false // 是否开启全局错误响应提示,默认关闭
    }
  }
}

2)创建 axios 实例

创建 axios 实例时,会将用户传入的 options 配置与默认配置合并,然后调用定义好的 interceptors 拦截器(下面会讲到),最后将 axios 实例返回。

  /**
   * 创建 axios 实例
   *
   * @param {Object} options 用户自定义配置
   * @return {Axios} 返回 axios 实例
   * @memberof HttpRequest
   */
  createAxiosInstance (options) {
    const axiosInstance = axios.create()
    // 默认配置和用户自定义配置合并
    const newOptions = this.mergeOptions(this.defaultConfig, options)
    // 调用拦截器
    this.interceptors(axiosInstance)
    // 返回实例
    return axiosInstance(newOptions)
  }

  /**
   * 合并配置
   *
   * @param {Object} source 原配置项
   * @param {Object} target 目标配置项
   * @return {Object} 返回新配置
   * @memberof HttpRequest
   */
  mergeOptions (source, target) {
    if (typeof target !== 'object' || target == null) {
      return source
    }
    return Object.assign(source, target)
  }

3)拦截器设置

在拦截器中,我们将 axios 实例作为参数引入,然后定义 请求拦截器响应拦截器

  • 请求拦截器

    • 常规操作,每次请求都会带上 token 作为与后端数据交互的验证依据;
    • 这边只处理常用的 getpost 请求,根据配置中传进来的 method 属性来动态的执行请求方式(注意这里没有单独的封装 getpost 请求方法);
    • 传参方式有点区别, get 请求方式传参使用 params: {id: xx} 形式, post 方式则为 data: {id: xx} ;
    • 结合请求头的 Content-Type 属性的不同,来对传参对象序列化(详情看注释),这样后端才能正常接收到传的参数,当为上传文件类型时,需要自己在请求头中设置 'Content-Type': 'multipart/form-data;'
  • 响应拦截器

    • 拦截器处理比较简单,根据服务器响应的 status 状态码,正常响应时,将响应数据 data 返回出去,异常响应时,则将整个 response 以失败的形式返回出去。
/**
 * 拦截器
 *
 * @param {Axios} instance
 * @memberof HttpRequest
 */
interceptors (instance) {
  // 请求拦截器
  instance.interceptors.request.use((config) => {
    const { headers, method, params, data } = config
    // 每次请求都携带 token
    const token = getToken() || ''
    token && (headers.Authorization = token)

    // 如果 Content-type 类型不为 'multipart/form-data;' (文件上传类型 )
    if (!headers['Content-Type'].includes('multipart')) {
      // 如果请求方式为 post 方式,设置 Content-type 类型为 'application/x-www-form-urlencoded; charset=UTF-8'
      (method === 'post') && (headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8')
      // 根据 contentType 转换 data 数据
      const contentType = headers['Content-Type']
      // Content-type类型 'application/json;',服务器收到的raw body(原始数据) "{name:"nowThen",age:"18"}"(普通字符串)
      // Content-type类型 'application/x-www-form-urlencoded;',服务器收到的raw body(原始数据) name=nowThen&age=18
      const paramData = (method === 'get') ? params : data
      contentType && (config.data = contentType.includes('json') ? JSON.stringify(paramData) : qs.stringify(paramData))
    }
    return config
  }, (error) => {
    // 处理响应错误
    this.defaultConfig.isErrorHandle && errorHandle(error)
    return Promise.reject(error)
  })

  // 响应拦截器
  instance.interceptors.response.use((response) => {
    const { status, data } = response

    // 正常响应
    if (status === 200 || (status < 300 || status === 304)) {
      if (data.code === 401) {
        // token 错误或者过期,需要重新登录,并清空 store 和 localstorge 中的 token
        store.dispatch('user/toLogin') // 跳转到登录界面
      }
      // 返回数据
      return Promise.resolve(data)
    }
    return Promise.reject(response)
  }, (error) => {
    // 处理响应错误
    this.defaultConfig.isErrorHandle && errorHandle(error)
    return Promise.reject(error)
  })
}
  • 拦截器异常处理
    • 如请求超时、网络异常 / 断开、未授权 / 拒绝访问等情况,则调用封装好的全局统一错误响应处理函数 errorHandle
    • 全局统一错误响应提示默认关闭,可通过配置 isErrorHandletrue 属性来开启。

errorHandle在文件errorHandle.js 中定义,如下:

import { message } from 'ant-design-vue'

/**
* axios统一错误处理主要针对HTTP状态码错误
* @param {Object} err
*/
function errorHandle (err) {
 // 判断服务器响应
 if (err.response) {
   switch (err.response.status) {
     // 用户无权限访问接口
     case 401:
       message.error('未授权,请先登录~')
       break
     case 403:
       message.error('服务器拒绝访问~')
       break
     case 404:
       message.error('请求的资源不存在~')
       break
     case 500:
       message.error('服务器异常,请稍后再试~')
       break
   }
 } else if (err.message.includes('timeout')) {
   message.error('连接超时~')
 } else if (
   err.code === 'ECONNABORTED' ||
   err.message === 'Network Error' ||
   !window.navigator.onLine
 ) {
   message.error('网络已断开,请检查连接~')
 } else {
   // 进行其他处理
   console.log(err.stack)
 }
}

export default errorHandle

4)导出 HttpRequest 类

在文件 HttpRequest.js 的末尾将 HttpRequest 类导出:

export default HttpRequest

4、HttpRequest 类使用

1)api 引用入口文件

index.js 入口文件中,首先要把封装好的实例引入进来,然后这里使用 webpack 给我们提供的 require.context API 来将定义好的 接口url 自动引入,这样我们就不用一个个手动导入了。 require.context API 使用可以参考我的另一篇文章:自动注册Vue组件【require.context】

import api from './request/axiosApi' // 引入axios封装实例

// https://webpack.js.org/guides/dependency-management/#requirecontext
const apiFiles = require.context('./urls', true, /\.js$/)

// 自动加载 urls 目录下的所有配置的接口
const apiRequest = apiFiles.keys().reduce((apis, apiPath) => {
  const name = apiPath.replace(/^\.\/(.*)\.\w+$/, '$1')
  const value = apiFiles(apiPath)

  apis[name] = Object.keys(value.default).reduce((prev, cur) => {
    prev[cur] = (options = {}) => api.createAxiosInstance({ ...value.default[cur], ...options })
    return prev
  }, {})

  return apis
}, {})

// console.log(apiRequest)

export default apiRequest

当然,以上虽然实现了自动化引入,但是却引入了整个接口对象,而有时候我们需要 按需引入,则可以这样做:

import api from './request/axiosApi' // 引入axios封装实例

import users from './urls/users' // 引入 users api 接口配置

const createApi = (apiUrls) => {
  return Object.keys(apiUrls).reduce((prev, cur) => {
    prev[cur] = (options = {}) => axios.createAxiosInstance({ ...apiUrls[cur], ...options })
    return prev
  }, {})
}

export const user = createApi(users)
// 其他接口...

2)接口 url 文件

users.js 为例:

export default {
  login: {
    method: 'post',
    url: 'user/login'
  },
  logout: {
    method: 'post',
    url: 'user/logout'
  },
  getInfo: {
    method: 'get',
    url: 'user/getInfo'
  },
  ...
}

3)axios 实例导出文件

axiosApi.js

import config from './config'
import HttpRequest from './HttpRequest'

// 根据当前环境获取API URL根路径
const baseURL = process.env.NODE_ENV === 'production' ? config.baseURL.prod : config.baseURL.dev
// 创建一个HtpRequest对象实例
const axios = new HttpRequest(baseURL)

export default axios

4)api 使用

  • 全局引入

在项目 main.js 入口文件中:

import apiRequest from './api' // 引入封装的接口对象
Vue.prototype.$api = apiRequest

在组件中使用,接口请求示例:

this.$api.user.getInfo({
	params: {id: xx}
  })
  .then((res) => {
  	// 数据处理...
  })
  .catch((error) => {
  	// 异常处理...
  })
  • 按需引入

在使用的地方,接口请求示例:

import { user } from './api' // 按需引入封装的接口对象
user.getInfo({
	params: {id: xx}
  })
  .then((res) => {
  	// 数据处理...
  })
  .catch((error) => {
  	// 异常处理...
  })

5、小结

好了,把压箱底的 axios 封装拿出来稍微整理下,于是就产生了这篇文章,对于不同的项目业务需求,可能还有需要改进的地方,大家要是有更好的封装和想法,欢迎在下方导论提出优化建议,感谢你能耐心看到这里。