注册
web

大厂是怎么封装api层的?ts,axios 基于网易公开课

先看一下使用方法
请求封装2.png


先声明,这套写法是我在一次网易公开课学来的,说是大厂内部是怎么封装请求的,本人只是给它改成ts版本。
挺香的。


上核心代码


WX20231124-143933@2x.png


代码一:utils/request/getrequest.ts



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 Requestextends keyof ApiRouter = keyof ApiRouter> {
requestRouter: ApiRouter = {} as ApiRouter
requestTimes = 0;
requestMap: Record<string, CancelTokenSource> = {};
toLogined = false

/**
*
@feat <注册请求路由>
*/

parseRouter(routerName: T, defaultAxiosConfigMap: Record) {
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];

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,
data: requestParams,
};

/**
*
@feat <取消上一个同url请求>
*
@remarks [注:
* 个别页面需要同个api地址,多次请求,请传uniKey 作为区分,
* 不然有概率出现上一个请求被干掉
* ]
*/

if (selfMe.state === 'pending') this.cancelLastSameUrlRequest(requstConfig);

// 保险方案,传了 uniKey 才取消请求
// if (selfMe.state === 'pending' && requstConfig.uniKey) this.cancelLastSameUrlRequest(requstConfig);

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";
requstConfig.cancelToken = this.axiosSourceHandle(requstConfig).token;

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("请先登录");
}

generateReqKey(requestConfig: RequestConfig) {
return `${requestConfig.url}__${requestConfig.uniKey || ''}`
}
axiosSourceHandle(requestConfig: RequestConfig) {
const cancelToken = axios.CancelToken;
const source = cancelToken.source();

const reqKey = this.generateReqKey(requestConfig);
this.requestMap[reqKey] = source;

return source;
}
// 处理取消上一个请求
cancelLastSameUrlRequest(requestConfig: RequestConfig) {
const reqKey = this.generateReqKey(requestConfig);
const currentReqKey = this.requestMap[reqKey];

currentReqKey.cancel(`${manualStopProgram} reqKey: ${reqKey}`); // manualStopProgram 是一个标识,让外面的提示框忽略报错
}
}


export default new Request();



代码二:utils/request/server.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
}

import type { AxiosRequestConfig } from "axios";
import type { Api } from "@/apis/index";

export type RequestConfigany> = AxiosRequestConfig & {
uniKey?: string | number;
};

export type ApiConfig = {
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文件夹(即开头的那个图片),在这里配置接口信息,日常业务代码在这里写


接口.png


代码一 写api配置 :src/apis/modules/admin-admin/index.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


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


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. 解决的痛点:


以往我们看到最常见的封装方式,就这种

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 和结构权限表配合生成的


        <PermissionWrapper
hasPermission={store.
state.myPermission?.enterpriseAlarm?.edit}
>

<el-button type="primary" size="small" text onClick={openEditDialog}>
编辑
el-button>

PermissionWrapper>

ts直接提示,写起来很舒服,快准狠
ts提示.png


2. 请求函数封闭又开放


经过上面的 parseRouter 注册路由之后,sendMes 生成了N个请求函数,独一无二的函数,里面的fail success 可以做的事情很多,不如限制登录,取消上一个请求等等。大家有啥想法欢迎评论区写出来,我们一起优化它。


sendMes 最后一个参数,保持了开放性,在调用的时候我们传入uniKey就可以取消上一个请求了,还有一些特殊的参数,随便造。


3. 方便提取Api类型的参数和返回值类型(这是我额外拓展的)


我们经常会需要把参数和返回值的类型拿出来到页面上使用,这时候,就可以通过下面这个XX全局声明拿到。


declare namespace XX {
export type PromiseParams = T extends Promise ? T : never;

/**
*
@feat < 提取 api 参数 >
*
@describe <
type GetListParams = XX.ExtractApiParams

* >
*/

export type ExtractApiParams<Function_type> = Function_type extends (e: infer U) => any
? Excludeundefined>
: never;

/**
*
@feat < 提取 api 返回值 >
*
@describe <
type GetListReturn = XX.ExtractApiReturnType

* >
*/

export type ExtractApiReturnType<Function_type> = PromiseParams<
ReturnType<Function_type>
>["data"];

/**
*
@feat < 提取 api 为分页的 list item>
*
@describe <
type TableRow = XX.ExtractPromiseListItem

* >
*/

export type ExtractPromiseListItem<Function_type> =
ExtractApiReturnType<Function_type>["list"][number];
}
,>

下面是一些使用方法举例
image.png


image.png


image.png


image.png


用上面的写法很方便就能在页面中把具体的类型拿出来。做到一次类型声明,到处使用。


api封装是一个长期的话题,axios很好用,但其实它就是一个请求方法而已。相信大家也见过很多乱七八糟的写法。特别是一些老项目,想新增api都不知道放在哪个文件夹。


很幸运无意中看到网易公开课的老师们讲解,那时候他们写的是js版本,看到这种由配置对象直接生成api函数的做法瞬间眼前一亮,这不就是我一直在找的封装方式,满足了我所有的想象。感谢感谢


后来我花了点时间,让它变成ts版本,还封装XX这个全局声明,让它彻底好用起来。希望这个封装能让大家受益。


细心的读者可能会发现上面的代码,一直抛错误,但是却没有拦截提示。 这是笔者推崇的报错终止程序,而不是用return的方式。(js终止程序,我常用throw 替代 return


如果您有什么好的建议或想法,欢迎评论区留言。有用请点点赞,还有更多经验总结在路上。
嘴下留情,骂我倒无所谓,重要的是别把评论区搞得乌烟瘴气


原课程链接:js es5版本,有兴趣的可以看看。但我觉得它那个取消请求,得再升级一下不然万一同个页面调用同个接口2次,就会取消第一个请求了。所以我加装了uniKey作为标识。
live.study.163.com/live/index.…


作者:闸蟹
来源:juejin.cn/post/7304594468157849640

0 个评论

要回复文章请先登录注册