Files
query/src/query.ts

315 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { adapter, Method, AdapterOpts, methods } from './adapter.ts';
import type { QueryWs } from './ws.ts';
/**
* 请求前处理函数
* @param opts 请求配置
* @returns 请求配置
*/
export type Fn = (opts: {
url?: string;
headers?: Record<string, string>;
body?: Record<string, any>;
[key: string]: any;
timeout?: number;
}) => Promise<Record<string, any> | false>;
export type QueryOpts = {
adapter?: typeof adapter;
token?: string;
[key: string]: any;
} & AdapterOpts;
export type QueryOptions = {
url?: string;
baseURL?: string;
beforeRequest?: Fn;
adapter?: typeof adapter;
headers?: Record<string, string>;
timeout?: number;
isClient?: boolean;
tokenName?: string;
storage?: Storage;
}
export type Data = {
path?: string;
key?: string;
payload?: Record<string, any>;
[key: string]: any;
};
export type Result<S = any, U = {}> = {
code: number;
data?: S;
message?: string;
} & U;
// 额外功能
export type DataOpts = Partial<QueryOpts> & {
beforeRequest?: Fn;
afterResponse?: <S = any>(result: Result<S>, ctx?: { req?: any; res?: any; fetch?: any }) => Promise<Result<S>>;
/**
* 是否在stop的时候不请求
*/
noStop?: boolean;
};
export const wrapperError = ({ code, message }: { code?: number; message?: string }) => {
const result = {
code: code || 500,
message: message || '请求错误'
};
return result;
};
/**
* const query = new Query();
* const res = await query.post({
* path: 'demo',
* key: '1',
* });
*
* U是参数 V是返回值
*/
export class Query {
adapter: typeof adapter;
baseURL: string;
url: string;
/**
* 请求前处理函数
*/
beforeRequest?: DataOpts['beforeRequest'];
/**
* 请求后处理函数
*/
afterResponse?: DataOpts['afterResponse'];
headers?: Record<string, string>;
timeout?: number;
/**
* 需要突然停止请求比如401的时候
*/
stop?: boolean;
// 默认不使用ws
qws: QueryWs;
tokenName: string;
storage: Storage;
token: string;
constructor(opts?: QueryOptions) {
this.adapter = opts?.adapter || adapter;
this.tokenName = opts?.tokenName || 'token';
this.storage = opts?.storage || globalThis?.localStorage;
const defaultURL = opts?.isClient ? '/client/router' : '/api/router';
this.url = opts?.url || defaultURL;
if (this.url.startsWith('http')) {
const urlObj = new URL(this.url);
this.baseURL = urlObj.origin;
}
this.baseURL = opts?.baseURL || this.baseURL; // 如果opts中有baseURL优先
this.headers = opts?.headers || {
'Content-Type': 'application/json',
};
this.timeout = opts?.timeout || 60000 * 3; // 默认超时时间为 60s * 3
if (opts?.beforeRequest) {
this.beforeRequest = opts.beforeRequest;
} else {
this.beforeRequest = async (opts) => {
const token = this.token || this.storage?.getItem?.(this.tokenName);
if (token) {
opts.headers = {
...opts.headers,
Authorization: `Bearer ${token}`,
};
}
return opts;
};
}
}
setQueryWs(qws: QueryWs) {
this.qws = qws;
}
/**
* 突然停止请求
*/
setStop(stop: boolean) {
this.stop = stop;
}
/**
* 发送 get 请求,转到 post 请求
* T是请求类型自定义
* S是返回类型自定义
* @param params 请求参数
* @param options 请求配置
* @returns 请求结果
*/
async get<R = any, P = any>(params: Data & P, options?: DataOpts): Promise<Result<R>> {
return this.post(params, options);
}
/**
* 发送 post 请求
* T是请求类型自定义
* S是返回类型自定义
* @param body 请求体
* @param options 请求配置
* @returns 请求结果
*/
async post<R = any, P = any>(body: Data & P, options?: DataOpts): Promise<Result<R>> {
const url = options?.url || this.url;
const { headers, adapter, beforeRequest, afterResponse, timeout, ...rest } = options || {};
const _headers = { ...this.headers, ...headers };
const _adapter = adapter || this.adapter;
const _beforeRequest = beforeRequest || this.beforeRequest;
const _afterResponse = afterResponse || this.afterResponse;
const _timeout = timeout || this.timeout;
const req = {
url: url,
headers: _headers,
body,
timeout: _timeout,
...rest,
};
const isStartsWithHttp = req.url.startsWith('http');
// 如果是完整的url直接使用, 如果不是完整的url且baseURL存在则拼接baseURL
if (!isStartsWithHttp) {
if (this.baseURL) {
const baseURL = new URL(this.baseURL || globalThis?.location?.origin).origin;
req.url = baseURL + req.url;
}
}
try {
if (_beforeRequest) {
const res = await _beforeRequest(req);
if (res === false) {
return wrapperError({
code: 500,
message: '请求取消',
// @ts-ignore
req: req,
});
}
}
const headers = req.headers || {};
if (options?.token && !headers['Authorization']) {
headers['Authorization'] = `Bearer ${options.token}`;
}
} catch (e) {
console.error('request beforeFn error', e, req);
return wrapperError({
code: 500,
message: '请求在请求前处理时发生错误',
// @ts-ignore
req: req,
});
}
if (this.stop && !options?.noStop) {
const that = this;
const res = await new Promise((resolve) => {
let timer = 0;
const detect = setInterval(() => {
if (!that.stop) {
clearInterval(detect);
resolve(true);
}
timer++;
if (timer > 5) {
console.error('等待请求失败:', req.url, timer);
clearInterval(detect);
resolve(false);
}
}, 1000);
});
if (!res) {
return wrapperError({
code: 500,
message: '请求取消可能是因为用户未登录或者token过期',
// @ts-ignore
req: req,
});
}
}
return _adapter(req).then(async (res) => {
try {
if (_afterResponse) {
return await _afterResponse(res, {
req,
res,
fetch: adapter,
});
}
return res;
} catch (e) {
console.error('请求在响应后处理时发生错误', e, req);
return wrapperError({
code: 500,
message: '请求在响应后处理时发生错误',
// @ts-ignore
req: req,
});
}
});
}
/**
* 设置请求前处理,设置请求前处理函数
* @param fn 处理函数
*/
before(fn: DataOpts['beforeRequest']) {
this.beforeRequest = fn;
}
/**
* 设置请求后处理,设置请求后处理函数
* @param fn 处理函数
*/
after(fn: DataOpts['afterResponse']) {
this.afterResponse = fn;
}
async fetchText(urlOrOptions?: string | QueryOpts, options?: QueryOpts): Promise<Result<any>> {
let _options = { method: 'GET' as const, ...options };
if (typeof urlOrOptions === 'string' && !_options.url) {
_options.url = urlOrOptions;
}
if (typeof urlOrOptions === 'object') {
_options = { ...urlOrOptions, ..._options };
}
const headers = { ...this.headers, ..._options.headers };
if (options?.token && !headers['Authorization'] && _options.method !== 'GET') {
headers['Authorization'] = `Bearer ${options.token}`;
}
const res = await adapter({
..._options,
headers,
});
if (res && !res.code) {
return {
code: 200,
data: res,
}
}
return res;
}
}
export { adapter };
export class BaseQuery<T extends Query = Query, R extends { queryChain?: any; query?: any } = { queryChain: any; query?: T }> {
query: T;
queryDefine: R;
constructor(opts?: { query?: T; queryDefine?: R; clientQuery?: T }) {
if (opts?.clientQuery) {
this.query = opts.clientQuery;
} else {
this.query = opts?.query;
}
if (opts.queryDefine) {
this.queryDefine = opts.queryDefine;
this.queryDefine.query = this.query;
}
}
get chain(): R['queryChain'] {
return this.queryDefine.queryChain;
}
post<R = any, P = any>(data: P, options?: DataOpts): Promise<Result<R>> {
return this.query.post(data, options);
}
get<R = any, P = any>(data: P, options?: DataOpts): Promise<Result<R>> {
return this.query.get(data, options);
}
}