前端工程化(3):在專案中優雅的設計基於Axios的請求方案

HulkShen發表於2019-03-28

其實axios已經提供了很多很強大的api,我們在實際使用中直接呼叫就可以,但是每個團隊每個專案呼叫axios的姿勢不一樣,特別是在一個大型的團隊專案中,與後端互動邏輯迥異、配置複雜、地址繁多,所以一個風格統一化、配置靈活化、管理集中化的請求方案必不可少。

第一步、介面管理

首先在專案中建立一個名為api的資料夾,用來統一管理與後臺互動的邏輯。

分類

api資料夾下分別建立資料夾,按型別對介面地址進行分類(這點很有必要,特別是在大型專案中,按型別進行分類能讓你快速定位到介面寫入的位置):

前端工程化(3):在專案中優雅的設計基於Axios的請求方案
在每個資料夾下再建立index.js檔案,寫入屬於這個型別的所有介面地址:

cms

...,
export const CMS_DATA = '/cms/renderData'
複製程式碼

member

...,
export const MEMBER_INFO = '/rights/memberInfo'
複製程式碼

丟擲

api資料夾下建立index.js檔案:

前端工程化(3):在專案中優雅的設計基於Axios的請求方案

把所有型別的介面統一暴露出去:

// cms資訊
export * from './cms'
// 會員資訊
export * from './member'
複製程式碼

第二步、快取機制

api資料夾下建立cache.js檔案:

前端工程化(3):在專案中優雅的設計基於Axios的請求方案

基於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 => {
            // 返回快取資料
            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的一些預配置資訊:

前端工程化(3):在專案中優雅的設計基於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(function (config) {
    // Do something before request is sent
    return config;
}, function (error) {
    // Do something with request error
    return Promise.reject(error);
});

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

export default axios
複製程式碼

第四步、請求封裝

api資料夾下建立base.js檔案:

前端工程化(3):在專案中優雅的設計基於Axios的請求方案

主要封裝幾個常用的方法,這裡就列舉常用的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 Promise.resolve(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 Promise.resolve(res)
    }, err => {
        return Promise.reject(err)
    })
}
複製程式碼

第五步、全域性註冊

api資料夾下建立install.js檔案:

前端工程化(3):在專案中優雅的設計基於Axios的請求方案

把封裝好的方法註冊到全域性:

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

最後

整個設計好的方案就完成了:

前端工程化(3):在專案中優雅的設計基於Axios的請求方案
當然,隨著專案的複雜度,這個方案還有很多可以優化的地方,比如全域性loading,因為個人感覺適合移動端不適合pc端,所以在這就不舉例出來,有需要的同學可以在第四步進行封裝。也比如全域性的配置,可以在第三步進行補充。

這個方案說不上最好,但目前是我總結出來最優雅的方式了,也歡迎大佬們提出寶貴的意見。

相關文章