Fork me on GitHub

在Vue中应该如何封装Axios 管理API接口

之前项目一直是api接口直接写在项目中的,非常乱,不易统一维护和管理,特意学习了其它前辈如何管理api,保存下来。

一、axios的封装

Vue\React中多使用axios库做数据请求,如果还对axios不了解的,可以移步axios文档

安装

npm install axios; // 安装axios

引入

一般我会在项目的src目录中,新建一个request文件夹,然后在里面新建一个http.js和一个api.js文件。http.js文件用来封装我们的axios,api.js用来统一管理我们的接口。

// 在http.js中引入axios
import axios from 'axios'; // 引入axios
import 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';
}

设置请求超时

通过axios.defaults.timeout设置默认的请求超时时间。例如超过了10s,就会告知用户当前请求超时,请刷新等。

axios.defaults.timeout = 10000;

post请求头的设置

post请求的时候,我们需要加上一个请求头,所以可以在这里进行一个默认的设置,即设置post的请求头为application/x-www-form-urlencoded;charset=UTF-8

axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';

请求拦截

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

请求拦截

// 先导入vuex,因为我们要使用到里面的状态对象
// vuex的路径根据自己的路径去写
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);    
})

响应的拦截

// 响应拦截器
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,则正常返回数据,否则的根据错误的状态码类型进行一些我们需要的错误,其实这里主要就是进行了错误的统一处理和没登录或登录过期后调整登录页的一个操作。

要注意的是,上面的Toast()方法,是我引入的vant库中的toast轻提示组件,你根据你的ui库,对应使用你的一个提示组件。

封装get方法和post方法

我们常用的ajax请求方法有get、post、put等方法,相信小伙伴都不会陌生。axios对应的也有很多类似的方法,不清楚的可以看下文档。但是为了简化我们的代码,我们还是要对其进行一个简单的封装。下面我们主要封装两个方法: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模块来序列化我们的参数。这个很重要,如果没有序列化操作,后台是拿不到你提交的数据的。这就是文章开头我们import QS from '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.get()方法和axios.post()在提交数据时参数的书写方式还是有区别的。区别就是,get的第二个参数是一个{},然后这个对象的params属性值是一个参数对象的。而post的第二个参数就是一个参数对象。两者略微的区别要留意哦!

api统一管理

整齐的api就像电路板一样,即使再复杂也能很清晰整个线路。上面说了,我们会新建一个api.js,然后在这个文件中存放我们所有的api接口。考虑到模块化、多人开发、处理接口域名有多个情况等情况。

这里这里呢新建了一个api文件夹,里面有一个index.js和一个baseUrl.js,以及多个根据模块划分的接口js文件。index.js是一个api的出口,baseUrl.js管理接口域名,其他js则用来管理各个模块的接口。

先放index.js代码:

//  api/index.js
/**
 * api接口的统一出口
 */
// 文章模块接口
import article from "./article";
// 登录模块接口
import login from "./login";
// user模块接口
import user from "./user";
// message模块接口
import message from "./message";

// 导出接口
export default {
  article,
  login,
  user,
  message
};

index.js是一个api接口的出口,在组件里调用api的时候引入这个文件即可,这样就可以把api接口根据功能划分为多个模块,利于多人协作开发,比如一个人只负责一个模块的开发等,还能方便每个模块中接口的命名哦。

baseUrl.js:

//  api/baseUrl.js
/**
 * 接口域名的管理
 */
const base = {
  // 测试接口
  dev: "https://www.xxxx1.com/api",
  bd: "http://www.xxxxx2.com/api"
};

export default base;


通过base.js来管理我们的接口域名,不管有多少个都可以通过这里进行接口的定义。即使修改起来,也是很方便的。

最后就是接口模块的说明,例如上面的article.js:

/**
 * article模块接口列表
 */
import base from "./baseUrl"; // 导入接口域名列表
import axios from "./http"; // 导入http中创建的axios实例

const article = {
  // 获取新闻列表数据
  articleList(params) {
    return axios.get(`${base.dev}/topics`, {
      params: params
    });
  },
  // 获取新闻详情
  articleDetail(id) {
    return axios.get(`${base.dev}/topic/${id}`);
  }
};

export default article;

  1. 通过直接引入我们封装好的axios实例,然后定义接口、调用axios实例并返回,可以更灵活的使用axios,比如你可以对post请求时提交的数据进行一个qs序列化的处理等。
  2. 请求的配置更灵活,你可以针对某个需求进行一个不同的配置。关于配置的优先级,axios文档说的很清楚,这个顺序是:在 lib/defaults.js 找到的库的默认值,然后是实例的 defaults 属性,最后是请求的 config 参数。后者将优先于前者。
  3. restful风格的接口,也可以通过这种方式灵活的设置api接口地址。

其他的api接口,就在api.js中继续往下面扩展就可以了。友情提示,为每个接口写好注释哦!!!

api接口管理的一个好处就是,我们把api统一集中起来,如果后期需要修改接口,我们就直接在api.js中找到对应的修改就好了,而不用去每一个页面查找我们的接口然后再修改会很麻烦。关键是,万一修改的量比较大,就规格gg了。还有就是如果直接在我们的业务代码修改接口,一不小心还容易动到我们的业务代码造成不必要的麻烦。

最后,为了方便api的调用,我们需要将其挂载到vue的原型上。在main.js中:

import Vue from 'vue'
import App from './App'
import router from './router' // 导入路由文件
import store from './store' // 导入vuex文件
import api from './api' // 导入api接口

Vue.prototype.$api = api; // 将api挂载到vue的原型上

然后我们可以在页面中这样调用接口,eg:

methods: {    
    onLoad(id) {      
        this.$api.article.articleDetail(id, {        
            api: 123      
        }).then(res=> {
            // 执行某些操作      
        })    
    }  
}

好了,最后把http.js中axios封装的优化代码奉上。

// http.js
/**
 * axios封装
 * 请求拦截、响应拦截、错误统一处理
 */
import axios from 'axios';
// 用来序列化post类型的数据
import QS from 'qs';
import router from '../router';
import store from '../store/index';
import { Toast } from 'vant';

/** 
 * 提示函数 
 * 禁止点击蒙层、显示一秒后关闭
 */
const tip = msg => {    
    Toast({        
        message: msg,        
        duration: 1000,        
        forbidClick: true    
    });
}

/** 
 * 跳转登录页
 * 携带当前页面路由,以期在登录页面完成登录后返回当前页面
 */
const toLogin = () => {
    router.replace({
        path: '/login',        
        query: {
            redirect: router.currentRoute.fullPath
        }
    });
}

/** 
 * 请求失败后的错误统一处理 
 * @param {Number} status 请求失败的状态码
 */
const errorHandle = (status, other) => {
    // 状态码判断
    switch (status) {
        // 401: 未登录状态,跳转登录页
        case 401:
            toLogin();
            break;
        // 403 token过期
        // 清除token并跳转登录页
        case 403:
            tip('登录过期,请重新登录');
            localStorage.removeItem('token');
            store.commit('loginSuccess', null);
            setTimeout(() => {
                toLogin();
            }, 1000);
            break;
        // 404请求不存在
        case 404:
            tip('请求的资源不存在'); 
            break;
        default:
            console.log(other);   
        }}


// 创建axios实例
const service = axios.create({
  // 设置超时时间,单位毫秒
  timeout: 12 * 1000
});
// 设置post请求头
service.defaults.headers.post['Content-Type'] =
  'application/x-www-form-urlencoded;charset=UTF-8';
// 请求重试次数
service.defaults.retry = 3;
// 请求重试时间间隔,单位毫秒
service.defaults.retryDelay = 1000;
// 是否重试
service.defaults.shouldRetry = true;

/** 
 * 请求拦截器 
 * 每次请求前,如果存在token则在请求头中携带token 
 */ 
service.interceptors.request.use(    
    config => {        
        // 登录流程控制中,根据本地是否存在token判断用户的登录情况        
        // 但是即使token存在,也有可能token是过期的,所以在每次的请求头中携带token        
        // 后台根据携带的token判断用户的登录情况,并返回给我们对应的状态码        
        // 而后我们可以在响应拦截器中,根据状态码进行一些统一的操作。        
        const token = store.state.token;        
        token && (config.headers.Authorization = token);        
        return config;    
    },    
    error => Promise.error(error))

// 响应拦截器
service.interceptors.response.use(    
    // 请求成功
    res => res.status === 200 ? Promise.resolve(res) : Promise.reject(res),    
    // 请求失败
    error => {
        const { response } = error;
        if (response) {
            // 请求已发出,但是不在2xx的范围 
            errorHandle(response.status, response.data.message);
            return Promise.reject(response);
        } else {
            // 处理断网的情况
            // eg:请求超时或断网时,更新state的network状态
            // network状态在app.vue中控制着一个全局的断网提示组件的显示隐藏
            // 关于断网组件中的刷新重新获取数据,会在断网组件中说明
            if (!window.navigator.onLine) {
               store.commit('changeNetwork', false);
            } else {
                return Promise.reject(error);
            }
        }
    });

export default service;

完整目录结构大概是这样的:


├── src                   // 生产目录
│   ├── assets            // css js 和图片资源
│   └── api               // axio请求数据目录
│        ├── baseUrl.js            // 管理接口域名文件
│        ├── http.js               // 封装axios文件
│        ├── index.js              // api接口的统一出口
│        ├── article.js            // article模块接口列表文件
│        └── ...                   // 其它模块接口列表文件

记录其它项目封装

/**
 * 封装axios文件
 */

import axios from "axios";
import _m from "@vendor/MTool.min";
import { getAuthorization } from "@utils/authorityUtils";
import { statusMessage } from "@utils/util";
import NProgress from "nprogress";
import { message } from "ant-design-vue";
import { apiConfig, devConfig } from "@config/system";

// 配置项
const CONFIG = apiConfig;
// 请求分类
// const METHOD = { ...CONFIG.method };

// 设置超时时间,单位毫秒
const service = axios.create({
  // 设置超时时间,单位毫秒
  timeout: CONFIG.timeout,
});

// post请求头
// service.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8";
service.defaults.headers.post["Content-Type"] = "application/json";
// 请求重试次数
service.defaults.retry = CONFIG.retry;
// 请求重试时间间隔,单位毫秒
service.defaults.retryDelay = CONFIG.retryDelay;
// 是否重试
service.defaults.shouldRetry = CONFIG.shouldRetry;


// request拦截器
service.interceptors.request.use(
  (config) => {
    // 进度条
    if (!NProgress.isStarted()) {
      NProgress.start();
    }

    // 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的header都加上token,不用每次请求都手动添加了
    // 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断
    const token = getAuthorization();
    // Bearer 
    token && (config.headers.Authorization = `Bearer ${token}`);

    return config;
  },
  (error) => {
    NProgress.done();
    error.response = { ...statusMessage(error.response.status)};
    message.error(error.response.message);
    return Promise.reject(error);
  }
);

//  respone拦截器
service.interceptors.response.use(
  (response) => {
    NProgress.done();
    // 统一处理状态
    return Promise.resolve(response);
  },
  // 处理处理
  (error) => {
    NProgress.done();
    error.response = { ...statusMessage(error.response.status) };
    message.error(error.response.message);
    return Promise.reject(error.response);
  }
);

const $get = async (url, params) => {
  if (params) {
    params = _m.trimFields(params);
  }
  let { data } = await service.get(url, {
    params,
  });
  return data;
};

const $delete = async (url, params) => {
  if(params){
    params = _m.trimFields(params);
  }
  let { data } = await service.delete(url, {
    params,
  });
  return data;
};

const $post = async (url, params, config) => {
  if (params) {
    params = _m.trimFields(params);
  }
  if (config?.headers["content-type"].trim() === "application/x-www-form-urlencoded") {
    params = _m.convertObj2Str(params);
  }
  let { data } = await service.post(url, params, config);
  return data;
};

const $put = async (url, params, config) => {
  if (params) {
    params = _m.trimFields(params);
  }
  if (config?.headers["content-type"].trim() === "application/x-www-form-urlencoded") {
    params = _m.convertObj2Str(params);
  }
  let { data } = await service.put(url, params, config);
  return data;
};

const $upload = async (url, formData) => {
  let { data } = await service.post(url, formData, { headers: { 'Content-Type': 'multipart/form-data' } });
  return data;
}
export {
  $get,
  $delete,
  $post,
  $put,
  $upload
};


参考:
vue中Axios的封装和API接口的管理

posted @ 2019-10-20 19:55  较瘦  阅读(257)  评论(0编辑  收藏  举报
知识点文章整理