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

先看一下使用方法

先声明,这套写法是我在一次网易公开课学来的,说是大厂内部是怎么封装请求的,本人只是给它改成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停止程序的做法,实在是太笨了)

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

相关推荐
孜然卷k1 分钟前
前端导出word文件,并包含导出Echarts图表等
前端·javascript
家里有只小肥猫22 分钟前
uniApp小程序保存canvas图片
前端·小程序·uni-app
前端大全24 分钟前
Chrome 推出全新的 DOM API,彻底革新 DOM 操作!
前端·chrome
八角丶36 分钟前
元素尺寸的获取方式及区别
前端·javascript·html
冴羽44 分钟前
Svelte 最新中文文档教程(16)—— Context(上下文)
前端·javascript·svelte
前端小臻1 小时前
关于css中bfc的理解
前端·css·bfc
白嫖不白嫖1 小时前
网页版的俄罗斯方块
前端·javascript·css
HappyAcmen1 小时前
关于Flutter前端面试题及其答案解析
前端·flutter
顾比魁1 小时前
pikachu之CSRF防御:给你的请求加上“网络身份证”
前端·网络·网络安全·csrf
林的快手1 小时前
CSS文本属性
前端·javascript·css·chrome·node.js·css3·html5