Code前端首页关于Code前端联系我们

.env.development

terry 2小时前 阅读数 33 #Vue

Vue3项目里,企业级的Axios要怎么封装才不会踩坑?

这几年做Vue3项目踩了不少Axios封装的坑——要么是全局配置没统一,每次发请求都要手动改baseURL、超时时间;要么是拦截器逻辑混乱,token过期处理跳登录重复触发;要么是错误提示太笼统,后端给的错误码都没法精准反馈给用户,后来跟着团队重新梳理了好几遍,还参考了不少国内头部互联网公司的前端工程化实践,总算摸出一套相对通用、扩展性强的企业级封装方案,下面就从最基础的为什么要封装开始,一步步讲具体怎么做,最后还会给几个常见场景的优化技巧。

为什么明明Axios本身就挺好用,还要花时间封装?

很多刚接触Vue3的朋友可能会觉得:“直接import axios from 'axios'用不就行了?何必多此一举写个js/ts文件?”其实这个问题在小项目里(比如只有三四个页面、十几个接口)确实存在,但只要项目稍微一复杂,或者换了环境(开发/测试/生产),不封装的问题就会立刻暴露出来。

维护成本高,比如你们公司的后端突然换了接口域名的前缀,从/api/v1改成了/api/v2,如果每个请求里都硬写了前缀,你就得去翻几百个文件一个一个改,改完还得仔细检查有没有漏的,想想都头疼,要是封装过,只需要改一行baseURL的配置就行。

功能复用难,很多请求都需要加统一的配置,比如超时时间、请求头里的token、跨域带不带cookie;响应回来也需要统一处理,比如token过期跳登录、403权限不足弹窗提示、网络错误给友好的文案,如果每次写请求都把这些逻辑抄一遍,不仅代码冗余,还容易出错——比如这次忘了加token,下次忘了处理404。

扩展性差,以后要是想加个请求取消功能、接口防抖节流、错误上报到Sentry这些高级功能,要是没封装,根本没办法全局统一加,难道又要去改所有接口吗?

所以说,不管项目大小,哪怕是练手的Vue3项目,也建议简单封装一下Axios,养成好习惯。

企业级Axios封装的核心思路是什么?

核心思路其实就是一句话:高内聚、低耦合、可扩展。

高内聚指的是把Axios的所有核心功能——全局配置、请求拦截、响应拦截、错误处理、请求取消这些——都放在一个单独的模块里,不要分散到各个组件或者工具类里。

低耦合指的是这个封装好的模块要尽量独立,不要依赖Vue3的响应式API、组件实例这些非必要的东西,以后哪怕换个前端框架(比如React),也只要改一点点适配代码就能用。

可扩展指的是要留好接口,比如可以自定义不同业务的请求头、自定义错误处理逻辑、自定义响应数据的解析方式,不要把所有东西都写死在里面。

从零开始,一步步写Vue3企业级Axios封装

这次我会用TypeScript来写,因为TypeScript的类型提示能帮我们减少很多低级错误,现在的企业级Vue3项目基本都是用TS的,如果你还不会TS,也没关系,把类型注解删掉就能直接在JS项目里用。

第一步:创建封装目录结构

在Vue3项目的src目录下创建一个utils文件夹,然后在里面建一个request文件夹,里面放四个文件:

  1. index.ts:主入口文件,负责导出封装好的axios实例和常用的请求方法
  2. config.ts:存放全局配置,比如不同环境的baseURL、超时时间、请求头默认值
  3. interceptors.ts:存放请求拦截器和响应拦截器的逻辑
  4. types.ts:存放TypeScript的类型定义,比如请求配置的扩展类型、响应数据的通用类型

这样的目录结构很清晰,以后维护起来也方便——改配置去config.ts,改拦截逻辑去interceptors.ts,加类型去types.ts。

第二步:写全局配置文件config.ts

全局配置文件的核心是根据不同的环境(开发/测试/生产)设置不同的baseURL,很多Vue3项目现在都是用Vite搭建的,Vite可以通过import.meta.env来读取环境变量,我们可以利用这个特性。

在项目根目录下创建三个环境变量文件:

  1. .env.development:开发环境的变量
  2. .env.staging:测试环境的变量
  3. .env.production:生产环境的变量

然后在这三个文件里分别写:

VITE_API_TIMEOUT=10000
# .env.staging
VITE_API_BASE_URL=/staging-api
VITE_API_TIMEOUT=15000
# .env.production
VITE_API_BASE_URL=https://api.yourcompany.com
VITE_API_TIMEOUT=20000

注意,Vite的环境变量必须以VITE_开头,不然在代码里读不到。

接下来写config.ts: 这里除了设置baseURL和超时时间,还可以设置一些默认的请求头,比如Content-Type: application/json,还有跨域带不带cookie的withCredentials(如果你的接口需要带cookie登录的话,就设为true),还可以加一个自定义的showLoading字段,用来控制是否显示全局的加载动画,这个字段是Axios原生没有的,需要后面在类型定义里扩展。

第三步:写类型定义文件types.ts

因为我们加了自定义的showLoading、showError、ignoreCancel这些字段,所以需要扩展Axios的RequestConfig类型,后端返回的响应数据一般都是固定的格式,

{
  "code": 200,
  "message": "成功",
  "data": { ... }
}

所以我们也可以定义一个通用的响应数据类型,这样在调用接口的时候,TypeScript就能自动提示data的类型了。

扩展RequestConfig的时候,要注意Axios是有默认类型的,所以我们需要用interface来继承它的InternalAxiosRequestConfig接口,然后加上我们自己的字段。

第四步:写请求取消的工具类

请求取消这个功能在企业级项目里很常用,比如用户在搜索框输入的时候,每输入一个字符就会发一次请求,但如果用户输入得很快,上一次请求还没回来,下一次就发出去了,这时候我们应该取消上一次请求,只保留最后一次,还有用户快速切换页面的时候,上一个页面的请求还没回来,我们也应该取消,避免报错。

Axios提供了两种请求取消的方式:一种是用CancelToken(已经废弃,但还能用),另一种是用AbortController(推荐,因为是Web标准API),这次我们用AbortController来实现。

我们可以写一个单例的PendingRequest类,用来管理所有正在发起的请求,这个类里面有一个Map,key是请求的唯一标识(比如用method + url + JSON.stringify(data)拼接),value是对应的AbortController实例。

然后我们需要两个方法:

  1. addPending:在请求发起前,把这个请求加入到Map里,如果Map里已经有这个请求了,就先取消上一次的
  2. removePending:在请求完成后(不管成功还是失败),把这个请求从Map里移除

第五步:写拦截器文件interceptors.ts

拦截器是Axios封装的核心,分为请求拦截器和响应拦截器。

请求拦截器

请求拦截器主要做三件事:

  1. 处理请求取消:调用PendingRequest类的addPending方法,把当前请求加入到Map里
  2. 处理请求头:比如从localStorage或者pinia/vuex里取出token,加到请求头的Authorization字段里
  3. 处理全局加载动画:如果请求配置里的showLoading是true(可以设为默认值),就显示全局加载动画

这里要注意token的获取方式,不要直接在拦截器里操作localStorage,最好是封装一个auth工具类,里面有getToken、setToken、removeToken这些方法,这样以后如果换了token的存储方式(比如从localStorage改成sessionStorage或者cookie),只需要改这个工具类就行。

全局加载动画的实现也不要直接耦合在拦截器里,最好是用一个全局的事件总线或者pinia/vuex来管理,比如拦截器里触发一个showLoading的事件,然后在App.vue里监听这个事件,显示Element Plus或者Ant Design Vue的加载组件。

响应拦截器

响应拦截器也主要做三件事:

  1. 处理请求取消:调用PendingRequest类的removePending方法,把当前请求从Map里移除
  2. 处理全局加载动画:不管成功还是失败,都隐藏全局加载动画
  3. 处理响应数据和错误:
    • 如果响应成功(status是200),就看后端返回的code是不是成功的code(比如200、0),如果是,就直接返回data;如果不是,就根据code做对应的处理,比如401跳登录,403弹窗提示权限不足,然后抛出错误
    • 如果响应失败(status不是200),比如404、500、网络超时,就根据status做对应的处理,给友好的错误提示,然后抛出错误

这里要注意token过期的处理,比如后端返回401的时候,我们需要:

  1. 清除本地的token
  2. 取消所有正在发起的请求(避免重复跳登录)
  3. 跳转到登录页

跳登录的时候也要注意,如果用户已经在登录页了,就不要重复跳了,如果项目有多个登录入口(比如管理员登录和普通用户登录),也要根据之前的路由或者请求的接口前缀来判断跳哪个登录页。

还有错误提示的问题,不要每次都用alert,太丑了,最好用UI组件库的Message或者Notification组件,而且可以加一个showError的配置项,默认为true,如果有些接口的错误不需要全局提示(比如登录接口的密码错误,应该自己在组件里处理),就可以设为false。

第六步:写主入口文件index.ts

主入口文件主要做两件事:

  1. 创建Axios实例:用axios.create方法,传入config.ts里的全局配置
  2. 注册拦截器:把interceptors.ts里的请求拦截器和响应拦截器注册到这个实例上
  3. 导出常用的请求方法:比如get、post、put、delete、patch,这些方法可以封装一下,不用每次都传method参数

还可以导出这个Axios实例本身,方便以后有特殊需求的接口直接用原生的Axios方法。

常见场景的优化技巧

接口防抖节流

刚才我们讲了请求取消,用来解决用户快速输入的问题,但有时候我们也需要用防抖节流来减少请求的次数,比如搜索接口,我们可以用防抖,等用户停止输入500ms之后再发请求;比如滚动加载接口,我们可以用节流,每隔1s最多发一次请求。

这时候我们可以封装一个高阶函数,把请求方法作为参数传进去,返回一个防抖节流后的方法,然后在主入口文件里导出这个高阶函数,或者直接在组件里用。

多域名接口

有些项目可能需要调用多个不同域名的接口,比如一个是自己公司的后端接口,一个是第三方的地图接口,这时候我们可以创建多个Axios实例,每个实例对应一个域名,然后分别注册不同的拦截器(比如第三方接口不需要加自己公司的token)。

比如在config.ts里可以加一个第三方地图的配置,然后在index.ts里创建两个实例:service(自己公司的)和mapService(第三方的),然后分别导出。

错误上报

企业级项目一般都需要错误上报,把接口的错误上报到Sentry或者自己公司的监控平台,这时候我们可以在响应拦截器的错误处理里,加一段上报错误的代码,比如把请求的url、method、参数、响应的status、code、message、时间戳这些信息都上报上去。

同样,错误上报的逻辑也不要直接耦合在拦截器里,最好封装一个errorReport工具类,里面有reportApiError方法,这样以后如果换了监控平台,只需要改这个工具类就行。

文件上传下载

文件上传下载也是企业级项目里常用的功能,Axios本身就支持,但可以封装一下,让它更方便,比如文件上传的时候,需要设置Content-Type: multipart/form-data,但其实不用手动设置,只要传一个FormData对象,Axios会自动设置,比如文件下载的时候,需要设置responseType: blob,然后根据响应头里的Content-Disposition来获取文件名,然后创建一个a标签点击下载。

我们可以在主入口文件里导出uploadFile和downloadFile这两个方法,封装好这些逻辑。

写在最后

就是我总结的Vue3企业级Axios封装方案,这套方案我在好几个项目里都用过,效果都不错,维护起来也很方便,这套方案不是万能的,你可以根据自己项目的实际情况做调整,比如不需要请求取消功能,就可以把PendingRequest类删掉;不需要TypeScript,就把类型注解删掉。

最后再提醒大家一句:封装Axios的时候,不要过度封装,不要把所有东西都写死在里面,要留好扩展的接口,不然以后改起来会很麻烦。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门