Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(实现): 完成 #5

Merged
merged 1 commit into from
Apr 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/Cache.ts
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;
}
}
51 changes: 51 additions & 0 deletions src/HttpStatus.ts
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,
}
130 changes: 130 additions & 0 deletions src/index.ts
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 });
};
}
}
23 changes: 23 additions & 0 deletions src/types.d.ts
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;
}