From c84a0cc6c98d496df28466a3520776b94b90e97d Mon Sep 17 00:00:00 2001 From: nanasikeai Date: Wed, 27 Aug 2025 18:32:26 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4fetch=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E7=94=A8axios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../template-open-vue3-ts/src/http/api.ts | 13 ++- .../src/http/fetch/error-interceptor.ts | 22 ---- .../src/http/fetch/index.ts | 103 ------------------ .../src/http/fetch/success-interceptor.ts | 26 ----- .../src/http/request/cancel.ts | 56 ++++++++++ .../src/http/request/index.ts | 76 +++++++++++++ .../http/{fetch => request}/request-error.ts | 4 +- .../src/http/request/request.ts | 13 +++ .../src/http/request/response.ts | 92 ++++++++++++++++ 9 files changed, 250 insertions(+), 155 deletions(-) delete mode 100644 packages/@bkui/template-open-vue3-ts/src/http/fetch/error-interceptor.ts delete mode 100644 packages/@bkui/template-open-vue3-ts/src/http/fetch/index.ts delete mode 100644 packages/@bkui/template-open-vue3-ts/src/http/fetch/success-interceptor.ts create mode 100644 packages/@bkui/template-open-vue3-ts/src/http/request/cancel.ts create mode 100644 packages/@bkui/template-open-vue3-ts/src/http/request/index.ts rename packages/@bkui/template-open-vue3-ts/src/http/{fetch => request}/request-error.ts (67%) create mode 100644 packages/@bkui/template-open-vue3-ts/src/http/request/request.ts create mode 100644 packages/@bkui/template-open-vue3-ts/src/http/request/response.ts diff --git a/packages/@bkui/template-open-vue3-ts/src/http/api.ts b/packages/@bkui/template-open-vue3-ts/src/http/api.ts index 9c258c8..8e77518 100644 --- a/packages/@bkui/template-open-vue3-ts/src/http/api.ts +++ b/packages/@bkui/template-open-vue3-ts/src/http/api.ts @@ -1,5 +1,14 @@ -import fetch from './fetch'; +import http from './request'; + +interface User { + username: string; + avatar_url: string; +} const apiPrefix = 'api'; -export const getUser = () => fetch.get(`${apiPrefix}/user`); +export const getUser = () => http.get(`${apiPrefix}/user`, { + params: { + username: 'test', + }, +}); diff --git a/packages/@bkui/template-open-vue3-ts/src/http/fetch/error-interceptor.ts b/packages/@bkui/template-open-vue3-ts/src/http/fetch/error-interceptor.ts deleted file mode 100644 index 9afa9a4..0000000 --- a/packages/@bkui/template-open-vue3-ts/src/http/fetch/error-interceptor.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { IFetchConfig } from './index'; -import { Message } from 'bkui-vue'; -import { loginModal } from '@/common/auth'; - -// 请求执行失败拦截器 -export default (error: any, config: IFetchConfig) => { - const { - code, - message, - response, - } = error || {}; - switch (code) { - // 用户登录状态失效 - case 401: - loginModal(); - } - // 全局捕获错误给出提示 - if (config.globalError) { - Message({ theme: 'error', message }); - } - return Promise.reject(error); -}; diff --git a/packages/@bkui/template-open-vue3-ts/src/http/fetch/index.ts b/packages/@bkui/template-open-vue3-ts/src/http/fetch/index.ts deleted file mode 100644 index e104645..0000000 --- a/packages/@bkui/template-open-vue3-ts/src/http/fetch/index.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { deepMerge } from '@/common/util'; -import successInterceptor from './success-interceptor'; -import errorInterceptor from './error-interceptor'; -import RequestError from './request-error'; - -export interface IFetchConfig extends RequestInit { - responseType?: 'json' | 'text' | 'arrayBuffer' | 'blob' | 'formData', - globalError?: Boolean -} - -type HttpMethod = (url: string, payload?: any, config?: IFetchConfig) => Promise; - -interface IHttp { - get?: HttpMethod; - post?: HttpMethod; - put?: HttpMethod; - delete?: HttpMethod; - head?: HttpMethod; - options?: HttpMethod; - patch?: HttpMethod; -} - -// Content-Type -const contentTypeMap = { - json: 'application/json', - text: 'text/plain', - formData: 'multipart/form-data', -}; -const methodsWithoutData = ['delete', 'get', 'head', 'options']; -const methodsWithData = ['post', 'put', 'patch']; -const allMethods = [...methodsWithoutData, ...methodsWithData]; - -// 拼装发送请求配置 -const getFetchConfig = (method: string, payload: any, config: IFetchConfig) => { - // 合并配置 - let fetchConfig: IFetchConfig = deepMerge( - { - method: method.toLocaleUpperCase(), - mode: 'cors', - cache: 'default', - credentials: 'include', - headers: { - 'X-Requested-With': 'fetch', - 'Content-Type': contentTypeMap[config.responseType] || 'application/json', - }, - redirect: 'follow', - referrerPolicy: 'no-referrer-when-downgrade', - responseType: 'json', - globalError: true, - }, - config, - ); - // merge payload - if (methodsWithData.includes(method)) { - fetchConfig = deepMerge(fetchConfig, { body: JSON.stringify(payload) }); - } else { - fetchConfig = deepMerge(fetchConfig, payload); - } - return fetchConfig; -}; - -// 拼装发送请求 url -const getFetchUrl = (url: string, method: string, payload = {}) => { - try { - // 基础 url - const baseUrl = location.origin + window.SITE_URL + process.env.BK_AJAX_URL_PREFIX; - // 构造 url 对象 - const urlObject: URL = new URL(url, baseUrl); - // get 请求需要将参数拼接到url上 - if (methodsWithoutData.includes(method)) { - Object.keys(payload).forEach((key) => { - const value = payload[key]; - if (!['', undefined, null].includes(value)) { - urlObject.searchParams.append(key, value); - } - }); - } - return urlObject.href; - } catch (error: any) { - throw new RequestError(-1, error.message); - } -}; - -// 在自定义对象 http 上添加各请求方法 -const http: IHttp = {}; -allMethods.forEach((method) => { - Object.defineProperty(http, method, { - get() { - return async (url: string, payload: any, config: IFetchConfig = {}) => { - const fetchConfig: IFetchConfig = getFetchConfig(method, payload, config); - try { - const fetchUrl = getFetchUrl(url, method, payload); - const response = await fetch(fetchUrl, fetchConfig); - return await successInterceptor(response, fetchConfig); - } catch (err) { - return errorInterceptor(err, fetchConfig); - } - }; - }, - }); -}); - -export default http; diff --git a/packages/@bkui/template-open-vue3-ts/src/http/fetch/success-interceptor.ts b/packages/@bkui/template-open-vue3-ts/src/http/fetch/success-interceptor.ts deleted file mode 100644 index 30e7e7b..0000000 --- a/packages/@bkui/template-open-vue3-ts/src/http/fetch/success-interceptor.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { IFetchConfig } from './index'; -import RequestError from './request-error'; - -// 请求成功执行拦截器 -export default async (response: any, config: IFetchConfig) => { - const { - code = response.code, - data, - message = response.statusText, - } = await response[config.responseType](); - if (response.ok) { - // 对应 HTTP 请求的状态码 200 到 299 - // 校验接口返回的数据,status 为 0 表示业务成功 - switch (code) { - // 接口请求成功 - case 0: - return Promise.resolve(data); - // 后端业务处理报错 - default: - throw new RequestError(code, message || '系统错误', data); - } - } else { - // 处理 http 非 200 异常 - throw new RequestError(code || -1, message || '系统错误', data); - } -}; diff --git a/packages/@bkui/template-open-vue3-ts/src/http/request/cancel.ts b/packages/@bkui/template-open-vue3-ts/src/http/request/cancel.ts new file mode 100644 index 0000000..034f9e6 --- /dev/null +++ b/packages/@bkui/template-open-vue3-ts/src/http/request/cancel.ts @@ -0,0 +1,56 @@ +import type { IRequestConfig } from './index'; + +export class RequestCanceler { + private abortControllerMap: Map; + + constructor() { + this.abortControllerMap = new Map(); + } + + // 添加请求 + addRequest(config: IRequestConfig) { + const requestKey = this.getRequestKey(config); + this.removeRequest(requestKey); + + const controller = new AbortController(); + + const newConfig = { + ...config, + signal: controller.signal, + }; + + this.abortControllerMap.set(requestKey, controller); + return newConfig; + } + + // 移除请求 + removeRequest(requestKey: string) { + if (this.abortControllerMap.has(requestKey)) { + const controller = this.abortControllerMap.get(requestKey); + if (controller && !controller.signal.aborted) { + controller.abort(); + } + this.abortControllerMap.delete(requestKey); + } + } + + // 取消所有请求 + cancelAllRequest() { + this.abortControllerMap.forEach((controller) => { + controller.abort(); + }); + this.abortControllerMap.clear(); + } + + // 重置 + reset() { + this.abortControllerMap.clear(); + } + + // 生成唯一请求标识 + getRequestKey(config: IRequestConfig): string { + return `${config.method}-${config.url}-${JSON.stringify(config.params)}-${JSON.stringify(config.data)}`; + } +} + +export default new RequestCanceler(); diff --git a/packages/@bkui/template-open-vue3-ts/src/http/request/index.ts b/packages/@bkui/template-open-vue3-ts/src/http/request/index.ts new file mode 100644 index 0000000..1c85009 --- /dev/null +++ b/packages/@bkui/template-open-vue3-ts/src/http/request/index.ts @@ -0,0 +1,76 @@ +import axios, { + type AxiosInstance, + type AxiosRequestConfig, + type CustomParamsSerializer, +} from 'axios'; +import { stringify } from 'qs'; + +import requestInterceptor from './request'; +import responseInterceptor from './response'; + +export interface IRequestConfig extends AxiosRequestConfig { + globalError?: boolean; +} +interface IRequestResponseResult { + code: number; + message: string; + data: T; +} + +interface HttpWithoutData { + get: (url: string, config?: IRequestConfig) => Promise>; + head: (url: string, config?: IRequestConfig) => Promise>; + options: (url: string, config?: IRequestConfig) => Promise>; + delete: (url: string, config?: IRequestConfig) => Promise>; +} +interface HttpWithData { + post: ( + url: string, + data?: D, + config?: IRequestConfig, + ) => Promise>; + put: ( + url: string, + data?: D, + config?: IRequestConfig, + ) => Promise>; + patch: ( + url: string, + data?: D, + config?: IRequestConfig, + ) => Promise>; +} + +interface IHttp extends HttpWithoutData, HttpWithData {} + +const defaultConfig: IRequestConfig = { + timeout: 10000, + headers: {}, + withCredentials: true, + paramsSerializer: { + serialize: stringify as unknown as CustomParamsSerializer, + }, +}; + +// 创建 axios 实例 +const axiosInstance: AxiosInstance = axios.create(defaultConfig); + +// 添加响应拦截器 +responseInterceptor(axiosInstance.interceptors.response); +requestInterceptor(axiosInstance.interceptors.request); + +const requestData = (cfg: AxiosRequestConfig) => axiosInstance + .request, IRequestResponseResult, D>(cfg); + +// 构建 http 方法 +const http: IHttp = { + get: (url: string, config: IRequestConfig = {}) => requestData({ method: 'get', url, ...config }), + head: (url: string, config: IRequestConfig = {}) => requestData({ method: 'head', url, ...config }), + options: (url: string, config: IRequestConfig = {}) => requestData({ method: 'options', url, ...config }), + delete: (url: string, config: IRequestConfig = {}) => requestData({ method: 'delete', url, ...config }), + post: (url: string, data?: D, config: IRequestConfig = {}) => requestData({ method: 'post', url, data, ...config }), + put: (url: string, data?: D, config: IRequestConfig = {}) => requestData({ method: 'put', url, data, ...config }), + patch: (url: string, data?: D, config: IRequestConfig = {}) => requestData({ method: 'patch', url, data, ...config }), +}; + +export default http; diff --git a/packages/@bkui/template-open-vue3-ts/src/http/fetch/request-error.ts b/packages/@bkui/template-open-vue3-ts/src/http/request/request-error.ts similarity index 67% rename from packages/@bkui/template-open-vue3-ts/src/http/fetch/request-error.ts rename to packages/@bkui/template-open-vue3-ts/src/http/request/request-error.ts index 763759d..c27a1ce 100644 --- a/packages/@bkui/template-open-vue3-ts/src/http/fetch/request-error.ts +++ b/packages/@bkui/template-open-vue3-ts/src/http/request/request-error.ts @@ -1,8 +1,8 @@ export default class RequestError extends Error { public code: number; public message: string; - public response: any; - constructor(code: number, message: string, response?: any) { + public response: unknown; + constructor(code: number, message: string, response?: unknown) { super(); this.code = code; this.message = message; diff --git a/packages/@bkui/template-open-vue3-ts/src/http/request/request.ts b/packages/@bkui/template-open-vue3-ts/src/http/request/request.ts new file mode 100644 index 0000000..33c6a67 --- /dev/null +++ b/packages/@bkui/template-open-vue3-ts/src/http/request/request.ts @@ -0,0 +1,13 @@ +import type { + AxiosInterceptorManager, + InternalAxiosRequestConfig, +} from 'axios'; + +import requestCanceler from './cancel'; // 引入请求取消器 + +export default (interceptors: AxiosInterceptorManager) => { + interceptors.use((request) => { + // 使用返回的新配置 + return requestCanceler.addRequest(request) as InternalAxiosRequestConfig; + }, undefined); +}; diff --git a/packages/@bkui/template-open-vue3-ts/src/http/request/response.ts b/packages/@bkui/template-open-vue3-ts/src/http/request/response.ts new file mode 100644 index 0000000..4b0a2ec --- /dev/null +++ b/packages/@bkui/template-open-vue3-ts/src/http/request/response.ts @@ -0,0 +1,92 @@ +import { + AxiosError, + type AxiosInterceptorManager, + type AxiosResponse, +} from 'axios'; +import { Message } from 'bkui-vue'; + +import { loginModal } from '@/common/auth'; + +import requestCanceler from './cancel'; +import type { IRequestConfig } from './index'; +import RequestError from './request-error'; + +export default (interceptors: AxiosInterceptorManager) => { + interceptors.use( + (response: AxiosResponse) => { + const { data, config } = response; + const { responseType } = config; + + const requestKey = requestCanceler.getRequestKey(config); + requestCanceler.removeRequest(requestKey); + + // 根据不同的响应类型进行处理 + switch (responseType) { + case 'arraybuffer': + case 'blob': + // 二进制数据响应:直接返回,不进行业务状态码检查 + // 通常用于文件下载等场景 + return Promise.resolve(data); + + case 'text': + // 文本响应:直接返回,不进行业务状态码检查 + // 通常用于纯文本内容 + return Promise.resolve(data); + + case 'stream': + // 流响应:直接返回,不进行业务状态码检查 + // 通常用于大文件或实时数据流 + // 注意:stream 类型主要在 Node.js 环境中使用 + return Promise.resolve(data); + + case 'document': + // XML 文档响应:直接返回,不进行业务状态码检查 + // 通常用于 XML 数据 + return Promise.resolve(data); + + default: { + // 默认处理:尝试按 JSON 处理,如果不是 JSON 则直接返回 + const { code, message } = data; + + switch (code) { + case 0: + return Promise.resolve(data); + default: + throw new RequestError(code, message || '系统错误', data); + } + } + } + }, + (error: AxiosError) => { + const { response, message, code } = error; + const config = error.config as IRequestConfig; + + if (config) { + const requestKey = requestCanceler.getRequestKey(response.config); + requestCanceler.removeRequest(requestKey); + } + + // 服务器响应数据 + if (response) { + const { status } = response; + + // 处理特定错误码 + switch (status) { + // 用户登录状态失效 + case 401: + loginModal(); + break; + } + + return Promise.reject(new RequestError(status || -1, message, response)); + } + + // 全局捕获错误给出提示 + if (config?.globalError) { + Message({ theme: 'error', message }); + } + + return Promise.reject(new RequestError(Number(code) || -1, message, response)); + }, + ); +};