此文为万字长文详解从零搭建企业级 vue3 + vite2+ ts4 框架全过程衍生篇之一,虽然隔得有些久了,但我还是会坚持把它更完,敬请期待~
什么是抽象?
抽象是从众多的具体事物中,抽取共同的、本质的属性,舍弃个别的、非本质属性的过程。
就像人们常说的梵高的画作很抽象,是指画的内容晦涩难懂吗?显然不是,而是指梵高在画作中对艺术的表现力,丰富的感染力具备高度的提炼,让见闻者仿佛身临其境,能与作者感同身受。这种极具情感染力的情绪传递,才是梵高画作抽象真正的意义所在。
抽象的过程就类似于写一个公共组件,在面对不同的业务场景下既能够有公共部分的使用避免重复编码,也可以通过额外添加特定的属性以适配对应的业务功能。
使用过代码检查工具的小伙伴们应该知道,有一个衡量代码质量的标准就是“代码重复率”。减少编写重复代码就意味着需要对封装抽象有着较好的掌控能力,良好的封装意味着后续更好的维护能力。
本篇通过axios的封装来带你体会抽象的魅力吧!
前置处理
在我们想要对某个模块进行封装之前,先要明确两个内容:
- 为什么要封装?
- 封装后能达到的效果是什么?
为什么要封装axios?
可能会有很多人好奇:axios 这么强大了,既可以创建多个实例,又可以直接调用它的实例方法get
、post
等等去请求接口,为什么还要封装一层呢?是不是有些多此一举了?封装一层可以让我多写一篇文章(不是
我们通过简单的实际开发场景模拟来了解背后的真正原因吧:
首先我们前端的服务起的是4000端口,假设后台服务是8000端口,可以通过简单的代理进行访问:
server: {
port: 4000, // 设置服务启动端口号
cors: true, // 允许跨域
// 设置代理
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
secure: false,
rewrite: path => path.replace(/^/api/, '')
}
}
}
复制代码
当我们要调用后台定义的一个 get 接口/test/
,就可以通过在url前添加/api
正常访问了:
-
有一个后台接口
http://127.0.0.1:4000/test/
,请求后正常返回如下信息{ result: true, // 用于判断本次请求操作是否成功, data: 'result data' } 复制代码
前端如何获取并处理呢?
-
首先引入
Axios
import Axios from 'axios' 复制代码
-
然后创建一个 axios 实例
const instance = Axios.create({ // 指定后台的请求地址,通过添加前缀'/api'实现代理访问 baseURL: 'http://127.0.0.1:4000/api', }) 复制代码
-
然后直接调用
Axios
实例的get
方法就能正常调用接口了instance.get('/test/') 复制代码
-
前端接收成功后就可以在
.then
里处理数据了instance.get('/test/').then(res => { console.log(res) }) 复制代码
控制台打印下可以看到返回了很多参数:
这一步可以感知到,在实际开发中其实真正需要关注的参数只有
data
里的内容,其他参数都是我们不太需要关心的或者说不需要在每次返回的参数中进行处理的。 -
在日常使用场景中,不可避免地会出现接口挂掉的问题,比如常见的500(Internal Server Error),502(Bad Gateway)等等
AXios 的实例方法
get
、post
等返回的是个Promise
对象,我们可以通过.catch
进行捕获异常处理:instance.get('/test2/').then(res => { console.log(res, 'res') }).catch(err => { console.log(err, 'err') }) 复制代码
模拟请求一个不存在的接口
/test2/
,控制台打印如下:可以看到,报错信息会将完整的错误堆栈给抛出,对用户来说,这些都属于无效信息,唯一有效的是状态码404,所以需要用到拦截器对错误状态码处理成用户友好的反馈信息。
还有很多情况就不一一列举了。到现在,你应该知道为什么要对axios进行封装了吧,主要就是为了在开发中:
- 抽离公共配置,减少编写重复的代码逻辑
- 能够专注于业务代码处理,而不需要额外分心考虑接口报错、请求错误等问题
为了避免过度封装反而带来的低可维护性,我们仅主要针对以上两点来完成封装即可。
封装后能达到的效果
在使用 axios 做后台请求的时候,开发人员只需要考虑对正常请求的结果处理;对请求配置、错误请求、状态异常等功能做到无感知,真正做到专注于业务开发。 既然是对接口请求模块的封装,我们自然需要熟悉接口 API 的风格规范,比如最常使用的 RESTful API 风格: 本文也是基于RESTful 风格 API 进行对应处理RESTful 架构详解
RESTful 风格的 API具备与以下三个特征:
- 看 url 知道是进行什么处理的
- 看 http method 知道是干什么的
- 看 http status code 就知道返回结果是什么
通过请求方法和请求状态码的统一,能够保证我们的“有效封装”
功能搭建
创建一个 axios 实例:
import Axios, {
AxiosError,
AxiosResponse,
AxiosRequestConfig,
AxiosInstance
} from 'axios'
const BASE_URL = 'http://127.0.0.1:4000/api'
const TIME_OUT = 20 * 1000
const instance: AxiosInstance = Axios.create({
baseURL: BASE_URL, // 后台服务地址,根据实际情况调整
timeout: TIME_OUT // 设置请求最长时间
})
复制代码
请求携带Token
当向后台发送请求时,有时候需要前后端通过统一的 token 传递用于校验请求是否合法/避免跨域等等。
可以直接使用前置拦截器给请求头上添加相应的参数
instance.interceptors.request.use((config: AxiosRequestConfig) => {
config.headers && (config.headers['X-csrfToken'] = getToken())
return config
})
复制代码
获取token方法:
每个项目对应的获取token的方式不一样,需要和后台协同一致,这里给出一种使用 cookie 传递 token 的示例
const getToken = (): string => {
const DEFAULT_X_CSRFTOKEN = 'NOT_PROVIDED'
const { cookie } = document
if (!cookie) return DEFAULT_X_CSRFTOKEN
// 后台传递给前端的 cookie 名称可以在模板文件中使用 window 接收
const key = window.CSRF_COOKIE_NAME || 'csrftoken'
const patten = new RegExp(`^${key}=[\S]*`, 'g')
const value = cookie.split(';')?.find((item) => patten.test(item.trim()))
if (!value) return DEFAULT_X_CSRFTOKEN
return decodeURIComponent(value.split('=')[1] || DEFAULT_X_CSRFTOKEN)
}
复制代码
错误处理
首先定义一个错误返回的接口,分别是状态码、返回结果和错误信息
interface IResponseError {
code: number, // 状态码
result: boolean, // 返回结果
message: string // 错误信息
}
复制代码
接着再定义一个状态码处理函数errorCodeHandler
和错误信息处理函数errorHandler
,状态码对应信息参考HTTP 状态代码列表
这里将状态码与对应的错误信息提取为单独的函数,保证函数功能单一原则的同时带来更舒适的观感。
const errorCodeHandler = (code, data): string | undefined => {
const message = data?.message
const msgMap = {
400: message || '400 error 请求无效',
401: '401 error 登录失效,请重新登录!',
403: '403 error 对不起,你没有访问权限!',
404: '404 Not Found 请检查请求路径是否正确!',
500: message || '500 error 后台错误,请联系开发人员!',
502: '502 error 平台环境异常',
504: '504 error 网关超时,请重试!'
}
return msgMap[code]
}
const errorHandler = (error): IResponseError => {
const { data, status, statusText } = error.response
const msg = errorMsgHandler(status, data) ||
`${status} error ${data ? data.message : statusText}`
alert(msg) // 可以引用各自的UI框架中全局消息提示的组件,此处使用alert简单模拟报错提示
return {
code: status,
result: false,
message: msg
}
}
复制代码
我们对常见的一些错误进行精准匹配并返回一些自定义的提示信息,对用户体验更好,毕竟用户对http状态码可能并没有我们这些开发人员那样熟悉。对不常见的错误返回直接返回原生信息即可。
使用后置拦截器拦截错误请求
instance.interceptors.response.use(
(response: AxiosResponse) => {
const { data, status } = response
const { result, message } = data
if (result) return data
// 1、对于一些请求正常,但后台处理失败的内容进行拦截,返回对应错误信息
alert(message || '请求异常,请刷新重试')
return {
code: status,
message: message || '请求异常,请刷新重试',
result: false
}
},
(error: AxiosError) => {
// 2、超出 2xx 范围的状态码都会触发该函数
return error.response ? errorHandle(error) : Promise.reject(error)
}
)
复制代码
后台请求报错一般有三种:
一种是环境因素:平台不稳定,网络错误,请求超时等等,我们通过 errorHandle 对特定状态码做特定信息返回处理;
一种是因为后台代码错误导致的,一般会返回错误状态码 500(内部服务器错误),也是通过 errorHandle 中对应处理了;
还有一种是请求正常,但是未能得到预期的返回结果,此时,状态码是返回 200 的,而由后台控制返回字段的 result 为 false,对应的是上文第一个拦截处理。该部分的处理需要和各自后台开发约定好,具有一定的特异性。请以实际开发场景为准
取消请求
在实际应用场景中,取消请求通常会用在:
- 当在不同组件/页面之间切换时,可以对上一个组件/页面未完成的请求进行取消。
- 当用户重复点击提交按钮时,当第一个请求尚未完成时,取消重复的请求,避免多次提交
对于第二个场景,应该对表单提交的交互完善,在第一次点击提交后,禁用提交按钮或者给提交按钮添加loading效果知道接口请求完成,这是当前大多数UI组件默认做到的事情,防止重复提交的事件被触发,而不是取消重复请求。
因此,我们只需要做到对第一个场景的处理即可。
从 v0.22.0
开始,Axios 支持以 fetch API 方式—— AbortController
取消请求:
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// 取消请求
controller.abort()
复制代码
而大家常见的以CancelToken
取消请求的方式其实已经被官方废弃了(因为它的实现是是基于被撤销的一个提案 cancelable promises proposal),虽然不影响使用,但与时俱进才是技术人需要的坚持!
使用前先查看下浏览器的支持情况,有备无患"AbortController" | Can I use... Support tables for HTML5, CSS3, etc
可以看到,稍微高一点版本的浏览器基本都是支持的,那就可以放心使用了。
页面切换之前,取消未完成的所有请求,定义一个变量controllers
用于存放所有请求的controller
let controllers: AbortController[] = []
复制代码
在前置拦截器中,每当有请求发起,添加一个controller
// 前置拦截器(发起请求之前的拦截)
instance.interceptors.request.use((config: AxiosRequestConfig) => {
const controller = new AbortController()
config.signal = controller.signal
controllers.push(controller)
return config
})
复制代码
定义并导出一个取消方法,遍历所有的controller
并调用abort()
方法
export const cancelRequest = () => {
controllers.forEach(controller => {
controller.abort()
})
controllers = []
}
复制代码
在路由守卫中使用,每当页面跳转之前,调用cancelRequest
,取消所有未完成的请求
import { cancelRequest } from '@/utils/axios'
router.beforeEach((to, from, next) => {
cancelRequest()
next()
})
复制代码
实现效果:
请求方法导出
将常用的请求方法做个简单的封装并导出
const ajaxGet = (url: string, params?: any): Promise<AxiosResponse> => instance.get(url, { params })
const ajaxDelete = (url: string, params?: any): Promise<AxiosResponse> => instance.delete(url, { params })
const ajaxPost = (url: string, params: any, config?: AxiosRequestConfig): Promise<AxiosResponse> => instance.post(url, params, config)
const ajaxPut = (url: string, params: any, config?: AxiosRequestConfig): Promise<AxiosResponse> => instance.put(url, params, config)
const ajaxPatch = (url: string, params: any, config?: AxiosRequestConfig): Promise<AxiosResponse> => instance.patch(url, params, config)
export {
ajaxGet,
ajaxDelete,
ajaxPost,
ajaxPut,
ajaxPatch
}
复制代码
使用方式
import { ajaxGet } from '@/utils/axios'
// 1. 链式调用
ajaxGet('/test1').then(res => {
console.log(res, ' res')
}).catch((e) => {
console.log(e, 'error')
})
// 2. try await catch
try {
const res = await ajaxGet('/test1')
console.log(res, 'res2')
} catch (e) {
console.log(e, 'error2')
}
复制代码
俩种使用方式其实没有什么优劣之分,注意在一个项目中的统一即可
写在最后
抽象虽好,但要避免过度,基于以上能力的一个axios 模块其实除了某些极特殊的场景外其实已经足够用了。 诸如一些请求缓存,进度条等可以根据项目情况适度添加。
源码仓库地址
参考
axios/axios: Promise based HTTP client for the browser and node.js