前端工程化(3):在项目中优雅的设计基于Axios的请求方案

9,940 阅读5分钟

其实axios已经提供了很多很强大的api,我们在实际使用中直接调用就可以,但是每个团队每个项目调用axios的姿势不一样,特别是在一个大型的团队项目中,与后端交互逻辑迥异、配置复杂、地址繁多,所以一个风格统一化、配置灵活化、管理集中化的请求方案必不可少。

第一步、接口管理

首先在项目中创建一个名为api的文件夹,用来统一管理与后台交互的逻辑。

分类

api文件夹下分别创建文件夹,按类型对接口地址进行分类(这点很有必要,特别是在大型项目中,按类型进行分类能让你快速定位到接口写入的位置):

在每个文件夹下再创建index.js文件,写入属于这个类型的所有接口地址:

cms

...,
export const CMS_DATA = '/cms/renderData'

member

...,
export const MEMBER_INFO = '/rights/memberInfo'

抛出

api文件夹下创建index.js文件:

把所有类型的接口统一暴露出去:

// cms信息
export * from './cms'
// 会员信息
export * from './member'

第二步、缓存机制

api文件夹下创建cache.js文件:

基于axios开发了一套缓存机制,基于请求地址url和请求参数params给每个请求结果进行缓存,同时可以给每个请求结果设置缓存有限期和缓存模式:

export default class Cache {
    constructor(axios, config = {}) {
        this.axios = axios
        this.caches = []
        if (!this.axios) {
            throw new Error('请传入axios实例')
        }
        this.config = config
        this.defaultConfig = {
            cache: false,
            expire: 100 * 1000
        }
        this.CancelToken = this.axios.CancelToken
        this.init()
    }

    init() {
        this.requestInterceptor(this.config.requestInterceptorFn)
        this.responseInterceptor(this.config.responseInterceptorFn)
        window.onbeforeunload = () => {
            this.mapStorage()
        }
    }

    requestInterceptor(callback) {
        this.axios.interceptors.request.use(async config => {
            let newConfig = callback && (await callback(config))
            config = newConfig || config
            let { url, data, params, cacheMode, cache = this.defaultConfig.cache, expire = this.defaultConfig.expire } = config
            if (cache === true) {
                let getKey = data ? `${url}?cacheParams=${data}` : `${url}?cacheParams=${params}`
                let obj = this.getStorage(cacheMode, getKey)
                // 判断缓存数据是否存在
                if (obj) {
                    let curTime = this.getExpireTime()
                    let source = this.CancelToken.source()
                    config.cancelToken = source.token
                    // 判断缓存数据是否存在,存在的话是否过期,如果没过期就停止请求返回缓存
                    if (curTime - obj.expire < expire) {
                        source.cancel(obj)
                    } else {
                        this.removeStorage(cacheMode, url)
                    }
                }
            } else {
                this.clearStorage(url)
            }
            return config
        }, error => {
            return Promise.reject(error)
        })
    }

    responseInterceptor(callback) {
        this.axios.interceptors.response.use(async response => {
            let newResponse = callback && (await callback(response))
            response = newResponse || response
            // the http request error, do not store the result, direct return result
            if (response.status !== 200 || response.data.ret || !response.data.success) {
                return response.data
            }
            /*
             * `data` is the data to be sent as the request body, only applicable for request methods 'PUT', 'POST', and 'PATCH'
             * `params` are the URL parameters to be sent with the request, can be applicable for request methods 'GET'
             */
            let { url, cache, cacheMode, data, params } = response.config
            if (cache === true) {
                let obj = {
                    expire: this.getExpireTime(),
                    params,
                    data,
                    result: response.data
                }
                let setKey = data ? `${url}?cacheParams=${data}` : `${url}?cacheParams=${params}`
                this.caches.push(setKey)
                this.setStorage(cacheMode, setKey, obj)
            }
            return response.data
        }, error => {
            let newError = callback && (await callback(newError))
            error = newError || error
            // 返回缓存数据
            if (this.axios.isCancel(error)) {
                return Promise.resolve(error.message.result)
            }
            return Promise.reject(error)
        })
    }

    // 设置缓存
    setStorage(mode = 'sessionStorage', key, cache) {
        window[mode].setItem(key, JSON.stringify(cache))
    }

    // 获取缓存
    getStorage(mode = 'sessionStorage', key) {
        let data = window[mode].getItem(key)
        return JSON.parse(data)
    }

    // 清除缓存
    removeStorage(mode = 'sessionStorage', key) {
        window[mode].removeItem(key)
    }

    // 设置过期时间
    getExpireTime() {
        return new Date().getTime()
    }

    // 清空缓存
    clearStorage(key) {
        if (window.localStorage.getItem(key)) {
            window.localStorage.removeItem(key)
        } else {
            window.sessionStorage.removeItem(key)
        }
    }

    // 清空没用到的缓存
    mapStorage() {
        let length = window.localStorage.length
        if (length) {
            for (let i = 0; i < length; i++) {
                let key = window.localStorage.key(i)
                if (!this.caches.includes(key) && key.includes('?cacheParams=')) {
                    window.localStorage.removeItem(key)
                }
            }
        }
    }
}

由于缓存机制是基于url+params来进行缓存的,在有效期内再次访问相同的url+params,浏览器就会直接读取缓存不会再发送请求。如果有效期过了或者请求地址变了或者请求参数变了,浏览器则会绕过缓存直接发送请求。(支持分页缓存的场景)

第三步、配置Axios

api文件夹下创建config.js文件,用来存放axios的一些预配置信息:

全局配置

import axios from 'axios'
import Cache from './cache'

axios.defaults.withCredentials = true
axios.defaults.baseURL = process.env.NODE_ENV === 'production' ? '' : '/api'
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
...

拦截器

由于设计的缓存机制依赖拦截器机制,为了避免额外的拦截器配置,在设计缓存机制时留有拦截器配置入口,如下:

new Cache(axios, {
    requestInterceptorFn: config => {
        // 自定义请求拦截器
        /* */
        // 需要用Promise将config返回
        return Promise.resolve(config)
    },
    responseInterceptorFn: response => {
        // 自定义响应拦截器,可统一返回的数据格式也可拦截错误
        /* */
        // 需要用Promise将response返回
        return Promise.resolve(response)
    }
})

export default axios

如果没有采用缓存机制的话,可以直接配置拦截器,如下:

axios.interceptors.request.use(config => {
    // Do something before request is sent
    return config;
}, error => {
    // Do something with request error
    return Promise.reject(error);
});

// Add a response interceptor
axios.interceptors.response.use(response => {
    // Do something with response data
    return response || {};
}, error => {
    // Do something with response error
    return Promise.reject(null);
});

export default axios

第四步、请求封装

api文件夹下创建base.js文件:

主要封装几个常用的方法,这里就列举常用的getpost方法:

import axios from './config'
import qs from 'qs'

export const post = (url, data, extend = {isJson: true, cache: false}) => {
    let defaultConfig = {
        url,
        method: 'POST',
        data: extend.isJson ? data : qs.stringify(data) // 通过isJson来确定传参格式是json还是formData,默认是json
    }
    let config = {...defaultConfig, ...extend}
    return axios(config).then(res => {
        // 可以统一返回的数据格式
        return res
    }, err => {
        return Promise.reject(err)
    })
}

export const get = (url, data, extend = {cache: false}) => {
    let defaultConfig = {
        url,
        method: 'GET',
        params: data
    }
    let config = {...defaultConfig, ...extend}
    return axios(config).then(res => {
        // 可以统一返回的数据格式
        return res
    }, err => {
        return Promise.reject(err)
    })
}

第五步、全局注册

api文件夹下创建install.js文件:

把封装好的方法注册到全局:

import { get, post } from 'api/base'

export const install = function(Vue, config = {}) {
    Vue.prototype.$_get = get
    Vue.prototype.$_post = post
}

main.js中写入:

import { install as Axios } from './api/install'
Vue.use(Axios)

第六步、调用

调用时只需要引入想要调用的接口地址就行:

import { CMS_DATA, MEMBER_INFO } from 'api'

methods: {
    receiveCMS() {
        // post参数形式为formData
        this.$_post(CMS_DATA, data, { jsJson: false }).then(res => {
            console.log(res)
        }),
    },
    receiveMember() {
        // 开启缓存,设置缓存时间为一个小时,缓存的模式为localStorage
        this.$_get(MEMBER_INFO, data, { cache: true, expires: 1000 * 60 * 60, cacheMode: 'localStorage' }).then(res => {
            console.log(res)
        }),
    }
}

缓存默认是关闭的,需要手动开启,如果开启的话,缓存有效期默认是10分钟,缓存的模式默认为sessionStorage

最后

整个设计好的方案就完成了:

当然,随着项目的复杂度,这个方案还有很多可以优化的地方,比如全局loading,因为个人感觉适合移动端不适合pc端,所以在这就不举例出来,有需要的同学可以在第四步进行封装。也比如全局的配置,可以在第三步进行补充。

这个方案说不上最好,但目前是我总结出来最优雅的方式了,也欢迎大佬们提出宝贵的意见。