axios的使用和封装(九)

1,344 阅读6分钟

前言

axios 是一个基于 Promise 用于浏览器和 nodejs 的 HTTP 客户端,本质上也是对原生XHR的封装,只不过它是Promise的实现版本,符合最新的ES规范。本章我们将介绍并演示在项目使用 axios 并以项目级编码的要求对 axios 进行二次封装以提升开发效率。


axios 的使用

我们先在当前项目根目录执行npm install axios安装axios,安装成功后可以看到它被添加在package.json的依赖列表中:

image.png

现在我们在App.vuescript块引入axios并在created钩子中通过Axios.request(config)的方式使用:

import Axios from 'axios';

export default {
  created() {
    Axios.request({
      url: 'https://console-mock.apipost.cn/app/mock/project/985272cc-2b5e-4570-d80d-99fff239bc44/todos',
      method: 'get'
    }).then(rs => {
      console.log('响应结果:', rs)
    })
  }
}

我们调用 axios 提供的request方法,并在配置对象config传入我们请求的地址url(这里的地址访问的是我自己的mockServer)以及请求的方式method。因为 axios 是支持 promise 的,所以我们可以直接在then中获取成功响应的结果:

image.png

image.png

这里,我们把获取到的响应结果的每个属性进行简要说明:

  • configaxios请求的配置信息。
  • data:由服务器提供的响内容。
  • headers:服务器的响应头。
  • request:生成此响应的请求,若请求来自浏览器则是XMLHttpRequest实例。
  • status:来自服务器响应的HTTP状态码。
  • statusText:来自服务器响应的状态信息。

当响应成功时,会执行then中的回调,若then中出现异常错误会被 axios 捕获并执行catch

    Axios.request({
      url: 'https://console-mock.apipost.cn/app/mock/project/985272cc-2b5e-4570-d80d-99fff239bc44/todos',
      method: 'get'
    }).then(rs => {
      console.log('响应结果:', rs)
      throw new Error('代码异常')
    }).catch(err => {
      console.log('出现异常:', err)
    })

image.png

除了这种情况,当请求超时、取消以及状态码>300都会触发catch

这里我们用到了urlmethod作为用于发出请求的配置参数,除此之外还有以下常用配置项,其中url是必传的,如果未指定method,则请求将默认为GET

{
    url: '/user',  // 请求地址
    method: 'get',  // 请求方式,默认get
    baseURL: 'https://some-domain.com/api/',  // `除非url是绝对路径,否则baseURL将被置于url之前
    headers: {'X-Requested-With': 'XMLHttpRequest'},  // 自定义请求头
    params: {
        ID: 12345
    },  // 携带在URL上的查询字符串
    data: {
        firstName: 'Fred'
    },  // 请求体的数据
    timeout: 1000,  // 指定请求超时前的毫秒数
    responseType: 'json', // 表示服务器响应的数据类型,默认为json
    withCredentials: false // 跨域请求时是否携带上cookie
}

除了使用request方法,axios 为方便起见,为所有支持的请求方法提供了别名:get、post、put、delete、options、patch、head。我们以get为例,此类的方法接收的第一个参数为请求地址,第二参数为指定的配置,该配置会与 axios 实例的配置合并:

  created() {
    const url = 'https://console-mock.apipost.cn/app/mock/project/985272cc-2b5e-4570-d80d-99fff239bc44/todos'

    Axios.request({
      url,
      method: 'get'
    }).then(rs => {
      console.log('Axios.request响应结果:', rs)
    })

    Axios.get(url,{
      params: {
        id: 1001,
        type: 2
      }
    }).then(rs => {
      console.log('Axios.get响应结果:', rs)
    })
  }

image.png

实际上我们会有一些通用的接口行为,如loading状态、错误处理、会话保持等,显然我们希望能在一个地方通用配置好这些行为避免在业务处一一实现,这时候你就需要 axios 提供的拦截器:

<script>
import Axios from 'axios';
// 添加请求拦截器
Axios.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  console.log('---请求拦截---')
  config.headers.token = 'auth-08jh34'
  return config;
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error);
});

// 添加响应拦截器
Axios.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  console.log('---响应拦截---')
  if (response.status === 200) {
    response = response.data
  }
  return response;
}, function (error) {
  // 对响应错误做点什么
  return Promise.reject(error);
});

export default {
  created() {
    const url = 'https://console-mock.apipost.cn/app/mock/project/985272cc-2b5e-4570-d80d-99fff239bc44/todos'

    Axios.get(url,{
      params: {
        id: 1001,
        type: 2
      }
    }).then(rs => {
      console.log('Axios.get响应结果:', rs)
    })
  }
}
</script>

我们对每次请求发送前都添加了一个自定义的token头。接收响应时,若状态码为200则直接输出服务器响应的data

image.png

image.png

axios 的封装

在具体的视图中应该更专注于业务的处理,对于涉及数据源的操作(获取和更改)应该单独在一个地方被统一管理并暴露使用方法,若引入使用了第三方状态管理器(如vuex、redux),视图应使用管理器操作以保持数据流是单向的。现在我们仅就视图不使用第三方状态管理器的情况进行 axios 的封装和使用演示。

我们在src目录下新建utils目录用于存放我们的工具类,并在该目录中创建我们用于封装 axios 的request.js文件:

image.png

先定义IS_PRPDUCTION常量用于区分环境,这里暂定只区分生产环境和开放环境。通过axios的create方法创建一个实例并设置好基础URL和请求超时时间并导出该实例:

import axios from 'axios';

// 是否是生产环境
const IS_PRPDUCTION = false;

const baseURL = IS_PRPDUCTION ? 
    'https://console-mock.apipost.cn/app/mock/project/985272cc-2b5e-4570-d80d-99fff239bc44' :
    'http://test-environment/api';


const $http = axios.create({
    baseURL,
    timeout: 60000,
});

// TODO


export default $http;

现在我们先将请求拦截处理、响应拦截处理、请求错误处理、响应错误处理的方法先定义出来:

......

// 请求拦截处理
const requestInterceptor = config => {
    return config;
}

// 请求错误处理
const requestErrorHandler = error => {
    return Promise.reject(error);
}

// 响应拦截处理
const responseInterceptor = response => {
    return response;
}

// 响应错误处理
const responseErrorHandler = error => {
    return Promise.reject(error);
}

$http.interceptors.request.use(requestInterceptor, requestErrorHandler);
$http.interceptors.response.use(responseInterceptor, responseErrorHandler);

export default $http;

首先统一添加自定义请求头auth-token用于会话状态验证,并对状态码200的响应进行“去皮”操作直接返回服务端的数据:

// 请求拦截处理
const requestInterceptor = config => {
    // 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的header都加上token,不用每次请求都手动添加了
    const token = localStorage.getItem('auth-token');        
    token && (config.headers['auth-token'] = token);  
    return config;
}

......

// 响应拦截处理
const responseInterceptor = response => {
    const { status, data } = response
    if(status === 200) return data
    return response
}

现在我们模拟一个登录接口和获取列表数据接口,完善 token 使用的整个流程。在 src 目录下新建api目录用于存放我们所有的接口,并在该目录下的user.js中定义接口:

image.png

import $http from '../utils/request'

export const login = () => {
    return $http.post('/user/login')
}

export const getTodos = () => {
    return $http.get('/user/todos')
}

在 APP.vue 添加相应业务代码:

<template>
  <div id="app">
    <button @click="getTodos">获取数据</button>

    <ul>
      <li v-for="(item, index) in todos" :key="item.id">
        <input type="checkbox" id="checkbox" v-model="item.done">
        <p>{{ index + 1 }}、</p> 
        <p>{{ item.content }}</p>
      </li>
    </ul>
  </div>
</template>
<script>
import { login, getTodos } from './api/user'

export default {
  data() {
    return {
      todos: []
    }
  },
  methods: {
    getTodos() {
      getTodos().then(res => {
        const { code, data } = res
        if(code === 0) {
          this.todos = data.list
        } else {
          // 这里的code是业务状态码,根据业务需求进行相应提示操作
        }
      })
    }
  },
  beforeCreate() {
    login().then(res => {
      const { code, data } = res
      if(code === 0) {
        localStorage.setItem('auth-token', data.token)
        alert('登录成功')
      } else {
        // 这里的code是业务状态码,根据业务需求进行相应提示操作
      }
    })
  }
}
</script>

现在我们查看控制台,发现请求接口/user/todos时请求头带上了通过登录拿到的token:

20220328_171538.gif

当我们会话失效即超过 toekn 有效期时,通常会将用户重定向到登录页进行重新登录,这时候就需要在响应拦截器的错误处理里面添加以下类似代码:

// 响应错误处理
const responseErrorHandler = error => {
    if(error.response){
      // 失败响应的status需要在response中获得
      switch(error.response.status){
        // 对得到的状态码的处理,具体的设置视自己的情况而定
        case 401:
        case 404:
            console.log('token失效和404')
            // 通常结合 vue-router 进行路由控制
            window.location.href='/'
            break
        case 405:
            console.log('不支持的方法')
            break
        // case ...
        default:
            console.log('其他错误')
            break
      }
    }
   
    return Promise.reject(error)
}

网络请求时进行相应的 loading 状态展示是必不可少的,现在我们也将它进行统一处理:

......

let ExistingRequest = 0

const showLoading = () => {
    ExistingRequest++;
    // 结合全局状态和UI库进行loading展示
    // 类似触发 -> $bus.emit('showLoading');
};

const hideLoading = () => {
    ExistingRequest--;
  if (ExistingRequest <= 0) {
    // 结合全局状态和UI库进行loading隐藏
    // 类似触发 -> $bus.emit('hideLoading');
  }
};

// 请求拦截处理
const requestInterceptor = config => {
    // 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的header都加上token,不用每次请求都手动添加了
    const token = localStorage.getItem('auth-token');    
    console.log('auth-token:', token)    
    token && (config.headers['auth-token'] = token); 
    showLoading()
    return config
}

// 请求错误处理
const requestErrorHandler = error => {
    return Promise.reject(error);
}

// 响应拦截处理
const responseInterceptor = response => {
    const { status, data } = response
    if(status === 200) return data
    hideLoading()
    return response
}

// 响应错误处理
const responseErrorHandler = error => {
    hideLoading()
    if(error.response){
      // 失败响应的status需要在response中获得
      switch(error.response.status){
        // 对得到的状态码的处理,具体的设置视自己的情况而定
        case 401:
        case 404:
            console.log('token失效和404')
            // 或使用vue-router进行控制
            window.location.href='/'
            break
        case 405:
            console.log('不支持的方法')
            break
        // case ...
        default:
            console.log('其他错误')
            break
      }
    }
    // 注意这里应该return promise.reject(),
    // 因为如果直接return err则在调用此实例时,响应失败了也会进入then(res=>{})而不是reject或catch方法
    return Promise.reject(error)
}

内容预告

之前我们都是在 App.vue 单组件中进行演示代码的,在下一章我们将继续深入组件,学习复合组件的相关知识和运用实践。