diff --git a/src/plugins/http/Repeat.ts b/src/plugins/http/Repeat.ts new file mode 100644 index 0000000..2644db8 --- /dev/null +++ b/src/plugins/http/Repeat.ts @@ -0,0 +1,48 @@ +import { isCancel, AxiosRequestConfig, AxiosResponse, AxiosError } from "axios" +import { IPlugin } from "./base" + +declare module "axios" { + interface AxiosRequestConfig { + global?: boolean + } +} + +// 取消重复请求 +export class RepeatPlugin implements IPlugin { + static name: string = "repeat" + name: string = "repeat" + pendingPool = new Map() + clearPendingPool(whiteList: string[] = []) { + if (!this.pendingPool.size) return + const pendingUrlList = Array.from(this.pendingPool.keys()).filter(url => !whiteList.includes(url)) + if (!pendingUrlList.length) return + + pendingUrlList.forEach(pendingUrl => { + if (!this.pendingPool.get(pendingUrl).global) { + this.pendingPool.get(pendingUrl).cancelFn(`${pendingUrl} 请求取消`) + this.pendingPool.delete(pendingUrl) + } + }) + return pendingUrlList + } + beforeRequestConfig({ config }: { config: AxiosRequestConfig }) { + const controller = new AbortController(); + config.signal = controller.signal + const cancelFn = (reason?: string) => controller.abort(reason) + this.pendingPool.has(config.url) + ? cancelFn(`${config.url} 请求重复`) + : this.pendingPool.set(config.url, { cancelFn, global: config.global }) + } + beforeResponse({ response }: { response: AxiosResponse }) { + const { config } = response + this.pendingPool.delete(config.url) + } + beforeResponseError({ error }: { error: AxiosError }) { + const requestConfig = error.config! + if (!isCancel(error)) this.pendingPool.delete(requestConfig.url) + if (!error) return + if (isCancel(error)) { + throw new Error((requestConfig.signal as AbortSignal)?.reason || error.message || `请求'${requestConfig.url}' 被取消`) + } + } +} diff --git a/src/plugins/http/base.ts b/src/plugins/http/base.ts index 1b90413..ed4200c 100644 --- a/src/plugins/http/base.ts +++ b/src/plugins/http/base.ts @@ -1,62 +1,104 @@ import axios, { AxiosInstance, CreateAxiosDefaults } from "axios"; import { deepAssign } from "./utils"; -export const enum IHttpMethod { - GET = "GET", - POST = "POST", - DELETE = "DELETE", - PUT = "PUT", +export interface IPlugin { + name: string + created?(...argus: any[]): void + beforeCreate?(...argus: any[]): void + beforeDestory?(...argus: any[]): void + beforeRequestConfig?(...argus: any[]): void + beforeRequestError?(...argus: any[]): void + beforeResponse?(...argus: any[]): void + beforeResponseError?(...argus: any[]): void + destory?(...argus: any[]): void } -export enum IHttpContentType { - FORM = "application/x-www-form-urlencoded", - JSON = "application/json", -} - -class Plugin { - beforeInit() { } +export class Plugin implements IPlugin { + name: string = "" + created() { } + beforeCreate() { } + beforeDestory() { } + beforeRequestConfig() { } + beforeRequestError() { } + beforeResponse() { } + beforeResponseError() { } + destory() { } } export abstract class PluginsManager { - static #plugins: Plugin[] = [] - static use(plugin: Plugin) { - this.#plugins.push(plugin) - } + static plugins: IPlugin[] = [] + static use(plugin: IPlugin) { + if (Array.isArray(plugin)) { + PluginsManager.plugins = this.plugins.concat(plugin) + } else PluginsManager.plugins.push(plugin) + } - callPlugin(key: keyof Plugin, ...argus: any[]) { - PluginsManager.#plugins.forEach(p => p[key].apply(this, argus as [])) + plugins: IPlugin[] = [] + use(plugin: IPlugin) { + if (Array.isArray(plugin)) { + this.plugins = this.plugins.concat(plugin) + } else this.plugins.push(plugin) + } + callPluginByName<T>(name: string, key: keyof T, ...argus: any[]) { + const array = [...PluginsManager.plugins, ...this.plugins] + for (let i = 0; i < array.length; i++) { + let p = array[i] + if (!p.name) continue + if (name === p.name) { + const fn = (p as T)[key] + typeof fn === "function" && fn.apply(p, argus) + } } + } + callPlugin(key: keyof IPlugin, ...argus: any[]) { + const array = [...PluginsManager.plugins, ...this.plugins] + for (let i = 0; i < array.length; i++) { + let p = array[i] + const fn = p[key] + typeof fn === "function" && fn.apply(p, argus) + } + } } export abstract class httpBase extends PluginsManager { - static get method() { - return { - GET: "GET", - POST: "POST", - DELETE: "DELETE", - PUT: "PUT", - } - } - static get contentType() { - return { - FORM: "application/x-www-form-urlencoded", - JSON: "application/json", - } - } - - initDefaultConfig: CreateAxiosDefaults = { - baseURL: process.env.VUE_APP_BASEURL, - timeout: 10000, - headers: { - 'Content-Type': httpBase.contentType.FORM, - } - } + instance: AxiosInstance | null = null + requestInterceptorId: null | number = null + responseInterceptorId: null | number = null - instance: AxiosInstance | null = null + create<T>(config?: CreateAxiosDefaults<T>) { + this.instance = axios.create(deepAssign<CreateAxiosDefaults<T>>(axios.defaults, config ?? {})) + this.requestInterceptorId = this.instance.interceptors.request.use(config => { + const argu = { config } + this.callPlugin("beforeRequestConfig", argu) + return argu.config + }, error => { + const argu = { error } + this.callPlugin("beforeRequestError", argu) + return Promise.reject(error) + }) + this.responseInterceptorId = this.instance.interceptors.response.use(response => { + const argu = { response } + this.callPlugin("beforeResponse", argu) + return Promise.resolve(argu.response) + }, (error) => { + const argu = { error } + this.callPlugin("beforeResponseError", argu) + return Promise.reject(error) + }) + return this.instance + } - create<T>(config?: CreateAxiosDefaults<T>) { - this.instance = axios.create(deepAssign<CreateAxiosDefaults<T>>(axios.defaults, this.initDefaultConfig, config ?? {})) - return this.instance + destory() { + // 清理一些事务 + if (this.requestInterceptorId) { + this.instance?.interceptors.request.eject(this.requestInterceptorId) + } + if (this.responseInterceptorId) { + this.instance?.interceptors.response.eject(this.responseInterceptorId) } + httpBase.plugins = [] + this.plugins = [] + this.instance = null + } } diff --git a/src/plugins/http/index.ts b/src/plugins/http/index.ts index 95d1c66..79303a6 100644 --- a/src/plugins/http/index.ts +++ b/src/plugins/http/index.ts @@ -1,37 +1,99 @@ -import { httpBase, PluginsManager } from "./base" - -export const enum IHttpMethod { - GET = "GET", - POST = "POST", - DELETE = "DELETE", - PUT = "PUT", -} - -export enum IHttpContentType { - FORM = "application/x-www-form-urlencoded", - JSON = "application/json", -} - +import { httpBase, IPlugin } from "./base" +import { CreateAxiosDefaults, AxiosRequestConfig, AxiosResponse } from "axios" +import { RepeatPlugin } from "./Repeat" const FlyPoll: Map<string, any> = new Map() class Fly extends httpBase { - constructor() { - super() - this.Init() + static dispose(name: string) { + if (FlyPoll.has(name)) { + const instance = FlyPoll.get(name) + instance.#destory() + FlyPoll.delete(name) + } } - static get(name?: string): Fly { - if (!name) name = "$defalt" + static init(name: string, config: any, plugins: IPlugin[] = []) { if (!FlyPoll.has(name)) { - FlyPoll.set(name, new Fly()) + const instance = new Fly(name) + FlyPoll.set(name, instance) + instance.plugins = plugins + instance.#init(config || instance.default) + } + } + static invoke(name?: string): Fly { + if (!name) name = "$defalut" + const instance = FlyPoll.get(name) + if (!instance) throw new Error("未初始化此实例") + return instance + } + + static get method() { + return { + GET: "GET", + POST: "POST", + DELETE: "DELETE", + PUT: "PUT", + } + } + + static get contentType() { + return { + FORM: "application/x-www-form-urlencoded", + JSON: "application/json", + } + } + + default: CreateAxiosDefaults = { + // baseURL: process.env.VUE_APP_BASEURL, + timeout: 10000, + headers: { + 'Content-Type': Fly.contentType.FORM, } - return FlyPoll.get(name) } - Init() { - this.callPlugin("beforeInit") - this.create() + + name: string + + constructor(name: string) { + super() + this.name = name + } + get(...argus: [url: string, config?: AxiosRequestConfig<unknown> | undefined]) { + return this.instance!.get.apply(this.instance, argus ?? []) as Promise<AxiosResponse> + } + request(...argus: [config: AxiosRequestConfig<unknown>]): Promise<AxiosResponse> { + return this.instance!.request.apply(this.instance, argus) as Promise<AxiosResponse> + } + #init(config: AxiosRequestConfig) { + this.callPlugin("beforeCreate") + this.create(config) + this.callPlugin("created") + } + #destory() { + this.callPlugin("beforeDestory") + this.destory() + this.callPlugin("destory") } } -Fly.get() +export { + Fly +} + +// https://api.52vmy.cn/ +// Fly.invoke().request({ method: "get", url: "http://localhost:5173/", global: false }).then((res) => console.log(res.data)).catch(console.log) +// Fly.invoke().request({ method: "get", url: "http://localhost:5173/", global: true }).then((res) => console.log(res.data)).catch(console.log) + +// Fly.invoke().request({ method: "get", url: "https://api.52vmy.cn/api/wl/word/bing/tu", global: true }).then((res) => console.log(res.data)).catch(console.log) + +// Fly.invoke().request({ method: "get", url: "https://api.52vmy.cn/api/wl/word/bing/tu", global: true }).then((res) => console.log(res.data)).catch(console.log) + +// Fly.invoke("empty").request({ method: "get", url: "https://api.52vmy.cn/api/wl/yan/bay" }).then((res) => console.log(res.data)).catch(console.log) + +// Fly.invoke("empty").request({ method: "get", url: "https://api.52vmy.cn/api/img/tu/girl" }).then((res) => console.log(res.data)).catch(console.log) + + +// setTimeout(()=>{ +// // Fly.invoke("empty").request({method: "get", url: "https://api.52vmy.cn/api/img/tu/man"}).then((res)=>console.log(res.data)).catch(console.log) +// Fly.invoke().callPluginByName<RepeatPlugin>(RepeatPlugin.name, "clearPendingPool") +// }, 0) diff --git a/src/plugins/request.ts b/src/plugins/request.ts new file mode 100644 index 0000000..46f0d0a --- /dev/null +++ b/src/plugins/request.ts @@ -0,0 +1,15 @@ +import { Fly } from "./http" +import { RepeatPlugin } from "./http/Repeat" + + +Fly.init("$defalut", { + timeout: 10000, +}, [new RepeatPlugin()]) + +Fly.init("empty", { + timeout: 10000, +}) + +export { + Fly +} \ No newline at end of file diff --git a/src/ui/Register/Register.tsx b/src/ui/Register/Register.tsx index e93b9eb..5afcbe8 100644 --- a/src/ui/Register/Register.tsx +++ b/src/ui/Register/Register.tsx @@ -60,7 +60,7 @@ export function Register({ onSuccess, children }: IProps) { return ( <> - <form onSubmitCapture={clickRegister}> + <form onSubmit={()=>false} onSubmitCapture={clickRegister}> <WrapperComp> <FormGroup helperText="请输入一个可用的邮箱" label="邮箱" labelFor="text-input" labelInfo="(必须)"> <InputGroup diff --git a/src/views/Home/index.tsx b/src/views/Home/index.tsx index 7c0e9aa..95961f1 100644 --- a/src/views/Home/index.tsx +++ b/src/views/Home/index.tsx @@ -1,18 +1,29 @@ import withPage from "@/base/withPage" import { Hero } from "@/ui/Hero" -import { ReactNode } from "react" +import { Button } from "@blueprintjs/core" +import { ReactNode, useCallback } from "react" +import { Fly } from "@/plugins/request" interface IProps { children: ReactNode } -export default withPage(function Project({}: IProps) { +export default withPage(function Project({ }: IProps) { + + const onClick = useCallback(() => { + Fly.invoke().request({ + method: "get", + url: "https://api.52vmy.cn/api/wl/word/bing/tu", + global: false + }).then((res) => console.log(res.data)).catch(console.log) + }, []) + return ( <> <Hero></Hero> <div className="container"> - + <Button onClick={onClick}>请求</Button> </div> </> )