趁着这段时间找工作正好总结一下之前在公司的多个项目。

首先是前端数据请求方面。数据请求是开发中非常重要的一部分,它涉及到与后端的交互, 且很容易写出重复的代码,需要对其有一个统一的管理。

抽离与分层

从项目整体出发,我们要做的第一步是抽离与分层。抽离代码能让我们在后期维护更加方便 ,同时也减少了重复代码。分层有助于我们将业务层和逻辑层隔离开,在版本迭代时会发挥 巨大的作用。

考虑这样一个问题:假如后端获取用户信息的API名称改掉了,而这个API又在多个页面 被调用(在壹诺微信端有四个入口,没有登录拦截),此时的维护成本很高。在这种情况下 ,我们可以将整个的数据请求从我们的View层拿出来,即在请求和页面之前加一 层service层:

// get user info
export async function getUserInfo(params, options) {
    return post('/user/info', params)
}

View里:

import { getUserInfo } from 'services.js'
getUserInfo().then((data) => {
    //这里对用户数据处理
})

我们可以看到,通过架设一层服务层,页面和请求就完全隔离开了,无论是对后期迭代还是 逻辑而言都是很好的方案。

拦截与控制

当我们将页面和请求隔离开后,需要进一步考虑服务层,即所谓的请求。公司的后端接口都 是统一要走网关的(防 CSRF),前端在请求接口时必须在header加上Authorization凭 证。

难不成要在所有的接口请求都配置一变吗?

这个时候就会想到将这部分相同的代码抽离出来,也就是对请求方法做一个封装:

//借款项目的封装

const Axios = function (defaultConfig) {
    return this.packageAxios(defaultConfig)
}
Axios.prototype = {
    packageAxios: function (options) {
        this.getBaseParams(options)

        return Eajax({
            method: this.method,
            url: this.url,
            data: this.data,
            withCredentials: true,
            timeout: 10000,
        })
            .then((res) => {
                // 请求完成关闭loading
                if (this.UIComponents.loading) {
                    Indicator.close()
                }
                // 未登录跳转登录页
                if (res.data.errCode == -1 && this.urlType === 1) {
                    localStorage.removeItem('token')
                    apiCloud.exitLogin({})
                    window.history.go(0)
                }

                return res
            })
            .catch((error) => {
                if (this.UIComponents.loading) {
                    Indicator.close()
                    error.message.indexOf('timeout') !== -1
                        ? Toast({ message: '请求超时', duration: 2000 })
                        : ''
                }
            })
    },
    getBaseParams: function (options) {
        options = options || {}
        this.method = options.method || 'post' // 方法
        this.url = options.url || process.env.BASE_URL + 'h5Interface' // 地址
        this.urlParams = options.urlParams || '' // 地址参数
        this.urlType = options.urlType || 0 // 判断url的类型来请求不同的接口
        this.callback = options.callback || undefined // 回调
        this.UIComponents = options.UIComponents || {} // 加载UI组件
        if (this.UIComponents.loading) {
            Indicator.open('加载中...')
        }
        this.cutRequestData(options)
        // this.judgeEncrypt(options)
    },
    // 切换请求头格式
    cutRequestData(options) {
        switch (this.urlType) {
            case 0:
                this.data = qs.stringify({
                    requestMsg: JSON.stringify({
                        header: {
                            transcode: options.data.transcode,
                        },
                        body: options.data.body,
                    }),
                })
                break
            case 1:
                this.data = options.data
                this.url = process.env.P2P_BASE_URL + this.urlParams
                break
        }
    },
}

const createInstance = (defaultConfig) => {
    return new Axios(defaultConfig)
}

export default createInstance

上面代码是借款项目的请求封装,当时接口域名有四五个,每个域名的后端写法都不同。针 对具体的业务做出不同的封装,不同的字段走不同的url甚至是不同的逻辑层。一个请求 涉及到了参数配置、异常处理、接口拦截等方面,在这种情况下我们可以借用axios的拦 截器来做文章。

// 请求之前的拦截器

axios.interceptors.request.use((config) => {
    let url =
        config.url.includes('gw.inuol.com') && !config.url.includes('sys/item')
    if (store.state.Authorization && url) {
        config.headers.Authorization = store.state.Authorization
    }
    return config
})

回到我们一开始说的Authorization里,我们可以在axios的请求拦截器里进行统一的配 置。而对接口返回的异常处理也可以在响应拦截器里操作:

axios.interceptors.response.use(
    function (response) {
        return response
    },
    function (error) {
        //
        // 服务器返回不是 2 开头的情况,会进入这个回调
        // 可以根据后端返回的状态码进行不同的操作
        const responseCode = error.response.status
        switch (responseCode) {
            // 401:未登录
            case 401:
                // 跳转登录页
                Toast('登录失效,请重新登录')
                router.replace({
                    path: '/login',
                })
                break
            // 404请求不存在
            case 404:
                Toast('网络请求不存在')
                break
            case 500:
                Toast('服务器报错')
                break
            case 502:
                Toast('服务器报错')
                break
            // 其他错误,直接抛出错误提示
            default:
                Toast('请求失败')
        }
        return Promise.reject(error)
    }
)

所谓的拦截器其实就是一个链式调用,明白这点后即使不用axios,我们也可以仿照其拦截 器原理自己封装一个请求的拦截器模式。当然最后还有一点是对于超时的处理,假如我们接 口请求的那一刻服务器恰好出问题,在 5s 之后服务器恢复正常,但我们前端只能手动再次 触法请求才能得知。有没有一种方法能做到连续请求呢?

同样是在拦截器里可以做文章:

axios.defaults.retry = 4
axios.defaults.retryDelay = 1000

axios.interceptors.response.use(undefined, function axiosRetryInterceptor(err) {
    var config = err.config
    // If config does not exist or the retry option is not set, reject
    if (!config || !config.retry) return Promise.reject(err)

    // Set the variable for keeping track of the retry count
    config.__retryCount = config.__retryCount || 0

    // Check if we've maxed out the total number of retries
    if (config.__retryCount >= config.retry) {
        // Reject with the error
        return Promise.reject(err)
    }

    // Increase the retry count
    config.__retryCount += 1

    // Create new promise to handle exponential backoff
    var backoff = new Promise(function (resolve) {
        setTimeout(function () {
            resolve()
        }, config.retryDelay || 1)
    })

    // Return the promise in which recalls axios to retry the request
    return backoff.then(function () {
        return axios(config)
    })
})

上面展示的一个请求超时后继续发送请求 3 次后才会抛出错误,这样的话对接口暂时故障 的容错率大大提高。

最后,程序设计无处不在,我们必须对代码的合理性和维护性有一定的把握,当然借款项目 的请求封装还有一定的问题,以及拦截器的整合等等。这些路由、请求方法、服务层的分层 也需要考虑清楚。