每次接手的项目都是别人封装好的东西,时间久了,我们也就只是一个代码搬运工,工作五年,依旧不知道axios 是咋封装的,想想都挺对不起自己的,难道真的35岁以后,只能送外卖了吗?
为了35岁以后,还能有口饭吃,自己造的轮子,还是自己造起来吧,看别人代码挺难的,但是我就喜欢从最简单入手,一次性搞定Axios的封装。
本人测试案例不依赖任何框架,就是简单的html,js,css,目的是为了要大家从根本上认识最原始的js。为什么这么做呢,原因是我在面试过程中发现很多应聘者,抛开框架就不会搞js了,它对整个前端的认识是狭隘的,片面的,或许通过背面试题他们都能答出一些皮毛,但是一旦深入,就会死翘翘。对问题的深入理解决定了咱们的工资高低是不是?再者,如果你无法理解最原始的html,js, css,那么你如何能够全面掌握vue,react呢?
上传下载封装参看上一篇# 二次封装 Axios 下载文件和上传文件
Axios封装三部曲:
- 创建axios实例
- 拦截请求
- 拦截响应
- 导出方法
基础代码如下:
不依赖框架,看看最原始的html和js咋用axios,在此基础上我相信,你会对axios及其他相关知识有新的认识,毕竟框架下的东西,把很多难点都遮盖了。
创建一个项目:axios-test
cd axios-test
npm init -y
npm i axios -S
创建几个文件,如下:
为啥要启动服务呢,就是要前端开发者自己看看,从接口到前端,其实没有什么奥秘。自己动手丰衣足食呗!
1.服务端:
2.html
3.index.js
4.axios.js
测试
1.启动服务执行: node ./server/index.js,如果你没有安装express, 你就安装下,然后再启动。
2.启动前端有多种方法:
1. 如果你集成了 vite 就直接执行 vite 命令就好了
2. 如果没有集成任何打包工具,那就直接打开 html 文件,右键点击 open with live server ,如果没有这个选项大概是因为你的 VScode 没有安装 live-server 插件,安装下就好了,具体如图所示:
3. 安装下 http-server 执行命令: npx http-server -p 5666
测试结果如下:
axios的代码如下:use() 方法的入参是两个函数,一个处理拦截成功,一个处理拦截失败。成功了,就把接受到的,处理后的配置项return出去,失败了就把错误用promise包裹下return出去。
import axios from '../../node_modules/axios/dist/esm/axios.js'; //直接用import axios from 'axios' 浏览器无法解析,需要esmodule模块
// 创建 axios 实例
const instance = axios.create({
baseURL: ' http://127.0.0.1:3002/api',
timeout: 10000,
timeoutErrorMessage: '请求超时,请稍后重试',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// 请求拦截器
const requestConfig = (config) => {
return config;
} //所有请求拦截的东西全部放这里
instance.interceptors.request.use(requestConfig, (error) => Promise.reject(error))
// 响应拦截器
const responseData = (data) => {
return data;
} //所有响应拦截的东西全部放这里
instance.interceptors.response.use(responseData, (error) => Promise.reject(error))
export default {
get(url,params,options,){
return instance.get(url, { params, ...options })
},
post(url, params, options){
return instance.post(url, params, options)
}
}
接下来,我们做的所有的事情都是在 axios.js 里面扩展。 比如:在axios里面添加防抖,节流,loading,错误展示,跳转页面,token验证,文件的上传,下载等等!上一篇文章( 二次封装 Axios 下载文件和上传文件)已经对文件的上传和下载封装做了全面的解释,本篇文件不再解释。
1.错误拦截
咱们在请求的时候,错误状态码会有很多,最常见的就是500和404,他们每一个都代表不一样的意思,所以需要我们从接受到的数据里面,把他们分离出来,给出明确的提示。以免每个接口报错,都要处理一次,麻烦!
调试记得用VScode的调试,不要只会console.log了,还有记得修改服务端接口:
带着大家调试下看看,点击vscode的debugger模式,创建launch.json文件,然后将端口改成前端页面的端口,如下:(如果你不会,请移步《# 面试官问:在日常的开发中,你是如何调试代码的?》)
点击绿色箭头就开始进入debugger模式,我们启动下看看
debugger调试写代码明显比console.log的速度快很多。这样错误的封装就结束了。
2.防抖
防抖的基础是啥?就是当我执行完这一次以后,要等一下,等多少秒之后再次执行。表现为点击多次,只执行一次呗!写个简单的防抖:把这个html运行起来,就能用了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btn">点我</button>
<script>
//节流每次点击一次就把上次的timer删掉
function myDebounce(fn, delay){
let timer;
return function(){
if(timer){
clearTimeout(timer)
}
timer = setTimeout(()=>{
fn.apply(this, arguments)
}, delay)
}
}
document.getElementById('btn').addEventListener('click',myDebounce(()=>{
console.log(222)
},1000))
</script>
</body>
</html>
首先我们思考下防抖的应用场景,一般在搜索的时候会用到,不是所有的接口都要防抖的,所以没有必要给所有的接口加防抖,需要防抖的就传个防抖参数进来,不传就不防抖。所以说,在发送请求的时候应该有是参数来控制下,我们根据参数来确定到底要不要进入防抖控制。
引入axios
请求下
测试下看看:
多次点击按钮,只生效一次,600ms以后再次生效。
思考下为什么防抖的时候要用Map保存下时间,而不是直接像下图:
每次请求都是一个axios的实例,如果用timer的话,所有的请求公用一个timer,明显就不对了,所以要单独存放; 测试如下:我再html里面放了2个按钮请求接口
结论如下:btn2直接被拦截了,一次都没有触发。
用map的长这样,谁是谁的,互不干涉。
3.节流
手写节流的基础代码如下:自己启动个服务跑一下看看。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div style="height: 1000px"></div>
<script>
// 固定时间触发一次
function myThrottle(fn, time){
let startTime = 0
return function(){
let endTime = new Date();
if(endTime-startTime > time){
fn.apply(this, arguments);
startTime = endTime
}
}
}
function myScroll(){
console.log(1111)
}
document.addEventListener('scroll',myThrottle(myScroll,1000))
</script>
</body>
</html>
接下咱们可以把节流封装到自己的axios里面去,只要有isThrottle为true,那axios请求自动开始节流,否则不节流。
测试如下:
给所有的接口添加token等等,都可以用这样的方法写。我把他们全部放在不同的文件里面,是为了好理解,我看很多项目里面,大家都喜欢把axios的封装放在一个文件里面,导致我们在业务开发的时候,不想花费任何时间去阅读它。
如果你想快速测试,用这个模板
import axios from 'axios'; //直接用import axios from 'axios' 浏览器无法解析,需要esmodule模块
// 创建 axios 实例
const instance = axios.create({
baseURL: ' http://127.0.0.1:3002/api',
timeout: 10000,
timeoutErrorMessage: '请求超时,请稍后重试',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// 防抖
const cancelMap = new Map()
instance.interceptors.request.use(config => {
const tokenKey = `${config.method}-${config.url}`
const cancel = cancelMap.get(tokenKey)
if (cancel) {
cancel()
}
return new Promise(resolve => {
const timer = setTimeout(() => {
clearTimeout(timer)
resolve(config)
}, 800)
cancelMap.set(tokenKey, () => {
clearTimeout(timer)
resolve('节流处理中,请稍后重试!')
})
})
}, error => {
console.log(error)
return Promise.reject(error)
})
// 节流
let lastTime = new Date().getTime()
instance.interceptors.request.use(config => {
//两种写法
// const nowTime = new Date().getTime()
// if (nowTime - lastTime < 600) {
// return Promise.reject('节流处理中,请稍后重试!')
// }
// lastTime = nowTime
// return config
const time = config?.time || 600;
return new Promise((resolve, reject) => {
const nowTime = new Date().getTime()
if (nowTime - lastTime < time) {
reject('节流处理中,稍后再试!')
}
lastTime = nowTime
resolve(config)
})
}, error => {
console.log(error)
return Promise.reject(error)
})
// 响应
instance.interceptors.response.use(response => {
return response
}, error => {
console.log(error)
return Promise.reject(error)
})
export default {
get(url,params,options,){
return instance.get(url, { params, ...options })
},
post(url, params, options){
return instance.post(url, params, options)
}
}
是不是很简单呀!
4.防止接口重复请求
防止接口重复请求的办法有哪些呢?
- 1.全屏loading等待
- 2.收集请求信息,强制拦截
- 3.缓存请求信息派发共享
挂起请求的办法就是:return Promise.reject()
1.1 全屏loading等待
这个方案固然已经可以满足我们目前的需求,直接搞个全屏Loading很不太美观,何况有些组件本身就自带局部loading,两个圈圈一起转?好玩吗?会挨骂的呀!
1.2 收集请求信息,拦截
请求内容包括请求方法,地址,参数以及请求发出的页面hash。我们可以把这些信息转成一个字符串,然后存放在set里面。接下来就在请求拦截和响应拦截里面操作这个set。
axios拿到请求以后,先去set里面对比,如果不存在就加入,存在报错,拦截掉。等set里面的请求拿到数据以后,就把set里面对应的key删掉。
这个方式感觉很不错,但是如果一个页面2个组件,同时调用同一个接口呢?还有,如果我们的接口做了单独的异常处理呢?它就会出现明明没有调接口,却报错了!隐藏的bug还真不少!
1.3 收集请求信息,拦截
延续我们方案二的前面思路,仍然是拦截相同请求,但这次我们可不可以不直接把请求挂掉,而是对于相同的请求我们先给它挂起,等到最先发出去的请求拿到结果回来之后,把成功或失败的结果共享给后面到来的相同请求。
实现要点:
- 挂起的请求时,我们要用到发布订阅模式
- 对于挂起的请求,我们需要将它拦截,不能让它执行正常的请求逻辑,所以一定要在请求拦截器中通过
return Promise.reject()来直接中断请求,并做一些特殊的标记,以便于在响应拦截器中进行特殊处理。
import axios from "axios"
let instance = axios.create({
baseURL: "/api/"
})
// 发布订阅
class EventEmitter {
constructor() {
this.event = {}
}
on(type, cbres, cbrej) {
if (!this.event[type]) {
this.event[type] = [[cbres, cbrej]]
} else {
this.event[type].push([cbres, cbrej])
}
}
emit(type, res, ansType) {
if (!this.event[type]) return
else {
this.event[type].forEach(cbArr => {
if(ansType === 'resolve') {
cbArr[0](res)
}else{
cbArr[1](res)
}
});
}
}
}
// 根据请求生成对应的key
function generateReqKey(config, hash) {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}
// 存储已发送但未响应的请求
const pendingRequest = new Set();
// 发布订阅容器
const ev = new EventEmitter()
// 添加请求拦截器
instance.interceptors.request.use(async (config) => {
let hash = location.hash
// 生成请求Key
let reqKey = generateReqKey(config, hash)
if(pendingRequest.has(reqKey)) {
// 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
// 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
let res = null
try {
// 接口成功响应
res = await new Promise((resolve, reject) => {
ev.on(reqKey, resolve, reject)
})
return Promise.reject({
type: 'limiteResSuccess',
val: res
})
}catch(limitFunErr) {
// 接口报错
return Promise.reject({
type: 'limiteResError',
val: limitFunErr
})
}
}else{
// 将请求的key保存在config
config.pendKey = reqKey
pendingRequest.add(reqKey)
}
return config;
}, function (error) {
return Promise.reject(error);
});
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 将拿到的结果发布给其他相同的接口
handleSuccessResponse_limit(response)
return response;
}, function (error) {
return handleErrorResponse_limit(error)
});
// 接口响应成功
function handleSuccessResponse_limit(response) {
const reqKey = response.config.pendKey
if(pendingRequest.has(reqKey)) {
let x = null
try {
x = JSON.parse(JSON.stringify(response))
}catch(e) {
x = response
}
pendingRequest.delete(reqKey)
ev.emit(reqKey, x, 'resolve')
delete ev.reqKey
}
}
// 接口走失败响应
function handleErrorResponse_limit(error) {
if(error.type && error.type === 'limiteResSuccess') {
return Promise.resolve(error.val)
}else if(error.type && error.type === 'limiteResError') {
return Promise.reject(error.val);
}else{
const reqKey = error.config.pendKey
if(pendingRequest.has(reqKey)) {
let x = null
try {
x = JSON.parse(JSON.stringify(error))
}catch(e) {
x = error
}
pendingRequest.delete(reqKey)
ev.emit(reqKey, x, 'reject')
delete ev.reqKey
}
}
return Promise.reject(error);
}
export default instance;
这个拦截也有个问题,就是对上传文件来说,他们永远调用的都是一个接口,我的总不能每次都拦截吧,所以需要用请求的FormData类型去刷选他。
function isFileUploadApi(config) {
return Object.prototype.toString.call(config.data) === "[object FormData]"
}
总结下:
1.利用 axios.create() 工厂方法创建一个 asxios 实例。
2.每个 axios 实例都有一个拦截器,每个拦截器都有两个对象,一个是request,专门用来拦截请求的。另一个是 response 专门用来拦截响应。他们都有一个use方法,用来处理拦截任务。
3.在 request 的拦截器里面有个重要的参数是config,从它里面可以拿到发起请求时候的所有判断条件。然后在这里修改请求头,防抖,节流,判断token是不是失效,文件的上传下载等等
4.在 response 的拦截器里面有个重要的参数是data,从它里面可以拿到后端返回的所有值。然后在这里你可以将解构后端传过来的数据,对请求错误做综合处理。
5.最后一步就是返回 axios 实例。
整个封装就上面这五个步骤。
推荐文章: