Axios请求详解

1,359 阅读10分钟

一、Axios简介

Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests。

axios有以下特性:

  • 从浏览器创建 XMLHttpRequests

  • 从 node.js 创建 http 请求

  • 支持 Promise API

  • 拦截请求和响应

  • 转换请求和响应数据

  • 取消请求

  • 自动转换JSON数据

  • 客户端支持防御XSRF
    目前,axios支持基本的网络请求方法。

  • get:获取数据,请求指定的信息,返回实体对象

  • post:向指定资源提交数据(例如表单提交或文件上传)

  • put:更新数据,从客户端向服务器传送的数据取代指定的文档的内容

  • patch:更新数据,是对put方法的补充,用来对已知资源进行局部更新

  • delete:请求服务器删除指定的数据

  • head:获取报文首部
    为了方便开发者快速的使用上面的这些请求方式,axios为所有支持的请求方法提供了别名:

  • axios(config)

  • axios.request(config)

  • axios.get(url [,config])

  • axios.post(url [,data [,config]])

  • axios.put(url [,data [,config]])

  • axios.delete(url [,config])

  • axios.patch(url [,data [,config]])

  • axios.head(url [,config])

二、axios实例及配置方法

创建axios实例

创建axios实例使用的是create方法。

axios.create([config])

可以同时创建多个axios实例,示例代码:

const instance = axios.create({baseURL: 'https://some-domain.com/api/',timeout: 1000,headers: {'X-Custom-Header': 'foobar'}});

配置方法

配置对象常用的配置项:

这些是创建请求时可以用的配置选项。只有 url 是必需的。如果没有指定 method,请求将默认使用 GET 方法,详细的配置说明如下:

{  // 路径url  url: '/user',  // 请求方法,默认get  method: 'get',   //基础url,最终请求的url是 baseURL+url拼接,所以再全局设置默认,可以使得发送请求时的url变得简洁  baseURL: 'https://some-domain.com/api/',  //设置请求头  headers: {'X-Requested-With': 'XMLHttpRequest'},  //设置请求url的query参数,可以使得url简洁。  //比如url是https://some-domain.com/api/user  然后params如下设置,那么最终的url是:  //https://some-domain.com/api/user?ID=12345&name=Jack  params: {    ID: 12345,    name:"Jack"  }, //设置请求体  data: {    firstName: 'Fred'  },  //设置请求的另外一种格式,不过这个是直接设置字符串的  data: 'Country=Brasil&City=Belo Horizonte', //请求超时,单位毫秒,默认0,不超时。  timeout: 1000,  //响应数据类型,默认json  responseType: 'json',   //响应数据的编码规则,默认utf-8  responseEncoding: 'utf8',    //响应体的最大长度   maxContentLength: 2000,  // 请求体的最大长度  maxBodyLength: 2000,  //设置响应状态码为多少时是成功,调用resolve,否则调用reject失败  //默认是大于等于200,小于300  validateStatus: function (status) {    return status >= 200 && status < 300;   },

默认配置

可以设置全局默认配置,是为了避免多种重复配置在不同请求中重复,比如baseURL、timeout等,这里设置baseURL。

axios.defaults.baseURL = 'https://api.example.com';axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';

也可以在创建axios实例的时候自定义默认配置:

// 创建实例时配置默认值const instance = axios.create({  baseURL: 'https://api.example.com'});// 创建实例后修改默认值instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;

配置的优先顺序

配置会以一个优先顺序进行合并。这个顺序是:在 lib/defaults.js 找到的库的默认值,然后是实例的 defaults 属性,最后是请求的 config 参数。后者将优先于前者,示例如下:

// 使用由库提供的配置的默认值来创建实例// 此时超时配置的默认值是 `0`var instance = axios.create();// 覆写库的超时默认值// 现在,在超时前,所有请求都会等待 2.5 秒instance.defaults.timeout = 2500;// 为已知需要花费很长时间的请求覆写超时设置instance.get('/longRequest', {  timeout: 5000});

序列化为JSON

默认情况下,axios将JavaScript对象序列化为JSON。 要以application / x-www-form-urlencoded格式发送数据,您可以使用以下选项之一。

在浏览器中,您可以使用URLSearchParams API,如下所示:

const params = new URLSearchParams();params.append('param1', 'value1');params.append('param2', 'value2');axios.post('/foo', params);

或者使用另一种方式(ES6):

import qs from 'qs';const data = { 'bar': 123 };const options = {  method: 'POST',  headers: { 'content-type': 'application/x-www-form-urlencoded' },  data: qs.stringify(data),  url,};axios(options);

三、拦截器

在请求或响应被 then 或 catch 处理前拦截它们,自定义的axios实例也可添加拦截器,如下所示。

const instance = axios.create();instance.interceptors.request.use(function () {/*...*/});

请求拦截器

在发起请求之前,使用interceptors添加请求拦截器,如下:

// 添加请求拦截器axios.interceptors.request.use(function (config) {    // 在发送请求之前做些什么    return config;  }, function (error) {    // 对请求错误做些什么    return Promise.reject(error);  });

响应拦截器

在响应请求之前,使用interceptors添加响应拦截器,如下:

// 添加响应拦截器axios.interceptors.response.use(function (response) {    // 对响应数据做点什么    return response;  }, function (error) {    // 对响应错误做点什么    return Promise.reject(error);  });

取消拦截器

如果想要取消拦截器,如下:

const myInterceptor = axios.interceptors.request.use(function () {/*...*/});axios.interceptors.request.eject(myInterceptor);

错误处理

axios封装了对返回错误的处理,如下所示。

axios.get('/user/12345')  .catch(function (error) {    if (error.response) {      // The request was made and the server responded with a status code      // that falls out of the range of 2xx      console.log(error.response.data);      console.log(error.response.status);      console.log(error.response.headers);    } else if (error.request) {      // The request was made but no response was received      // `error.request` is an instance of XMLHttpRequest in the browser and an instance of      // http.ClientRequest in node.js      console.log(error.request);    } else {      // Something happened in setting up the request that triggered an Error      console.log('Error', error.message);    }    console.log(error.config);  });

四、取消请求

从 v0.22.0 开始,Axios 支持以 fetch API 方式—— AbortController 取消请求,CancelToken API被弃用。这里我们两种方法都介绍一下,使用过程中能用 AbortController 就尽量别用 CancelToken。

CancelToken

可以使用 CancelToken.source 工厂方法创建 cancel token:

let source = axios.CancelToken.source();axios.get('/users/12345',{                cancelToken: source.token            }).then(res=>{                console.log(res)            }).catch(err=>{                //取消请求后会执行该方法                console.log(err)            })//取消请求,参数可选,该参数信息会发送到请求的catch中source.cancel('取消后的信息');

当然,也可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token:

const CancelToken = axios.CancelToken;let cancel;axios.get('/user/12345', {  cancelToken: new CancelToken(function executor(c) {    // executor 函数接收一个 cancel 函数作为参数    cancel = c;  })});// cancel the requestcancel();

AbortController

使用AbortController方式取消请求如下:

const controller = new AbortController();axios.get('/foo/bar', {   signal: controller.signal}).then(function(response) {   //...});// 取消请求controller.abort()

五、axios封装

我们希望封装的axios工具类能够实现下面的功能:

  • 优化配置,设置默认配置项(responseType、跨域携带cookie、token、超时设置)

  • 统一设置请求头

  • 根据环境设置 baseURL

  • 通过 Axios 方法直接发起请求

  • 添加请求拦截器

  • 添加响应拦截器

  • 导出 Promise 对象

  • 封装 Post 方法,精简 post 请求方式

  • 封装 Get 方法,精简 get 请求方式

  • 请求成功,配置业务状态码

  • 全局的loading配置
    首先,我们在项目的src目录中新建一个request文件夹,然后在里面新建一个http.js和一个api.js文件。http.js文件用来封装我们的axios,api.js用来统一管理我们的接口。

    // 在http.js中引入axiosimport axios from 'axios'; // 引入axiosimport QS from 'qs'; // 引入qs模块,用来序列化post类型的数据,后面会提到// vant的toast提示框组件,大家可根据自己的ui组件更改。import { Toast } from 'vant';

环境切换

我们的项目环境可能有开发环境、测试环境和生产环境。我们通过node的环境变量来匹配我们的默认的接口url前缀。axios.defaults.baseURL可以设置axios的默认请求地址就不多说了,如下所示。

// 环境的切换if (process.env.NODE_ENV == 'development') {        axios.defaults.baseURL = 'https://www.baidu.com';} else if (process.env.NODE_ENV == 'debug') {        axios.defaults.baseURL = 'https://www.ceshi.com';} else if (process.env.NODE_ENV == 'production') {        axios.defaults.baseURL = 'https://www.production.com';}

请求拦截

我们在发送请求前可以进行一个请求的拦截,为什么要拦截呢,我们拦截请求是用来做什么的呢?比如,有些请求是需要用户登录之后才能访问的,或者post请求的时候,我们需要序列化我们提交的数据。这时候,我们可以在请求被发送之前进行一个拦截,从而进行我们想要的操作。

import store from '@/store/index';// 请求拦截器axios.interceptors.request.use(        config => {                // 每次发送请求之前判断vuex中是否存在token                // 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况        // 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断         const token = store.state.token;                token && (config.headers.Authorization = token);                return config;        },        error => {                return Promise.error(error);    })

这里说一下token,一般是在登录完成之后,将用户的token通过localStorage或者cookie存在本地,然后用户每次在进入页面的时候(即在main.js中),会首先从本地存储中读取token,如果token存在说明用户已经登陆过,则更新vuex中的token状态。然后,在每次请求接口的时候,都会在请求的header中携带token,后台人员就可以根据你携带的token来判断你的登录是否过期,如果没有携带,则说明没有登录过。这时候或许有些小伙伴会有疑问了,就是每个请求都携带token,那么要是一个页面不需要用户登录就可以访问的怎么办呢?其实,你前端的请求可以携带token,但是后台可以选择不接收啊。
接下来,我们看一下响应拦截器:

// 响应拦截器axios.interceptors.response.use(        response => {           // 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据             // 否则的话抛出错误        if (response.status === 200) {                        return Promise.resolve(response);                } else {                        return Promise.reject(response);                }        },        // 服务器状态码不是2开头的的情况    // 这里可以跟你们的后台开发人员协商好统一的错误状态码        // 然后根据返回的状态码进行一些操作,例如登录过期提示,错误提示等等    // 下面列举几个常见的操作,其他需求可自行扩展    error => {                    if (error.response.status) {                        switch (error.response.status) {                                // 401: 未登录                // 未登录则跳转登录页面,并携带当前页面的路径                // 在登录成功后返回当前页面,这一步需要在登录页操作。                                case 401:                                        router.replace({                                                path: '/login',                                                query: {                             redirect: router.currentRoute.fullPath                         }                    });                    break;            // 403 token过期            // 登录过期对用户进行提示            // 清除本地token和清空vuex中token对象            // 跳转登录页面                            case 403:                 Toast({                    message: '登录过期,请重新登录',                    duration: 1000,                    forbidClick: true                });                // 清除token                localStorage.removeItem('token');                store.commit('loginSuccess', null);                // 跳转登录页面,并将要浏览的页面fullPath传过去,登录成功后跳转需要访问的页面                 setTimeout(() => {                                            router.replace({                                                    path: '/login',                                                    query: {                             redirect: router.currentRoute.fullPath                         }                                            });                                    }, 1000);                                    break;             // 404请求不存在            case 404:                Toast({                    message: '网络请求不存在',                    duration: 1500,                    forbidClick: true                });                break;            // 其他错误,直接抛出错误提示            default:                Toast({                    message: error.response.data.message,                    duration: 1500,                    forbidClick: true                });        }        return Promise.reject(error.response);    }}    });

响应拦截器很好理解,就是服务器返回给我们的数据,我们在拿到之前可以对他进行一些处理。例如上面的思想:如果后台返回的状态码是200,则正常返回数据,否则的根据错误的状态码类型进行一些我们需要的错误,其实这里主要就是进行了错误的统一处理和没登录或登录过期后调整登录页的一个操作。

封装get方法和post方法

get方法

我们通过定义一个get函数,get函数有两个参数,第一个参数表示我们要请求的url地址,第二个参数是我们要携带的请求参数。get函数返回一个promise对象,当axios其请求成功时resolve服务器返回 值,请求失败时reject错误值,最后通过export抛出get函数,如下所示。

/** * get方法,对应get请求 * @param {String} url [请求的url地址] * @param {Object} params [请求时携带的参数] */export function get(url, params){        return new Promise((resolve, reject) =>{                axios.get(url, {                        params: params                }).then(res => {            resolve(res.data);        }).catch(err =>{            reject(err.data)            })    });}

post方法
原理同get基本一样,但是要注意的是,post方法必须要使用对提交从参数对象进行序列化的操作,所以这里我们通过node的qs模块来序列化我们的参数。这个很重要,如果没有序列化操作,后台是拿不到你提交的数据的。

/**  * post方法,对应post请求  * @param {String} url [请求的url地址]  * @param {Object} params [请求时携带的参数]  */export function post(url, params) {    return new Promise((resolve, reject) => {         axios.post(url, QS.stringify(params))        .then(res => {            resolve(res.data);        })        .catch(err =>{            reject(err.data)        })    });}

最后,看下axios封装后的代码:

/**axios封装 * 请求拦截、相应拦截、错误统一处理 */import axios from 'axios';import QS from 'qs';import { Toast } from 'vant';import store from '../store/index'// 环境的切换if (process.env.NODE_ENV == 'development') {        axios.defaults.baseURL = '/api';} else if (process.env.NODE_ENV == 'debug') {        axios.defaults.baseURL = '';} else if (process.env.NODE_ENV == 'production') {        axios.defaults.baseURL = 'http://api.123dailu.com/';}// 请求超时时间axios.defaults.timeout = 10000;// post请求头axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';// 请求拦截器axios.interceptors.request.use(        config => {        // 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的header都加上token,不用每次请求都手动添加了        // 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断        const token = store.state.token;                token && (config.headers.Authorization = token);                return config;        },        error => {                return Promise.error(error);        })// 响应拦截器axios.interceptors.response.use(        response => {                if (response.status === 200) {                        return Promise.resolve(response);                } else {                        return Promise.reject(response);                }        },    // 服务器状态码不是200的情况        error => {                if (error.response.status) {                        switch (error.response.status) {                                // 401: 未登录                                // 未登录则跳转登录页面,并携带当前页面的路径                                // 在登录成功后返回当前页面,这一步需要在登录页操作。                                case 401:                                        router.replace({                                                path: '/login',                                                query: { redirect: router.currentRoute.fullPath }                     });                    break;                // 403 token过期                                // 登录过期对用户进行提示                                // 清除本地token和清空vuex中token对象                                // 跳转登录页面                                case 403:                                         Toast({                                                message: '登录过期,请重新登录',                                                duration: 1000,                                                forbidClick: true                                        });                                        // 清除token                                        localStorage.removeItem('token');                                        store.commit('loginSuccess', null);                                        // 跳转登录页面,并将要浏览的页面fullPath传过去,登录成功后跳转需要访问的页面                    setTimeout(() => {                                                router.replace({                                                        path: '/login',                                                        query: {                                 redirect: router.currentRoute.fullPath                             }                                                });                                        }, 1000);                                        break;                 // 404请求不存在                                case 404:                                        Toast({                                                message: '网络请求不存在',                                                duration: 1500,                                                forbidClick: true                                        });                                    break;                                // 其他错误,直接抛出错误提示                                default:                                        Toast({                                                message: error.response.data.message,                                                duration: 1500,                                                forbidClick: true                                        });                        }                        return Promise.reject(error.response);                }           });/**  * get方法,对应get请求  * @param {String} url [请求的url地址]  * @param {Object} params [请求时携带的参数]  */export function get(url, params){        return new Promise((resolve, reject) =>{                axios.get(url, {                        params: params                })                .then(res => {                        resolve(res.data);                })                .catch(err => {                        reject(err.data)                })        });}/**  * post方法,对应post请求  * @param {String} url [请求的url地址]  * @param {Object} params [请求时携带的参数]  */export function post(url, params) {        return new Promise((resolve, reject) => {                 axios.post(url, QS.stringify(params))                .then(res => {                        resolve(res.data);                })                .catch(err => {                        reject(err.data)                })        });}

api统一管理

axios的封装基本就完成了,下面再简单说下api的统一管理。

/** * article模块接口列表 */import base from './base'; // 导入接口域名列表import axios from '@/utils/http'; // 导入http中创建的axios实例import qs from 'qs'; // 根据需求是否导入qs模块const article = {        // 新闻列表        articleList () {                return axios.get(`${base.sq}/topics`);        },        // 新闻详情,演示        articleDetail (id, params) {                return axios.get(`${base.sq}/topic/${id}`, {                        params: params                });        },    // post提交        login (params) {                return axios.post(`${base.sq}/accesstoken`, qs.stringify(params));        }    // 其他接口…………}export default article;

然后,就可以在页面中调用这些封装的api方法了。