文章目录
前言
以下演示基于vue3与element-plus,react可以仿造这个写,原理都是一样的。
之前在b站上的前端三十up主那里看来的,感觉写的挺有意思的。特此写篇文章记录下,还加上自己的一些思考。
我们平时封装的axios函数其实网上一搜一大把,但这个up给我们展示了一种“另类”的封装方法,我重新按自己想法重构了遍,来看看咋写的
设计
文件结构建议
封装的文件结构
把二次封装axios所有有关的代码全部放在request文件夹中,其中init.js是二次封装的初始配置,index.js是对init.js再进行一次功能拓展封装,我们真正使用时也是用的这个文件,utils.js是存放相关的工具函数,webConfig.js是一些全局的变量。
为什么这么设计:
- 代码能够与其他功能解耦
- 方便直接拷贝整个文件夹放到一个新的项目中直接使用
接口管理文件
接口都单独放在api文件夹中,并且以后端的业务服务模块进行分类。
二次封装axios的初始配置
init文件
也就是src/request/init.js
文件:
// 配置全局得基础配置
import axios from "axios";
// 配置中心
import webConfig from "./webConfig.js"
// 需要的工具函数
import { isCheckTimeout } from "./utils";
// 相关库
import router from "@/router";
import store from "@/store";
import { ElMessage } from "element-plus";
let request = axios.create({
//1,基础配置
baseURL: process.env.VUE_APP_BASE_API, // process.env.VUE_APP_BASE_API就是我们在根目录创建对应的环境变量文件里的配置,
timeout: 5000, // 设置超时时间为5s
})
// 设置请求拦截器
request.interceptors.request.use((config) => {
// 1 token添加在请求头中,看看项目的token适合放在localStorage里还是vuex中
let token = localStorage.getItem("token");
// let token = store.getters.token;
// 2 首先有些接口是不需要设置token的,那就不用设置token了
let whiteList = webConfig.whiteListApi // 获取不需要设置token的接口白名单
let url = config.url
if (whiteList.indexOf(url) === -1 && token) {
if (isCheckTimeout()) {
// 超时了做登出操作
store.dispatch("user/logout");
return Promise.reject(new Error("token 失效"));
}
// 注入token到请求头中
config.headers.Authorization = `Bearer ${token}`;
}
// 3 配置接口国际化
config.headers["Accept-Language"] = store.getters.language;
// 4 配置响应数据类型
config.headers.responseType = "json"
return config; // 必须返回配置
}, error => {
return Promise.reject(new Error(error))
})
// 响应拦截器(处理返回的数据)
request.interceptors.response.use((res) => {
//响应得统一处理
let { status, message, data } = res.data; // status 这里的状态码是后端定义的,也就是每次请求都是200,但是后端根据情况返回401,500等状态码
if (status === 401) {
router.push("/noAuth")
return Promise.reject(new Error(message)); // 抛出可以自定义错误提示
}
if (status !== 200) {
ElMessage.error("错误码" + status + ":" + message);
return Promise.reject(new Error(message));
}
return res.data; // res 里面其实有很多东西下面截图
}, error => {
// 例如断网、跨域、状态码问题的报错
ElMessage.error(error.message);
// error.response.status 这里可以拿到接口真正的状态码,还是要看后端是怎么设计接口的
return Promise.reject(new Error(error));
})
export default request;
其中响应数据res为:
响应失败的error内容有:
所需其他的文件
utils文件
utils.js:
/* 处理token的时效性 */
// 常量设置
// token 时间戳
export const TIME_STAMP = "timeStamp";
// token超时时长(毫秒) 两小时
export const TOKEN_TIMEOUT_VALUE = 2 * 3600 * 1000;
/**
* 获取时间戳
*/
export function getTimeStamp() {
return window.localStorage.getItem(TIME_STAMP);
}
/**
* 设置时间戳(就是当前登陆时获取token的时间)
*/
export function setTimeStamp() {
window.localStorage.setItem(TIME_STAMP, Date.now());
}
/**
* 是否超时
*/
export function isCheckTimeout() {
// 当前时间戳
var currentTime = Date.now();
// 缓存时间戳
var timeStamp = getTimeStamp();
return currentTime - timeStamp > TOKEN_TIMEOUT_VALUE;
}
webConfig文件
webConfig.js:
export default {
whiteListApi: ["/a", "/b"],
pageSize: [20, 40, 80] // table的页码
}
第三次封装(重点)
首先要知道为什么还需要进行一次封装,因为单单只对axios做简单的初始化封装还是太过于简陋,未来随着业务的拓展,我们有可能会对接口请求的api做一些功能的拓展。
例如到了某个阶段,我们需要给接口返回的参数添加前端数据缓存。又到了某个阶段,需要阻止接口短时间内重复请求的功能等等。
这时候就很考验我们的代码设计了。
重点来了!!!!!!
推荐一种链式调用函数的方式去封装,举个例子:
function p() {
let promise = Promise.resolve()
function fn1(result) { // 功能封装1
console.log('fn1');
return Promise.resolve('fn1')
}
function fn2(result) { // 功能封装2
console.log('fn2');
return Promise.resolve('fn2')
}
let arr = [fn1, fn2]
while (arr.length) {
promise = promise.then(arr.shift())
}
return promise
}
p('1') // fn1 fn2 轮流执行
好好研究你就会发现,我们是相当于把一个一个功能封装函数当做是一个节点,只需要把传入项经过节点处理后,就得到一个封装过的方法。
按照这个思路我们来试试 index.js 怎么写
import request from "./init";
import { ElLoading } from 'element-plus'
// 要点 1 把同步的方法也统一以异步调用处理,保证所有方法的顺序 2 可通过配置关闭制定节点处理
// 默认配置
//{
// nendCache: false, // 是否直接拿取接口的上一个缓存数据(谨慎开启)
// nendLoading: true, // 全局加载
// needRequestingOnlyOne: false, // 是否开启,当前接口在请求时,禁止重复请求的情况(谨慎开启),因为有时候我们需要并发请求
// }
let requestApi = (function () {
let mapCache = new Map(); // 接口返回的数据缓存
let requestingUrl = []; // 正在发送的请求
let loadingInstance = null // 全局加载组件
return function (config) {
let { url, nendCache, nendLoading, needRequestingOnlyOne } = config
let promise = Promise.resolve() // 获取一个成功状态的promise
// 节点:全局加载
function nodeLoading() {
if (nendLoading === undefined || nendLoading === true) {
loadingInstance = ElLoading.service({ fullscreen: true })
}
return Promise.resolve({ keepGoing: true, type: 'then' })
}
// 节点:是否直接拿取接口的上一个缓存数据
function nodeCache() {
if (nendCache === true) {
// 如果之前已经请求过了,就直接返回缓存的,下面的节点都不用走了ß
if (mapCache.has(url)) {
if (loadingInstance) loadingInstance.close() // 关闭全局加载
return Promise.resolve({ keepGoing: false, type: 'then', data: mapCache.get(url) })
} else {
return Promise.resolve({ keepGoing: true, type: 'then' })
}
}
return Promise.resolve({ keepGoing: true, type: 'then' })
}
// 节点:接口还在请求过程中,又发了一次,阻止这种情况
function nodeRequestingOnlyOne() {
if (needRequestingOnlyOne === undefined || needRequestingOnlyOne === true) {
// 看看是不是在请求中
if (requestingUrl.indexOf(url) !== -1) {
return Promise.resolve({ keepGoing: false, type: 'catch', data: '请求已经提交' })
} else {
return Promise.resolve({ keepGoing: true, type: 'then' })
}
}
return Promise.resolve({ keepGoing: true, type: 'then' })
}
// 节点:发起请求
async function nodeRequest() {
let resData = await request({ ...config }) // 发送请求,等待获取数据
return Promise.resolve({ keepGoing: true, type: 'then', data: resData.data })
}
// 添加新节点
// 节点:请求完成后的收尾工作
function nodeRequestedFinalDo(result = { keepGoing: true }) {
// 需要抛出错误的情况
if (result.type === 'catch') {
return Promise.reject(result.data)
}
// 剔除掉正在请求中的接口的标识
requestingUrl = requestingUrl.filter(item => {
if (item !== url) {
return item
}
})
// 写入接口缓存数据
mapCache.set(url, result.data)
// 关闭全局加载
if (loadingInstance) loadingInstance.close()
return Promise.resolve(result.data)
}
let _handleArr = [nodeLoading, nodeCache, nodeRequestingOnlyOne, nodeRequest, nodeRequestedFinalDo] // 需要经过处理的节点
function startNodeList() {
while (_handleArr.length > 0) {
let fn = _handleArr.shift() // 获取当前节点
function _final(result = { keepGoing: true }) { // keepGoing 为false表示当前节点不执行,跳到下一个节点
if (!result.keepGoing) { // 后面keepGoing为false的节点都走这里
return Promise.resolve(result.data || result) // 如果是中途有keepGoing为false的需要取data
}
// keepGoing为true节点都走这里
return fn.call(this, result)
}
promise = promise.then(_final)
}
}
startNodeList()
return promise
}
})()
export {
requestApi as request, // 进过三次封装的
request as initRequest // 不进过第三次封装的
}
这片代码对js的基础要求较高,属于高阶用法了(时间久了我估计也不会写了哈哈),多看看!
环境配置
之前写init.js的时候,需要配置接口请求基本地址的环境变量。需要在目录中创建:
然后分别为
# 标志
ENV = 'development'
# base api
VUE_APP_BASE_API = '/api'
# 标志
ENV = 'production'
# base api
VUE_APP_BASE_API = 'https://mock.mengxuegu.com/mock/625c05ab66abf914b1f1bf10/crm'
使用例子
ok都写完后就可以使用了,在api/user.js中举例子:
import { initRequest, request } from "../request"
// 用二次封装的get请求,参数以对象的形式传入,axios会自动帮我们拼接在请求地址中
export const getUsersAllType = (params) => {
return initRequest({
url: "/usersAllType",
method: "get",
params: {
...params
},
})
}
// 用三次封装的post请求
export const getUserProfile = (params) => {
return request({
url: "/profile",
method: "post",
params: {
...params
},
needRequestingOnlyOne: true // 开启具体某项功能
})
}
建议
如果你想要让你的js功底再迈向进阶一小步的话,可以尝试一下,但是如果平时项目时间很紧,还是用你最熟悉的方式去封装吧,私底下再去研究哈哈。
如果你用react,那么恭喜你可以直接借助第三方库,也能有很好的体验。
借助第三方库就不用封三层了(推荐)
在react中,可以直接考虑把二次封装的异步请求结合hooks去使用,例如第三方hook库ahooks,里面有个useRequest
的hook,可以把异步请求放里面。然后能够返回很多状态去做判断。
例如官方上的一个例子:
const { loading, run, runAsync } = useRequest(service, {
manual: true
});
<button onClick={run} disabled={loading}>
{loading ? 'Loading' : 'Edit'}
</button>
你看如果我们要实现按钮的请求中防止点击,直接能通过返回的loading变量判断了,不用自己再去维护一个响应式的变量。
还有很多细节可以参考官方文档:ahooks
真的非常推荐!!!! 这样你就不用撒费苦心的二次封装那么多功能了。
结合TS后怎么设置后端返参类型
这是我之前学ts时候最好奇的问题了,好在也看到了答案!
// response 拦截:统一处理 errno 和 msg
instance.interceptors.response.use(res => {
const resData = (res.data || {}) as ResType
const { errno, data, msg } = resData
if (errno !== 0) {
// 错误提示
if (msg) {
message.error(msg)
}
throw new Error(msg)
}
return data as any
})
export default instance
// 后端返回的大对象最外层类型,这根据公司实际情况定制
export type ResType = {
errno: number
data?: ResDataType
msg?: string
}
// 对于拿不准的data数据,用这个写法
export type ResDataType = {
[key: string]: any
}
api这样写:
import axios, { ResDataType } from '../request/init'
// 获取用户信息
export async function getUserInfoService(): Promise<ResDataType> {
const url = '/api/user/info'
const data = (await axios.get(url)) as ResDataType
return data
}
完美,如果后端返回的大对象多了字段没关系,此时浏览器上运行的已经是js代码,不会报错啥的。
token无感知刷新方案
我放在另一篇文章里了,可以参考下:【场景方案】关于前端对接口行为的控制合集:轮番查询、并发请求、服务端通知、token无感刷新