先看一下使用方法
先声明,这套写法是我在一次网易公开课学来的,说是大厂内部是怎么封装请求的,本人只是给它改成ts版本。 挺香的。
上核心代码
代码一:utils/request/getrequest.ts
ts
![WX20231124-143933@2x.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/90bd1fe7356f4309af4950a6d9da7fa9~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=630&h=226&s=58455&e=png&b=2b2b2d)
import axios, { type AxiosRequestConfig, type CancelTokenSource } from "axios";
import { manualStopProgram } from '@/utils/index';
import server from "./server";
import type { RequestConfig, ApiRouter, ServerRes } from './server.types'
class Request<T extends keyof ApiRouter = keyof ApiRouter> {
requestRouter: ApiRouter = {} as ApiRouter
requestTimes = 0;
requestMap: Record<string, CancelTokenSource> = {};
toLogined = false
/**
* @feat <注册请求路由>
*/
parseRouter(routerName: T, defaultAxiosConfigMap: Record<keyof ApiRouter[T], AxiosRequestConfig>) {
const apiModule = this.requestRouter[routerName] = {} as ApiRouter[T]
Object.entries(defaultAxiosConfigMap).forEach((item) => {
type ApiName = keyof ApiRouter[T]
const [apiName, defaultRequestConfig] = item as [ApiName, RequestConfig];
/**
* @feat <绑定基本配置>
* @describe <
要点:通过bind方法绑定一部分的基本配置,将object配置生成函数。
笔者认为这点非常巧妙, this.sendMes 的第四个参数 otherAxiosConfig
则是在调用时可以自由插入自定义配置项参数
>
*/
const request = this.sendMes.bind(
this,
routerName,
apiName,
defaultRequestConfig,
);
apiModule[apiName] = request as ApiRouter[T][ApiName]
apiModule[apiName].state = "ready";
});
}
async sendMes<ApiName extends keyof ApiRouter[T] = keyof ApiRouter[T]>(
routerName: T,
apiName: ApiName,
defaultRequestConfig: RequestConfig,
requestParams: Record<string, any>,
otherAxiosConfig?: RequestConfig
): Promise<ServerRes> {
this.requestTimes += 1;
return new Promise(async (resolve, reject) => {
try {
const selfMe = this.requestRouter[routerName][apiName];
const requstConfig: RequestConfig = {
...defaultRequestConfig,
...otherAxiosConfig,
cancelToken:
otherAxiosConfig &&
this.cancelLastRequest(otherAxiosConfig.uniKey),
data: requestParams,
};
const successCb = (res: ServerRes) => {
const ret = this.responseHandle(res, requstConfig)
resolve(ret);
};
const failCb = (error: unknown) => {
console.error("接口报错: " + requstConfig.url, error);
// 处理错误逻辑
throw error;
};
const complete = () => {
selfMe.state = "ready";
this.requestTimes -= 1;
if (this.requestTimes === 0) {
this.toLogined = false;
}
};
selfMe.state = "pending";
await server(requstConfig).then(successCb).catch(failCb).finally(complete);
} catch (error) {
reject(error);
}
})
}
// 处理响应值
responseHandle(res: ServerRes, config: RequestConfig) {
const { code } = res;
console.warn(`请求返回: ${config.url}`, res);
if (code === 405) throw String("405 检查请求方式");
if (code === 401) this.toLogin();
if (code !== 200) throw String(res.message);
return res;
}
toLogin() {
if (this.toLogined) return;
throw String("请先登录");
}
// 处理取消上一个请求
cancelLastRequest(reqUniKey: RequestConfig["uniKey"]) {
if (reqUniKey) {
const currentReqKey = this.requestMap[reqUniKey];
// 有取消请求key,默认取消上一个请求
if (currentReqKey) {
currentReqKey.cancel(`${manualStopProgram} reqUniKey : ${reqUniKey}`); // manualStopProgram 是一个标识,让外面的提示框忽略报错
}
const cancelToken = axios.CancelToken;
const source = cancelToken.source();
this.requestMap[reqUniKey] = source;
return source.token;
}
}
}
export default new Request();
代码二:utils/request/server.ts
ts
import axios from "axios";
import { UserInfo } from "@/utils/index";
import type { RequestConfig, ServerRes } from "./server.types";
export default async function server(
axiosRequestConfig: RequestConfig
): Promise<ServerRes> {
const token = UserInfo.getToken() || "";
const reqData = (() => {
const data = axiosRequestConfig.data;
const isFormData = data instanceof FormData;
if (isFormData) {
data.append("token", token);
return data;
}
return {
...data,
token
};
})();
const { data: resBody, status } = await axios({
...axiosRequestConfig,
withCredentials: true,
data: reqData
}).catch((err) => {
const errMsg = err && typeof err === "object" && err !== null && "message" in err
if (errMsg) throw err.message;
throw err;
});
return resBody;
}
export {
server
}
ts
import type { AxiosRequestConfig } from "axios";
import type { Api } from "@/apis/index";
export type RequestConfig<D = any> = AxiosRequestConfig<D> & {
uniKey?: string | number | null;
};
export type ApiConfig<T, K> = {
params: T;
return: K;
};
export type List_pagiantion = {
page: number;
page_size: number;
};
// 这里有点绕,把各个api的参数和返回值 合成一个个特定的函数
export type ApiRouter = {
[K in keyof Api]: {
[T in keyof Api[K]]: Api[K][T] extends ApiConfig<any, any>
? {
(params: Api[K][T]["params"], otherRequestConfig?: RequestConfig): Promise<{
message: string;
code: number;
data: Api[K][T]["return"];
}>;
state?: 'pending' | 'ready'
}
: never;
};
}
export type ApiRouter__requestConfig = {
[K in keyof Api]: {
[T in keyof Api[K]]: RequestConfig;
};
}
export type ServerRes = {
code: number,
message: string,
data: any
};
接下来是apis文件夹(即开头的那个图片),在这里配置接口信息,日常业务代码在这里写
代码一 写api配置 :src/apis/modules/admin-admin/index.ts
ts
import type { ApiRouter__requestConfig } from "@/utils/modules/request/server.types.d";
const indexAdmin: ApiRouter__requestConfig["adminAdmin"] = {
getList: {
method: "post",
url: "/admin/admin/getList"
},
};
export default indexAdmin;
代码二 写接口类型声明 :src/apis/modules/admin-admin/index.types.d.ts
ts
import type { ApiConfig } from "@/utils/modules/request/server.types.d";
export type AdminAdmin = {
getList: ApiConfig<
{
page: number,
page_size: number,
phone?: string,
status?: ManagerStatus,
groups_id?: number
},
{
count: number,
list: {
id: number,
phone: string,
groups_id: number,
create_at: string,
status: number,
status_txt: string,
groups_txt: string
}[]
}
>,
};
代码三 注册路由 :src/apis/index.ts
ts
import { request } from "@/utils/index";
import indexAdmin from "./modules/index-admin/index";
request.parseRouter("indexAdmin", indexAdmin);
export type Api = {
indexAdmin: IndexAdmin;
}
// 这个是另一个作用,到处配置项,配合接口做权限控制,下面在说
export function getApiConfigMap() {
return {
indexAdmin,
};
}
下面说说这个封装方式好在哪里:
1. 解决的痛点:
以往我们看到最常见的封装方式,就这种
ts
export function Api1() {
return axios.get('xx')
}
export function Api2() {
return axios.get('xx')
}
export function Api3() {
return axios.get('xx')
}
export function Api4() {
return axios.get('xx')
}
这种就非常麻木,一直写函数,每一个都要写配置项,没有数据结构结构=(无法复用)。 如果换成上面的 const indexAdmin: ApiRouter__requestConfig["adminAdmin"] = {},这种写法,就有数据结构了,有了结构之后就可以进行组合复用 比如上面提到的 getApiConfigMap 可以把数据结构直接导出,配合接口做按钮级权限控制, 接口会返回一份配置项{authen1: '/admin/admin/getList'}。 我们二者一比对,就能判断出是否有权限了。
比如看下面代码 PermissionWrapper 是一个权限容器组件 hasPermission=true就显示按钮 store.state.myPermission?.enterpriseAlarm?.edit 是用 getApiConfigMap 和结构权限表配合生成的
ts
<PermissionWrapper
hasPermission={store.state.myPermission?.enterpriseAlarm?.edit}
>
<el-button type="primary" size="small" text onClick={openEditDialog}>
编辑
</el-button>
</PermissionWrapper>
ts直接提示,写起来很舒服,快准狠
2. 请求函数封闭又开放
经过上面的 parseRouter 注册路由之后,sendMes 生成了N个请求函数,独一无二的函数,里面的fail success 可以做的事情很多,不如限制登录,取消上一个请求等等。大家有啥想法欢迎评论区写出来,我们一起优化它。
sendMes 最后一个参数,保持了开放性,在调用的时候我们传入uniKey就可以取消上一个请求了,还有一些特殊的参数,随便造。
细心的读者可能会发现上面的代码,一直抛错误,但是却没有拦截提示。 这是笔者推崇的报错终止程序,而不是用return的方式。近期会再写(return停止程序的做法,实在是太笨了)
如果您有什么好的建议或想法,欢迎评论区留言。有用请点点赞,还有更多经验总结在路上。 嘴下留情,骂我倒无所谓,重要的是别把评论区搞得乌烟瘴气