10分钟,用axios封装一个解放生产力的http请求类

主要内容

  1. 实际项目中api模块的目录结构及使用方式

  2. 统一处理请求和响应

    • 处理后端通用错误,如登录过期、访问权限不足、系统维护等
    • 处理后端接口级错误,如新建文章功能,会有标题不合法、未选择文章分类等
  3. 用面向对象的方式开发项目的http功能

    • 好处是利于维护和扩展,下面会有使用场景和示例

目录结构及使用方式

API模块的目录结构

|-- api
    |-- modules -------------------- 模块文件夹,包含项目中关于请求的所有模块
        |-- auth.js ---------------- 模块名,如auth一般会包含login、logout等接口的api方法
        |-- xxx.js
        |-- xxx.js
    |-- http.js -------------------- http请求方法,在模块方法中被调用
    |-- index.js ------------------- 导出所有封装好的api方法

目录结构及使用方式

假设我们有一个请求文章详情的接口,后端在接口文档中提供以下信息

  • 地址:http://xx.com/api/article/detail
  • 参数:
    参数名 类型 说明
    id int 文章的id
  • 接口请求成功时返回文章详情
  • 接口参数错误时,返回如下内容
        {
            "isSuccess": false,
            "errorCode": "INVALID_ARTICLE_ID",
        }
    
    错误码 说明
    ARTICLE_ID_INVALID 无效的文章ID
    TOKEN_INVALID 无效的用户TOKEN

使用方式

  1. api/modules/ 下新建 article.js 文件

  2. api/index.js 中引入并导出 article 模块

    // 授权
    import auth from './modules/auth';
    
    // 文章
    import article from './modules/article';
    
    export default {
        auth,
        article,
    };
    
  3. article.js 文件中新建 获取文章详情 的方法

    import CommonHttp from '../http'
    const commonHttp = new CommonHttp()
    
    export default {
        getArticleDetail(params) {
            return commonHttp.http({
                url: '/api/article/detail',
                params: params,
                errorCodes: {
                // 是否要自己处理错误,设为true时,将不会弹出通用错误提示框
                dealSelf: true
                // INVALID_ID 为后台返回的错误码,可以在后面定义说明,若未定义说明,则会弹出错误码
                ARTICLE_ID_INVALID: '文章ID好像不太对呦!'
                },
            })
        },
    }
    
  4. 在需要使用该接口的地方引入 getArticleDetail 方法

    import api from '@/api'
    
    api.auth.getArticleDetail(params).then((res) => {
        // do something
    })
    

封装http类

  • 下面我们会定义一个CommonHttp的类,并为类定义一个名为http的实例方法供外部调用

由于代码量较大,建议您从http方法开始看

// http.js

import { Message } from 'element-ui'
import axios from 'axios'

/**
 * 错误提示
 */
const errorTip = (msg) => {
    Message.error(msg || '后端未返回错误码')
}
/**
 * 重新登录
 */
function resetLogin() {
    // 这里写跳转到登录页的方法
}


class CommonHttp {
    constructor() {
        this.url = ''
        this.params = null
        this.method = ''
        this.errorCodes = null
        this.responseAdapter = null

        this.requestHeaders = ''

        this.withCredentials = false
        this.headerContentType = 'application/json; charset=utf-8'
    }

    /**
    * 生成请求头
    */
    createRequestHeaders() {
        this.requestHeaders = {
            'Content-Type': this.headerContentType,
        }
        if (token) {
            requestHeaders.Authorization = localStorage.getItem('token')
        }
    }

    /**
     * 生成请求参数
     */
    createRequestParams() {
        const params = {}
        this.params && Object.keys(this.params).forEach((objKey) => {
            const val = this.params[objKey]
            // 剔除 undefined 、null和空字符串(是否需要剔除需要和后端沟通)
            if (val !== null && val !== '' && val !== undefined) {
                params[objKey] = val
            }
        })
        this.params = params
    }

    requestInterceptor() {
        const { method, url } = this

        // 如果未在调用http时指定请求方式,则从环境变量中读取
        const requestMethod = method === null ? env.REQUEST_METHOD : method

        // 组装请求配置,具体可见axios文档
        this.requestConfig = {
            url,
            headers: this.requestHeaders,
            method: requestMethod,
            timeout: 1000 * 10,
            withCredentials: this.withCredentials,
            baseURL: env.API_LOCATION,
        }

        if (requestMethod === 'GET') {
            this.requestConfig.params = this.params
        }

        if (requestMethod === 'POST') {
            this.requestConfig.data = this.params
        }
    }
    /**
     * 处理响应错误
     */
    handleError(reject, resultData) {
        // errorCodes 为调用http方法时传入的错误码映射对象
        const { errorCodes } = this
        if (resultData.errorCode === 'TOKEN_INVALID') {
            // 后端同事规定 TOKEN_INVALID 为TOKEN过期,需要重新登录
            resetLogin()
        } else {
            const errorCode = resultData.errorCode

            // 如果在传入errorCodes映射表中找不到,那么就直接显示返回的errorCode
            const errorText = errorCodes[errorCode] || errorCode

            // dealSelf 为调用http方法时传入,值为true,则不使用通用处理方式
            errorCodes.dealSelf || errorTip(errorText)
        }

        // 最后要把返回的错误抛出去,让外界能用catch捕捉到
        reject(resultData)
    }

    /**
     * 响应拦截器
     */
    responseInterceptor(resolve, reject, result) {
        // responseAdapter 为调用http方式时传入的响应适配器
        // 用于在后端返回的json不符合要求时,可以使用此方法重构json
        const { responseAdapter } = this

        // 返回的数据
        const resultData = result.data

        // 后端同事规定,只要接口正常,就不会返回200以外的问题
        if (result.status === 200) {
            if (resultData.isSuccess) {
                // 当isSuccess为true时,表示成功,则返回对应的内容
                resolve(responseAdapter(resultData))
            } else {
                // 当isSuccess为false时,则表示请求出现了问题
                // 我们需要根据返回的错误码做出不同的处理
                this.handleError(reject, resultData)
            }
        } else {
            errorTip(`未知错误 ${result}`)
        }
    }

    initConfig() {
        // 构造请求头
        this.createRequestHeaders()

        // 构造请求参数
        this.createRequestParams()

        // 组装构造好的请求头、请求参数、请求配置放到 this.requestConfig 里
        this.requestInterceptor()
    }

    // 发送请求
    request() {
        return new Promise((resolve, reject) => {
            axios(this.requestConfig).then((result) => {
                // 如果请求成功,则统一处理TOKEN过期、错误码映射、结果返回等问题
                this.responseInterceptor(resolve, reject, result)
            }).catch((error) => {
                // 后端规定,凡是200以外的状态码全部是服务端问题,前端不需要处理
                errorTip('服务端未响应,请检查网络或稍后重试')
                reject(error)
            })
        })
    }

    http({ url, params, method = null, errorCodes = {}, responseAdapter = data => data }) {
        // 参数传入后会放到实例属性里,方便其他方法调用
        this.url = url
        this.params = params
        this.method = method
        this.errorCodes = errorCodes
        this.responseAdapter = responseAdapter

        // 初始化请求配置
        this.initConfig()

        // 发送请求
        return this.request()
    }
}

export default CommonHttp

为什么要用面向对象来写?

  • 比如说我们在开发中,需要提交多媒体内容,那么我们就可以继承CommonHttp类来实现一个新的类

    import CommonHttp from './http'
    
    class HttpFormMultipart extends CommonHttp {
        constructor() {
            super()
            this.headerContentType = 'multipart/form-data'
        }
    
        createRequestParams() {
            const formData = new FormData()
            Object.keys(this.params).forEach((objKey) => {
                formData.append(objKey, this.params[objKey])
            })
            this.params = formData
        }
    }
    
    export default HttpFormMultipart
    
  • 在使用时,只需要实例化 HttpFormMultipart 类就可以了

    import HttpFormMultipart from '../HttpFormMultipart'
    const httpFormMultipart = new HttpFormMultipart()
    
    modifyUser(params) {
        return httpFormMultipart.http({
            url: '/user/avatar',
            params,
        })
    }
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352