Vue + Element UI 实现权限管理系统 前端篇(三):工具模块封装

封装 axios 模块

封装背景

使用 axios 发起一个请求是比较简单的事情,但是axios没有进行封装复用,项目越大的话会造成代码冗(rǒng )余,维护会越来越难。

所以在此二次封装,是项目中各个组件能够复用,让代码更容易维护。

封装要点

  • 统一 url 配置
  • 统一 api 请求
  • request(请求)拦截器,例如:带上token等,设置请求头
  • response(响应)拦截器,例如:统一错误处理,页面重定向等
  • 根据需要,结合 Vuex 做全局的 loading 动画,或者错误处理
  • 将 axios 封装成 Vue 插件使用

文件结构

在 src 下新建一个 http 文件夹,用来存放 http 交互 api 代码。

 

config.js: axios 默认配置,包含基础路径等信息。

axios.js: 二次封装 axios 模板,包含拦截器等信息。

interface.js: 请求接口汇总模块,汇聚模块 API。

index.js: 将 axios 封装成插件,按插件方式引入。

安装 js-cookie

在axios.js中会用 Cookie 获取 token

npm i js-cookie --save-dev

config.js

// axios 默认配置,包含基础路径等消息。
export default {
    method: 'get',
    // 基础 url 前缀
    baseURL: 'http://localhost:8080/',
    // 请求头信息
    headers: {
        'Content-Type': 'application/json;charset=UTF-8'
    },
    // 参数
    data: {},
    // 设置超时时间
    timeout: 1000,
    // 携带凭证,是否允许跨域属性
    withCredentials: true,
    // 返回数据类型
    responseType: 'json'
}

axios.js

 

// 二次封装 axios 模块,包含拦截器等信息
import axios from 'axios';
import config from './config';
import qs from 'qs';
import Cookies from 'js-cookie';
import router from '@/router';

// 使用vuex做全局loading时使用
// import store from '@/store'

export default function $axios(options) {   // 模板默认导出一个 Promise 实例
    return new Promise ((resolve, reject) => {
        const instance = axios.create({     // axios.create() 是实例化
            baseURL: config.baseURL,
            headers: {},
            transformResponse: [function (data) {   // transformResponse 在传递给 then/catch 前,允许修改响应数据,用的不多?
            }]
        });
        
        // request 拦截器
        instance.interceptors.request.use((config) => {
            let token = Cookies.get('token');
            // 1.请求开始的时候可以结合 vuex 开启全屏 loading 动画
            // console.log(store.state.loading)
            // console.log('准备发送请求...')
            
            // 2.带上 token
            if (token) {
                config.headers.accessToken = token
            } else {
                // 重定向到登入页面
                router.push('/login')
            };

            // 3.根据请求方法,序列化转来的参数,根据后端需求是否序列化
            if (config.method === 'post') {
                if (config.data.__proto__ === FormData.prototype
                    || config.url.endsWith('path')
                    || config.url.endsWith('mark')
                    || config.url.endsWith('patchs')
                ) {

                } else {
                    config.data = qs.stringify(config.data)
                }
            }
            return config       // if() {} 大括号里面的执行完,继续执行大括号后面的时候不加 else {}
        },
        error => {
            // 请求错误时
            console.log('request:', error);
            // 1.判断请求超时
            if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') !== -1) {
                console.log('timeout请求超时')
                // return service.request(originalRequest);  // 再重复请求一次
            };
            // 2.需要重定向到错误页面
            const errorInfo = error.response;
            console.log(errorInfo);
            if (errorInfo) {
                error = errorInfo.data  // 页面那边 catch 的时候就能拿到详细的错误信息,看最下边的Promise.reject
                const errorStatus = errorInfo.status;   // 404 403 500 ...
                router.push({
                    path: `/error/${errorStatus}`
                })
            }
            return Promise.reject(error);   // 在调用的那边可以拿到(catch)你想返回的错误信息
        }
        );

        // response 拦截器
        instance.interceptors.response.use(
            (response) => {
                let data;
                // IE9时response.data是undefined,因此需要使用response.request.responseText(Stringify后的字符串)
                // 在此处我以为说 IE9 需要,现在不需要了,就直接 let data = response.data 导致各种undefined
                if (response.data == undefined) {
                  data = JSON.parse(response.request.responseText)
                } else {
                  data = response.data
                }

                // 根据返回的 code 值来做不同处理
                switch (data.rc) {  
                    case 1:
                        console.log(data.desc)
                        break;
                    case 0:
                        store.commit('changeState')
                        // console.log('登入成功')
                    default:
                }
                // 若不是正确的返回code,且已经登入,就抛出错误
                // const err = new Error(data.desc)
                // err.data = data
                // err.response = response
                // throw err
                return data
        },
        (err) => {
            if (err && err.response) {
                switch (err.response.status) {
                    case 400:
                        err.message = '请求错误'
                        break;
                    case 401:
                        err.message = '未授权,请登入'
                        break;
                    case 403:
                        err.message = '拒绝访问'
                        break;
                    case 404:
                        err.message = `请求地址出错:${err.response.config.url}`
                        break;
                    case 408:
                        err.message = '请求超时'
                        break;
                    case 500:
                        err.message = '服务器内部错误'
                        break;
                    case 501:
                        err.message = '服务未实现'
                        break;
                    case 502:
                        err.message = '网关错误'
                        break;
                    case 503:
                        err.message = '服务不可用'
                        break;
                    case 504:
                        err.message = '网关超时'
                        break;
                    case 505:
                        err.message = 'HTTP版本不受支持'
                        break;
                    default:
                }
            }
            console.error(err)
            return Promise.reject(err)  // 返回接口返回的错误信息
        }
        );

        // 请求处理
        instance(options).then(res => {
            resolve(res);
            return false;
        }).catch(error => {
            reject(error);
        })

    })
}

 

interface.js

import axios from './axios';

/*
 * 将所有接口统一起来方便维护
 * 如果项目很大可以将 url 独立成文件,接口分成不同的模块
 */

// 单独导出
export const login = () => {
    return axios({
        url: '/login',
        method: 'get'
    })
};

export const getUser = () => {
    return axios({
        url: '/user',
        method: 'get'
    })
}

export const getMenu = data => {
    return axios({
        url: '/menu',
        method: 'post',
        data
    })
}

// 默认全部导出
export default {
    login,
    getUser,
    getMenu
}

index.js

 

// 将 axios 封装成插件, 挂载到Vue上,然后就可以按插件方式引入

// 导入所有接口
import apis from './interface';

const install = Vue => {        // install:安装
    if (install.installed) {
        return;
    } else {
        install.installed = true;
    };

    Object.defineProperties(Vue.prototype, {
        // 此处挂载在 Vue 原型的 $api 对象上
        $api: {
            get() {
                return apis;
            }
        }
    })
}

export default install;

 

 

 

代码实例

1.引入插件

在main.js 中以 vue 插件的形式引入 封装后的axios,这样在其它地方就可以通过 this.$api 调用相关接口了。

2.编写接口

在 interface.js 中添加 login 接口。

 

 3.调用接口

在登入界面 Login.vue 中,添加一个登入按钮,点击处理函数通过 axios 调用 login 接口返回数据。

成果返回后,将 token 放入 Cookie 并转跳到主页。

<template>
    <div class="page">
        <h2>Login Page</h2>
        <el-button type="primary" @click="login()">登入</el-button>
    </div>
</template>

<script>
    import mock from '@/mock/mock.js';
    import Cookies from 'js-cookie';
    import router from '@/router'

    export default {
        name: 'Login',
        methods: {
            login() {
                this.$api.login().then(function(res) {
                    alert(res.data.token)
                    Cookies.set('token', res.data.token)    // 放置 token 到 Cookie
                    router.push('/')    // 登入成功,转跳到主页
                }).catch(function(res) {
                    alert(res);
                });
            }
        }
    }
</script>

4.mock 接口

在mock.js 中添加一个 login 接口进行拦截, 返回一个token

Mock.mock('http://localhost:8080/login', {  
    data: {                    // 之前的数据现在也得用 data{}包起来
        'token': '4344323121398',
        'rc': 1
        // 其它数据
    }
});

启动测试

 

 


 

封装 mock 模块

为了统一可以统一管理和集中控制数据模拟接口,我们对 mock 模块进行了封装,可以方便的定制模拟接口的统一开关和个体开关。

文件结构

 

 

index.js: 模拟接口模块聚合文件

login.js: 登入相关接口模拟

user.js: 用户相关接口模拟

menu.js: 菜单相关接口模拟

index.js

// 聚合文件,总
import Mock from 'mockjs';
import * as login from './modules/login';
import * as user from './modules/user';
import * as menu from './modules/menu';

// 1.关闭/开启[业务模块]拦截,通过调用fnCreate方法[isOpen参数]设置。
// 2.关闭/开启[业务模块中的某个请求]拦截,通过函数返回对象中的[isOpen属性]设置。
fnCreate(login, true);
fnCreate(user, true);
fnCreate(menu, true);

/**
 * 创建mock模拟数据
 * @param {*} mod 模块
 * @param {*} isOpen 是否开启?
 */
function fnCreate (mod, isOpen = true) {
    if (isOpen) {
        for (var key in mod) {      // key = "login" key才是模块里面的函数, mod = Module {__esModule: true, Symbol(Symbol.toStringTag): "Module"} 这是一个模块。
            // key 似乎跟 res 是一样的? 但是好像不关联。这个 key 关联的是 mod[key] 里面的。
            // 只将 上方的 key 改成 res 会提示 已声明但未取值。
            ((res) => {
                    /** res?
                     *data: {msg: "success", code: 0, data: {…}}
                     *type: "get"
                     *url: "http://localhost:8080/login"
                     *__proto__: Object
                    */
                if (res.isOpen !== false) {
                    // 格式:Mock.mock( rurl, rtype, function( options ) )
                    //options:指向本次请求的 Ajax 选项集,含有 url、type 和 body 三个属性,参见 XMLHttpRequest 规范。
                    // url用正则写,这样get请求传参时,也能拦截数据了
                    Mock.mock(new RegExp(res.url), res.type, (opts) => {    // opts = {url: "http://localhost:8080/login", type: "GET", body: null, data: null}
                        opts['data'] = opts.body ? JSON.parse(opts.body) : null;
                        delete opts.body;
                        console.log('\n');
                        console.log('%cmock拦截, 请求: ', 'color:blue', opts);
                        console.log('%cmock拦截, 响应: ', 'color:blue', res.data);
                        return res.data; // 这是在 对应模块中获取的 data
                    })
                }
            }) (mod[key] () || {})
        }
    }
}

login.js

// 登入接口
export function login () {
    return {
        // isOpen: false,
        url: 'http://localhost:8080/login',
        type: 'get',
        data: {
            'msg': 'success',
            'code': 0,
            'data': {
                'token': '4344323121398'
                // 其它数据
            }
        }
    }
}

menu.js

// 获取用户信息
export function getUser () {
    return {
        // isOpen: false,
        url: 'http://localhost:8080/user',
        type: 'get',
        data: {
            'msg': 'success',
            'code': 0,
            'data': {
                'id': '@increment',
                'name': '@name',
                'email': '@email',
                'age|10-20': 12
                // 其它数据
            }
        }
    }
}

menu.js

// 获取菜单信息
export function getMenu () {
    return {
        // isOpen: false,
        url: 'http://localhost:8080/menu',
        type: 'get',
        data: {
            'msg': 'success',
            'code': 0,
            'data': {
                'id': '@increment',
                'name': 'menu',
                'order|10-20': 12
                // 其它数据
            }
        }
    }
}

然后将之前的 mock 引入的地方都改成引入 '@/mock/index.js'

 

 

posted @ 2020-07-31 07:43  Mock777  阅读(426)  评论(0编辑  收藏  举报