大厂是怎么封装请求的?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停止程序的做法,实在是太笨了)

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

相关推荐
gqkmiss18 分钟前
Chrome 浏览器插件获取网页 iframe 中的 window 对象
前端·chrome·iframe·postmessage·chrome 插件
m0_748247552 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255023 小时前
前端常用算法集合
前端·算法
真的很上进3 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203983 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2344 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
如若1234 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~5 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语5 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport5 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap