Skip to content
页面概要

数据请求

前端请求流程

在项目中,一个完整的前端 UI 交互到服务端处理流程是这样的:

  • 1、UI 组件交互操作;
  • 2、调用 server.ts api 请求函数;
  • 3、使用封装的 /src/utils/request.ts 发送请求;
  • 4、获取服务端返回;
  • 5、更新 data;

request.ts

/src/utils/request.ts 是基于 Axios 的封装,便于统一处理 POST,GET 等请求参数,请求头,以及错误提示信息等。它封装了全局 request拦截器response拦截器消除器统一的错误处理统一做了超时处理baseURL设置等。详细内容如下:

ts
/**
 * @description: 自定义 request 网络请求工具,基于axios
 * @author LiQingSong
 */
import qs from "qs";
import axios, { AxiosInstance, AxiosPromise, AxiosRequestConfig, AxiosResponse, Canceler } from "axios";
import { ContentTypeEnum, ResultCodeEnum } from "@/enums/utils.request.enum";
import { ajaxHeadersTokenKey, ajaxResponseNoVerifyUrl } from "@/config/settings";
import { getToken } from "@/utils/localToken";
import { isFunction } from "@/utils/is";
import router from "@/config/router";
import { IResponseData, ICodeMessage, IAxiosRequestConfig } from "@/@types/utils.request";

/* ================ 自定义请求消除器 相关 S ======================= */
/**
 * @description: 声明一个 Map 用于存储每个请求的标识 和 取消函数
 */
let requestPendingMap = new Map<string, Canceler>();

/**
 * @description: 序列化配置参数,生成唯一请求标识url
 * @param config 请求配置参数
 * @returns string
 */
export const getRequestPendingUrl = (config: IAxiosRequestConfig) =>
	[config.method, config.url, qs.stringify(config.data), qs.stringify(config.params)].join("&");

/**
 * @description: 自定义请求消除器类
 */
export class RequestCanceler {
	/**
	 * @description: 添加请求
	 * @param config 请求配置参数
	 * @returns void
	 */
	addPending(config: IAxiosRequestConfig) {
		// 在请求开始前,对之前的请求做检查取消操作
		this.removePending(config);
		const url = getRequestPendingUrl(config);
		config.cancelToken =
			config.cancelToken ||
			new axios.CancelToken((cancel) => {
				if (!requestPendingMap.has(url)) {
					// 如果 pending 中不存在当前请求,则添加进去
					requestPendingMap.set(url, cancel);
				}
			});
	}

	/**
	 * @description: 移除请求
	 * @param config 请求配置参数
	 * @returns void
	 */
	removePending(config: IAxiosRequestConfig) {
		const url = getRequestPendingUrl(config);
		if (requestPendingMap.has(url)) {
			// 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
			const cancel = requestPendingMap.get(url);
			cancel && cancel();
			requestPendingMap.delete(url);
		}
	}

	/**
	 * @description: 清空所有pending
	 * @returns void
	 */
	removeAllPending() {
		requestPendingMap.forEach((cancel) => {
			cancel && isFunction(cancel) && cancel();
		});
		requestPendingMap.clear();
	}

	/**
	 * @description: 重置
	 * @returns void
	 */
	reset(): void {
		requestPendingMap = new Map<string, Canceler>();
	}
}

/**
 * @description: 生成请求消除器
 */
export const requestCanceler = new RequestCanceler();

/* ================ 自定义请求类 相关 S ======================= */
/**
 * @description: 自定义状态码对应内容信息
 */
const customCodeMessage: ICodeMessage = {
	[ResultCodeEnum.LOGININVALID]: "当前用户登入信息已失效,请重新登入再操作", // 未登陆,自己可以调整状态码
};

/**
 * @description: 定义服务端状态码对应内容信息
 */
const serverCodeMessage: ICodeMessage = {
	200: "服务器成功返回请求的数据",
	400: "Bad Request",
	401: "Unauthorized",
	403: "Forbidden",
	404: "Not Found",
	500: "服务器发生错误,请检查服务器(Internal Server Error)",
	502: "网关错误(Bad Gateway)",
	503: "服务不可用,服务器暂时过载或维护(Service Unavailable)",
	504: "网关超时(Gateway Timeout)",
};

/**
 * @description: 异常处理程序
 * @returns Promise
 */
const errorHandler = (error: any) => {
	const { response, message } = error;
	if (message === "CustomError") {
		// 自定义错误
		const { config, data } = response;
		const { url, baseURL } = config;
		const { code, msg } = data;
		const reqUrl = url.split("?")[0].replace(baseURL, "");
		const noVerifyBool = ajaxResponseNoVerifyUrl.includes(reqUrl);
		if (!noVerifyBool) {
			alert(customCodeMessage[code] || msg || "Error");

			if (code === ResultCodeEnum.LOGININVALID) {
				// 如果未登录或失效,这里可以跳转到登录页面
				router.push("/user/login");
			}
		}
	} else if (response && response.status) {
		const errorText = serverCodeMessage[response.status] || response.statusText;
		const { status, request } = response;
		alert(`请求错误 ${status}: ${request.responseURL}\n${errorText}`);
	} else if (!response) {
		alert("网络异常:您的网络发生异常,无法连接服务器");
	}

	return Promise.reject(error);
};

/**
 * @description: 自定义请求类
 */
export class Request {
	ajax: AxiosInstance;
	public constructor(config: IAxiosRequestConfig) {
		const { contentType, ...otherCofing } = config || {};
		// 实例化axios,配置请求时的默认参数
		this.ajax = axios.create({
			...otherCofing,
			headers: {
				...otherCofing.headers,
				"Content-Type": contentType || ContentTypeEnum.JSON,
			},
		});

		/**
		 * @description: 请求前, 请求拦截器
		 */
		this.ajax.interceptors.request.use(
			(axiosConfig: AxiosRequestConfig) => {
				// 将当前请求添加到请求消除器 pending 中
				requestCanceler.addPending(config);

				if (!axiosConfig.headers) {
					axiosConfig.headers = {};
				}
				// 自定义添加token header
				const headerToken = getToken();
				if (headerToken) {
					axiosConfig.headers[ajaxHeadersTokenKey] = headerToken;
				}

				return axiosConfig;
			},
			/* ,error=> {} */ // 已在 export default catch
		);

		/**
		 * @description: 请求后, 响应拦截器
		 */
		this.ajax.interceptors.response.use(
			(response: AxiosResponse<IResponseData>) => {
				// 在请求结束后,移除本次请求
				requestCanceler.removePending(config);

				const res = response.data;
				const { code } = res;

				// 自定义状态码验证
				if (code !== ResultCodeEnum.SUCCESS) {
					return Promise.reject({
						response,
						message: "CustomError",
					});
				}

				return response;
			},
			/* , error => {} */ // 已在 export default catch
		);
	}

	/**
	 * @description: 请求方法
	 * @param config 请求参数
	 * @returns AxiosPromise
	 */
	all<T = any, R = AxiosResponse<T>>(config: IAxiosRequestConfig): AxiosPromise<R> {
		const { contentType, ...otherCofing } = config || {};
		return this.ajax({
			...otherCofing,
			headers: {
				...otherCofing.headers,
				"Content-Type": contentType || ContentTypeEnum.JSON,
			},
		})
			.then((response: AxiosResponse) => response.data)
			.catch((error: any) => errorHandler(error));
	}
}

/**
 * @description: 生成统一公共请求
 */
const ask = new Request({
	baseURL: import.meta.env.VITE_APP_API_URL || "", // url = api url + request url
	withCredentials: false, // 当跨域请求时发送cookie
	timeout: 0, // 请求超时时间,5000(单位毫秒) / 0 不做限制
});

/**
 * @description: 导出 ajax 方法
 * @param config IAxiosRequestConfig 请求参数
 * @returns AxiosPromise
 */
export default function ajax<T = any>(config: IAxiosRequestConfig): AxiosPromise<AxiosResponse<T>> {
	return ask.all<T>(config);
}

数据请求的详细样列可以查看 登录页面。基于此封装,如果二次开发,安装了第三方UI组件,可以把对应的弹框提示做优化。

Released under the MIT License.