-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from mengxinssfd/实现
feat(实现): 完成
- Loading branch information
Showing
4 changed files
with
249 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { CustomConfig } from './types'; | ||
|
||
type KeyHandler<K> = (k: K) => string; | ||
|
||
export default class Cache<K, V> { | ||
private readonly cache = new Map<string, { value: V; expires: number }>(); | ||
|
||
private readonly keyHandler: KeyHandler<K>; | ||
|
||
constructor(keyHandler?: KeyHandler<K>) { | ||
this.keyHandler = keyHandler || ((k) => JSON.stringify(k)); | ||
} | ||
|
||
clearDeadCache() { | ||
const now = Date.now(); | ||
for (const [k, v] of this.cache.entries()) { | ||
if (v.expires > now) continue; | ||
this.cache.delete(k); | ||
} | ||
} | ||
|
||
get(key: K) { | ||
const k = this.keyHandler(key); | ||
const v = this.cache.get(k); | ||
if (!v) return null; | ||
const now = Date.now(); | ||
if (now > v.expires) { | ||
this.cache.delete(k); | ||
return null; | ||
} | ||
return v.value; | ||
} | ||
|
||
set(key: K, value: V, customConfig: CustomConfig = {}) { | ||
const defaultTimeout = 5 * 1000; | ||
const timeout = | ||
typeof customConfig.useCache === 'object' ? customConfig.useCache?.timeout : defaultTimeout; | ||
this.cache.set(this.keyHandler(key), { value, expires: timeout + Date.now() }); | ||
this.clearDeadCache(); | ||
} | ||
|
||
has(key: K): boolean { | ||
return this.get(key) !== null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// 来源于nestjs @nestjs/common/enums/http-status.enum.d.ts | ||
export enum HttpStatus { | ||
CONTINUE = 100, | ||
SWITCHING_PROTOCOLS = 101, | ||
PROCESSING = 102, | ||
EARLYHINTS = 103, | ||
OK = 200, | ||
CREATED = 201, | ||
ACCEPTED = 202, | ||
NON_AUTHORITATIVE_INFORMATION = 203, | ||
NO_CONTENT = 204, | ||
RESET_CONTENT = 205, | ||
PARTIAL_CONTENT = 206, | ||
RESET_TOKEN = 207, | ||
AMBIGUOUS = 300, | ||
MOVED_PERMANENTLY = 301, | ||
FOUND = 302, | ||
SEE_OTHER = 303, | ||
NOT_MODIFIED = 304, | ||
TEMPORARY_REDIRECT = 307, | ||
PERMANENT_REDIRECT = 308, | ||
BAD_REQUEST = 400, | ||
UNAUTHORIZED = 401, | ||
PAYMENT_REQUIRED = 402, | ||
FORBIDDEN = 403, | ||
NOT_FOUND = 404, | ||
METHOD_NOT_ALLOWED = 405, | ||
NOT_ACCEPTABLE = 406, | ||
PROXY_AUTHENTICATION_REQUIRED = 407, | ||
REQUEST_TIMEOUT = 408, | ||
CONFLICT = 409, | ||
GONE = 410, | ||
LENGTH_REQUIRED = 411, | ||
PRECONDITION_FAILED = 412, | ||
PAYLOAD_TOO_LARGE = 413, | ||
URI_TOO_LONG = 414, | ||
UNSUPPORTED_MEDIA_TYPE = 415, | ||
REQUESTED_RANGE_NOT_SATISFIABLE = 416, | ||
EXPECTATION_FAILED = 417, | ||
I_AM_A_TEAPOT = 418, | ||
MISDIRECTED = 421, | ||
UNPROCESSABLE_ENTITY = 422, | ||
FAILED_DEPENDENCY = 424, | ||
TOO_MANY_REQUESTS = 429, | ||
INTERNAL_SERVER_ERROR = 500, | ||
NOT_IMPLEMENTED = 501, | ||
BAD_GATEWAY = 502, | ||
SERVICE_UNAVAILABLE = 503, | ||
GATEWAY_TIMEOUT = 504, | ||
HTTP_VERSION_NOT_SUPPORTED = 505, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import type { ResType, CustomConfig } from './types'; | ||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, Method } from 'axios'; | ||
import Qs from 'qs'; | ||
import Cache from './Cache'; | ||
|
||
// 使用模板方法模式处理axios请求, 具体类可实现protected方法替换掉原有方法 | ||
export default class AxiosWrapper { | ||
private readonly axios: AxiosInstance; | ||
private readonly cache: Cache<AxiosRequestConfig, Promise<any>>; | ||
constructor(config: AxiosRequestConfig = {}, private customConfig: CustomConfig<boolean>) { | ||
// 1、保存基础配置 | ||
this.axios = axios.create(config); | ||
this.setInterceptors(); | ||
// 缓存初始化 | ||
this.cache = new Cache((config) => { | ||
const url = config.url; | ||
const data = config.data || config.params; | ||
const headers = config.headers; | ||
return JSON.stringify({ url, data, headers }); | ||
}); | ||
} | ||
// 转换数据结构为ResType | ||
protected transferRes<T>(res: AxiosResponse): ResType<T> { | ||
return res.data as ResType; | ||
} | ||
// 获取拦截器 | ||
protected get interceptors() { | ||
return this.axios.interceptors; | ||
} | ||
protected setInterceptors() { | ||
// 重写此函数会在Request中调用 | ||
// example | ||
// this.interceptors.request.use(() => { | ||
// /* do something */ | ||
// }); | ||
} | ||
protected handleConfig(url: string, config: AxiosRequestConfig): AxiosRequestConfig { | ||
const finalConfig: AxiosRequestConfig = { ...config, url }; | ||
finalConfig.method = finalConfig.method || 'get'; | ||
return finalConfig; | ||
} | ||
protected handleParams(data: {}, config: AxiosRequestConfig) { | ||
if (config.method === 'get') { | ||
config.params = data; | ||
return; | ||
} | ||
if (!(data instanceof FormData)) { | ||
// 使用Qs.stringify处理过的数据不会有{}包裹 | ||
// 使用Qs.stringify其实就是转成url的参数形式:a=1&b=2&c=3 | ||
// 格式化模式有三种:indices、brackets、repeat | ||
data = Qs.stringify(data, { arrayFormat: 'repeat' }); | ||
} | ||
config.data = data; | ||
} | ||
protected handleResponse<T>( | ||
res: AxiosResponse<ResType<any>>, | ||
data: ResType<any>, | ||
customConfig: CustomConfig<boolean>, | ||
): Promise<ResType<T>> { | ||
const code = data.code ?? 'default'; | ||
const handlers = customConfig.statusHandlers; | ||
const defaultHandlers = this.customConfig.statusHandlers || { default: (res) => res.data }; | ||
const statusHandler = | ||
(handlers && (handlers[code] || handlers.default)) || | ||
defaultHandlers[code] || | ||
defaultHandlers.default; | ||
return statusHandler(res, data, customConfig as CustomConfig); | ||
} | ||
|
||
private _request(customConfig: CustomConfig = {}, axiosConfig: AxiosRequestConfig = {}) { | ||
if (customConfig.useCache) { | ||
const c = this.cache.get(axiosConfig); | ||
if (c) { | ||
return c; | ||
} | ||
} | ||
const res = this.axios(axiosConfig); | ||
|
||
if (customConfig.useCache) { | ||
this.cache.set(axiosConfig, res, customConfig); | ||
} | ||
|
||
return res; | ||
} | ||
|
||
request<T = never>(url: string, data?: {}): Promise<ResType<T>>; | ||
request<T = never, RC extends boolean = false>( | ||
url: string, | ||
data: {}, | ||
customConfig?: CustomConfig<RC>, | ||
axiosConfig?: AxiosRequestConfig, | ||
): Promise<RC extends true ? AxiosResponse<ResType<T>> : ResType<T>>; | ||
async request<T>( | ||
url: string, | ||
data: {} = {}, | ||
customConfig: CustomConfig = {}, | ||
axiosConfig: AxiosRequestConfig = {}, | ||
): Promise<any> { | ||
// 1、处理配置 | ||
const config = this.handleConfig(url, axiosConfig); | ||
// 2、处理参数 | ||
this.handleParams(data, config); | ||
try { | ||
// 3、请求 | ||
const response: AxiosResponse = await this._request(customConfig, config); | ||
// 4、请求结果数据结构处理 | ||
const data = this.transferRes<T>(response); | ||
// 5、状态码处理,并返回结果 | ||
return this.handleResponse<T>(response, data, customConfig); | ||
} catch (e: any) { | ||
// 错误处理 | ||
const response: AxiosResponse<ResType<any>> = e.response; | ||
const data = this.transferRes<T>(response); | ||
if (data && data.msg) { | ||
return this.handleResponse<T>(response, data, customConfig); | ||
} | ||
} | ||
} | ||
|
||
protected static methodFactory(method: Method, ins: AxiosWrapper) { | ||
return function <T = never, RC extends boolean = false>( | ||
url: string, | ||
data: {}, | ||
customConfig: CustomConfig<RC> = {}, | ||
axiosConfig: AxiosRequestConfig = {}, | ||
) { | ||
return ins.request<T, RC>(url, data, customConfig, { ...axiosConfig, method }); | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import type { AxiosResponse } from 'axios'; | ||
|
||
export type StatusHandler = ( | ||
res: AxiosResponse<ResType<any>>, | ||
data: ResType<any>, | ||
requestConfig: CustomConfig, | ||
) => any; | ||
|
||
// StatusHandlers | ||
export type StatusHandlers = Record<number, StatusHandler> & { default?: StatusHandler }; | ||
// CustomConfig | ||
export interface CustomConfig<T extends boolean = false> { | ||
returnRes?: T; // 返回res | ||
silent?: boolean; // 报错不弹窗 | ||
statusHandlers?: StatusHandlers; | ||
useCache?: boolean | { timeout: number }; | ||
} | ||
|
||
export interface ResType<T = never> { | ||
code: number; | ||
msg: string; | ||
data: T; | ||
} |