@anjianshi/utils 3.0.7 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export * from './lang/index.js';
2
2
  export * from './url.js';
3
- export * from './safe-request.js';
4
3
  export * from './logging/index.js';
package/index.js CHANGED
@@ -1,4 +1,3 @@
1
1
  export * from './lang/index.js';
2
2
  export * from './url.js';
3
- export * from './safe-request.js';
4
3
  export * from './logging/index.js';
package/lang/result.d.ts CHANGED
@@ -32,16 +32,23 @@ declare function failed(message: string, code?: string | number): Failed;
32
32
  declare function failed<T>(message: string, code: string | number | undefined, data: T): Failed<T>;
33
33
  export { failed };
34
34
  /**
35
- * 若传入值为 success,格式化其 data;否则原样返回错误
36
- * 支持传入会返回 Result 的 Promise
35
+ * 若传入值为 Success,格式化其 data;否则原样返回错误。
36
+ *
37
37
  */
38
38
  declare function formatSuccess<T1, T2, FT = void>(value: Result<T1, FT>, formatter: (value: T1) => T2): Result<T2, FT>;
39
39
  declare function formatSuccess<T1, T2, FT = void>(value: Promise<Result<T1, FT>>, formatter: (value: T1) => T2): Promise<Result<T2, FT>>;
40
40
  export { formatSuccess };
41
+ /**
42
+ * 若传入值为 Failed,格式化其内容;否则原样返回。
43
+ * 支持传入会返回 Result 的 Promise。
44
+ */
45
+ declare function formatFailed<T, FT>(value: Result<T>, formatter: (result: Failed) => Failed<FT>): Result<T, FT>;
46
+ declare function formatFailed<T, FT>(value: Promise<Result<T>>, formatter: (result: Failed) => Failed<FT>): Promise<Result<T, FT>>;
47
+ export { formatFailed };
41
48
  /**
42
49
  * 把可能抛出异常的 Promise 转换为返回 Result 的 Promise。
43
50
  * 其中返回的 Failed 对象的 data 是 catch 捕获到的错误对象。
44
51
  *
45
52
  * 通过此函数可避免写一长串嵌套的 try catch 语句。
46
53
  */
47
- export declare function handleException<T>(promise: Promise<T>): Promise<Result<T>>;
54
+ export declare function exceptionToFailed<T>(promise: Promise<T>): Promise<Result<T>>;
package/lang/result.js CHANGED
@@ -25,13 +25,19 @@ function formatSuccess(value, formatter) {
25
25
  return value.success ? success(formatter(value.data)) : value;
26
26
  }
27
27
  export { formatSuccess };
28
+ function formatFailed(value, formatter) {
29
+ if ('then' in value)
30
+ return value.then(finalValue => formatFailed(finalValue, formatter));
31
+ return value.success ? value : formatter(value);
32
+ }
33
+ export { formatFailed };
28
34
  /**
29
35
  * 把可能抛出异常的 Promise 转换为返回 Result 的 Promise。
30
36
  * 其中返回的 Failed 对象的 data 是 catch 捕获到的错误对象。
31
37
  *
32
38
  * 通过此函数可避免写一长串嵌套的 try catch 语句。
33
39
  */
34
- export async function handleException(promise) {
40
+ export async function exceptionToFailed(promise) {
35
41
  return promise.then(data => success(data), (error) => {
36
42
  let message;
37
43
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anjianshi/utils",
3
- "version": "3.0.7",
3
+ "version": "3.1.0",
4
4
  "description": "Common JavaScript Utils",
5
5
  "homepage": "https://github.com/anjianshi/js-packages/utils",
6
6
  "bugs": {
@@ -31,12 +31,12 @@
31
31
  "redis": "^5.5.6",
32
32
  "typescript": "^5.8.3",
33
33
  "vconsole": "^3.15.1",
34
- "@anjianshi/presets-prettier": "3.0.5",
35
34
  "@anjianshi/presets-eslint-node": "6.0.0",
35
+ "@anjianshi/presets-eslint-typescript": "6.0.0",
36
+ "@anjianshi/presets-eslint-base": "6.0.0",
36
37
  "@anjianshi/presets-eslint-react": "6.0.0",
37
38
  "@anjianshi/presets-typescript": "3.2.5",
38
- "@anjianshi/presets-eslint-base": "6.0.0",
39
- "@anjianshi/presets-eslint-typescript": "6.0.0"
39
+ "@anjianshi/presets-prettier": "3.0.5"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@emotion/react": "^11.14.0",
@@ -0,0 +1,35 @@
1
+ import { type Result, type Failed } from '../lang/result.js';
2
+ import { type Logger } from '../logging/index.js';
3
+ import { type RequestError } from './error.js';
4
+ import type { Options, PredefinedOptions, FormattedOptions } from './options.js';
5
+ /** 此基类不可直接使用,因其未对错误格式进行具体约定 */
6
+ export declare abstract class BaseRequestClient<FailedT> {
7
+ readonly logger: Logger;
8
+ readonly prefefinedOptions: PredefinedOptions;
9
+ constructor(options?: PredefinedOptions & {
10
+ loggerName?: string;
11
+ });
12
+ /** 生成一个快捷方式函数,调用它相当于调用 client.request() */
13
+ asFunction(): <T>(inputUrl: string, inputOptions?: Options) => Promise<Result<T, FailedT>>;
14
+ request<T>(inputUrl: string, inputOptions?: Options): Promise<Result<T, FailedT>>;
15
+ formatOptions(input: Options): Promise<FormattedOptions>;
16
+ /** 请求发起前调用此方法补充 Headers 内容 */
17
+ protected getHeaders(options: FormattedOptions, inputOptions: Options): Record<string, string> | undefined | Promise<Record<string, string> | undefined>;
18
+ /**
19
+ * 解析响应内容
20
+ * - 若 options.binary 为 true,返回二进制结果
21
+ * - 若 response Content-Type 为 'text/' 开头,返回文本结果
22
+ * - 尝试解析 JSON,若成功,返回 JSON 结果;
23
+ * - 若 JSON 解析失败:
24
+ * - 若 response Content-Type 为 'application/json' 开头,返回解析失败信息
25
+ * - 其他情况,返回纯文本结果
26
+ */
27
+ protected parseResponse(options: FormattedOptions, response: Response): Promise<Result<unknown, FailedT>>;
28
+ protected handleError(error: RequestError): Failed<FailedT>;
29
+ /** 生成符合 FailedT 约定的失败结果 */
30
+ protected abstract makeFailedResult(error: RequestError): Failed<FailedT>;
31
+ }
32
+ /** 默认的 RequestClient 实现。出现错误时,会把错误对象原样放入错误信息中。 */
33
+ export declare class RequestClient extends BaseRequestClient<RequestError> {
34
+ protected makeFailedResult(error: RequestError): Failed<RequestError>;
35
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * 实现一个功能完善、异常处理逻辑全面的请求发起器。
3
+ * 可通过继承子类来扩展其功能。
4
+ */
5
+ import pick from 'lodash/pick.js';
6
+ import { success, failed, formatFailed, exceptionToFailed, } from '../lang/result.js';
7
+ import { getLogger } from '../logging/index.js';
8
+ import { combineUrl } from '../url.js';
9
+ import { SendRequestFailed, RequestAborted, RequestTimedOut, NonSuccessStatus, ParseResponseBodyFailed, } from './error.js';
10
+ /** 此基类不可直接使用,因其未对错误格式进行具体约定 */
11
+ export class BaseRequestClient {
12
+ logger;
13
+ prefefinedOptions;
14
+ constructor(options = {}) {
15
+ this.logger = getLogger(options.loggerName ?? 'request');
16
+ this.prefefinedOptions = options;
17
+ }
18
+ /** 生成一个快捷方式函数,调用它相当于调用 client.request() */
19
+ asFunction() {
20
+ return async (inputUrl, inputOptions) => this.request(inputUrl, inputOptions);
21
+ }
22
+ // -------------------------------
23
+ // 发起请求
24
+ // -------------------------------
25
+ async request(inputUrl, inputOptions) {
26
+ const options = await this.formatOptions({
27
+ url: inputUrl,
28
+ ...(inputOptions ?? {}),
29
+ });
30
+ const { url, method, headers, body, timeout, signal: manualSignal } = options;
31
+ const timeoutSignal = timeout ? AbortSignal.timeout(timeout) : undefined;
32
+ const signal = manualSignal || timeoutSignal
33
+ ? AbortSignal.any([manualSignal, timeoutSignal].filter(Boolean))
34
+ : undefined;
35
+ // 发起请求
36
+ let response;
37
+ try {
38
+ response = await fetch(url, { method, headers, body, signal });
39
+ }
40
+ catch (error) {
41
+ // 失败情形“手动取消”
42
+ if (manualSignal && manualSignal.aborted && manualSignal.reason === error) {
43
+ const reason = error instanceof DOMException && error.name === 'AbortError' ? null : error;
44
+ return this.handleError(new RequestAborted(reason, options));
45
+ }
46
+ // 失败情形“请求超时”
47
+ if (error instanceof DOMException && error.name === 'TimeoutError') {
48
+ return this.handleError(new RequestTimedOut(options));
49
+ }
50
+ // 失败情形“请求发起失败”
51
+ return this.handleError(new SendRequestFailed(error, options));
52
+ }
53
+ // 失败情形“失败状态码”
54
+ if (!response.status.toString().startsWith('2')) {
55
+ // 此时服务端仍可能输出一些内容,试着解析出来
56
+ const responseDataRes = await this.parseResponse(options, response);
57
+ const responseData = responseDataRes.success ? responseDataRes.data : undefined;
58
+ return this.handleError(new NonSuccessStatus(response, responseData, options));
59
+ }
60
+ // 解析响应内容
61
+ const result = await this.parseResponse(options, response);
62
+ return result;
63
+ }
64
+ // -------------------------------
65
+ // 请求预处理
66
+ // -------------------------------
67
+ async formatOptions(input) {
68
+ const predefined = this.prefefinedOptions;
69
+ const { urlPrefix = predefined.urlPrefix ?? '', url: rawUrl, query = {}, method = predefined.method ?? 'GET', headers: rawHeaders = {}, body: rawBody = null, data, timeout = predefined.timeout ?? 0, binary = false, signal, } = input;
70
+ const headers = {
71
+ ...(predefined.headers ?? {}),
72
+ ...rawHeaders,
73
+ };
74
+ let body = rawBody;
75
+ if (data !== undefined) {
76
+ if (method === 'GET') {
77
+ Object.assign(query, data);
78
+ }
79
+ else {
80
+ body = data instanceof FormData ? data : JSON.stringify(data);
81
+ headers['Content-Type'] = 'application/json; charset=utf-8';
82
+ }
83
+ }
84
+ const url = combineUrl(urlPrefix + (rawUrl ?? ''), query);
85
+ const options = {
86
+ method,
87
+ url,
88
+ headers,
89
+ body,
90
+ timeout,
91
+ binary,
92
+ signal,
93
+ };
94
+ Object.assign(options.headers, await this.getHeaders(options, input));
95
+ return options;
96
+ }
97
+ /** 请求发起前调用此方法补充 Headers 内容 */
98
+ getHeaders(
99
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
100
+ options,
101
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
102
+ inputOptions) {
103
+ return undefined;
104
+ }
105
+ // -------------------------------
106
+ // 请求结果、错误处理
107
+ // -------------------------------
108
+ /**
109
+ * 解析响应内容
110
+ * - 若 options.binary 为 true,返回二进制结果
111
+ * - 若 response Content-Type 为 'text/' 开头,返回文本结果
112
+ * - 尝试解析 JSON,若成功,返回 JSON 结果;
113
+ * - 若 JSON 解析失败:
114
+ * - 若 response Content-Type 为 'application/json' 开头,返回解析失败信息
115
+ * - 其他情况,返回纯文本结果
116
+ */
117
+ async parseResponse(options, response) {
118
+ const contentType = (response.headers.get('Content-Type') ?? '').toLowerCase().trim();
119
+ if (options.binary) {
120
+ const blobResult = await exceptionToFailed(response.blob());
121
+ return formatFailed(blobResult, result => this.handleError(new ParseResponseBodyFailed(result.data, response, options, 'Parse Blob Body Failed')));
122
+ }
123
+ const textResult = await exceptionToFailed(response.text());
124
+ if (contentType.startsWith('text/') || !textResult.success) {
125
+ return formatFailed(textResult, result => this.handleError(new ParseResponseBodyFailed(result.data, response, options, 'Parse Text Body Failed')));
126
+ }
127
+ let jsonParseError;
128
+ try {
129
+ return success(JSON.parse(textResult.data));
130
+ }
131
+ catch (error) {
132
+ jsonParseError = error;
133
+ }
134
+ if (contentType.startsWith('application/json')) {
135
+ return this.handleError(new ParseResponseBodyFailed(jsonParseError, response, options, 'Parse JSON Body Failed'));
136
+ }
137
+ return textResult;
138
+ }
139
+ handleError(error) {
140
+ const info = pick(error.options, ['url', 'method']);
141
+ for (const key of ['originalError', 'status', 'response', 'responseData']) {
142
+ if (key in error)
143
+ Object.assign(info, pick(error, [key]));
144
+ }
145
+ this.logger.error(error.message, error);
146
+ return this.makeFailedResult(error);
147
+ }
148
+ }
149
+ /** 默认的 RequestClient 实现。出现错误时,会把错误对象原样放入错误信息中。 */
150
+ export class RequestClient extends BaseRequestClient {
151
+ makeFailedResult(error) {
152
+ return failed(error.message, undefined, error);
153
+ }
154
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 定义请求失败类型。
3
+ *
4
+ * 有两类失败情形不在处理范围内:
5
+ * 1. client 或子类自身代码报错,例如 formatOptions() 方法意外报错
6
+ * 2. 请求本身成功,但相应内容表达了业务上的失败,例如登录接口返回密码错误。
7
+ * 如有需要,可继承子类来处理这些失败情形。
8
+ */
9
+ import { type FormattedOptions } from './options.js';
10
+ export declare class RequestError extends Error {
11
+ readonly options: FormattedOptions;
12
+ constructor(message: string, options: FormattedOptions);
13
+ }
14
+ /** 手动取消 */
15
+ export declare class RequestAborted extends RequestError {
16
+ /** 取消原因,即调用 abortController.abort() 时传入的值,若未传值则为 null */
17
+ readonly reason: unknown;
18
+ constructor(reason: unknown, options: FormattedOptions);
19
+ }
20
+ /** 请求超时 */
21
+ export declare class RequestTimedOut extends RequestError {
22
+ constructor(options: FormattedOptions);
23
+ }
24
+ /** 请求发起失败(如网络异常) */
25
+ export declare class SendRequestFailed extends RequestError {
26
+ readonly originalError: unknown;
27
+ constructor(originalError: unknown, options: FormattedOptions);
28
+ }
29
+ /** 失败状态码(服务端响应了代表失败的状态码,如 500) */
30
+ export declare class NonSuccessStatus extends RequestError {
31
+ readonly response: Response;
32
+ readonly responseData: unknown;
33
+ readonly status: number;
34
+ constructor(response: Response, responseData: unknown, options: FormattedOptions);
35
+ }
36
+ /** 响应体解析失败 */
37
+ export declare class ParseResponseBodyFailed extends RequestError {
38
+ readonly originalError: unknown;
39
+ readonly response: Response;
40
+ readonly status: number;
41
+ constructor(originalError: unknown, response: Response, options: FormattedOptions, message?: string);
42
+ }
@@ -0,0 +1,54 @@
1
+ export class RequestError extends Error {
2
+ options;
3
+ constructor(message, options) {
4
+ super(message);
5
+ this.options = options;
6
+ }
7
+ }
8
+ /** 手动取消 */
9
+ export class RequestAborted extends RequestError {
10
+ /** 取消原因,即调用 abortController.abort() 时传入的值,若未传值则为 null */
11
+ reason;
12
+ constructor(reason, options) {
13
+ super('Request Aborted', options);
14
+ this.reason = reason;
15
+ }
16
+ }
17
+ /** 请求超时 */
18
+ export class RequestTimedOut extends RequestError {
19
+ constructor(options) {
20
+ super('Request TimedOut', options);
21
+ }
22
+ }
23
+ /** 请求发起失败(如网络异常) */
24
+ export class SendRequestFailed extends RequestError {
25
+ originalError;
26
+ constructor(originalError, options) {
27
+ super('Send Request Failed', options);
28
+ this.originalError = originalError;
29
+ }
30
+ }
31
+ /** 失败状态码(服务端响应了代表失败的状态码,如 500) */
32
+ export class NonSuccessStatus extends RequestError {
33
+ response;
34
+ responseData;
35
+ status;
36
+ constructor(response, responseData, options) {
37
+ super('Non-Success Status', options);
38
+ this.response = response;
39
+ this.responseData = responseData;
40
+ this.status = response.status;
41
+ }
42
+ }
43
+ /** 响应体解析失败 */
44
+ export class ParseResponseBodyFailed extends RequestError {
45
+ originalError;
46
+ response;
47
+ status;
48
+ constructor(originalError, response, options, message) {
49
+ super(message ?? 'Parse Response Body Failed', options);
50
+ this.originalError = originalError;
51
+ this.response = response;
52
+ this.status = response.status;
53
+ }
54
+ }
@@ -0,0 +1,5 @@
1
+ export type * from './options.js';
2
+ export * from './error.js';
3
+ export * as errors from './error.js';
4
+ export * from './client.js';
5
+ export * from './instance.js';
@@ -0,0 +1,4 @@
1
+ export * from './error.js';
2
+ export * as errors from './error.js';
3
+ export * from './client.js';
4
+ export * from './instance.js';
@@ -0,0 +1,4 @@
1
+ import { RequestClient } from './client.js';
2
+ /** 提供一个即取即用的 RequestClient 实例及其函数版本 */
3
+ export declare const client: RequestClient;
4
+ export declare const request: <T>(inputUrl: string, inputOptions?: import("./options.js").Options) => Promise<import("../index.js").Result<T, import("./error.js").RequestError>>;
@@ -0,0 +1,4 @@
1
+ import { RequestClient } from './client.js';
2
+ /** 提供一个即取即用的 RequestClient 实例及其函数版本 */
3
+ export const client = new RequestClient();
4
+ export const request = client.asFunction();
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 请求参数
3
+ */
4
+ export interface Options {
5
+ urlPrefix?: string;
6
+ url?: string;
7
+ query?: Record<string, string | number | undefined>;
8
+ method?: string;
9
+ headers?: Record<string, string>;
10
+ body?: string | FormData | null;
11
+ /**
12
+ * 向后端传递的数据。对于 GET 请求,会合并到 query 中;对于 POST 请求,会作为 POST body,代替 body 参数
13
+ * 注意:为了支持传入 interface 类型的值,Record 只能定义成 Record<string, any>
14
+ */
15
+ data?: FormData | Record<string, any>;
16
+ /** 是否把响应内容作为二进制处理(结果是 blob) */
17
+ binary?: boolean;
18
+ /** 超时时间,不指定或设为 0 代表不限 */
19
+ timeout?: number;
20
+ /** 可通过此信号手动终止请求 */
21
+ signal?: AbortSignal;
22
+ }
23
+ /** 可预指定的请求参数 */
24
+ export type PredefinedOptions = Pick<Options, 'urlPrefix' | 'method' | 'headers' | 'timeout'>;
25
+ /** 经过整理的请求参数(client 内部使用) */
26
+ export type FormattedOptions = Required<Pick<Options, 'url' | 'method' | 'headers' | 'body' | 'timeout' | 'binary'>> & Pick<Options, 'signal'>;
@@ -0,0 +1 @@
1
+ export {};
package/safe-request.d.ts DELETED
@@ -1,53 +0,0 @@
1
- import { type Result } from './lang/result.js';
2
- import { type Logger } from './logging/index.js';
3
- export type { Options as RequestOptions, FormattedOptions as RequestFormattedOptions };
4
- interface Options {
5
- urlPrefix?: string;
6
- url?: string;
7
- query?: Record<string, string | number | undefined>;
8
- method?: string;
9
- headers?: Record<string, string>;
10
- body?: string | FormData | null;
11
- /**
12
- * 向后端传递的数据。对于 GET 请求,会合并到 query 中;对于 POST 请求,会作为 POST body,代替 body 参数
13
- * 注意:为了支持传入 interface 类型的值,Record 只能定义成 Record<string, any>
14
- */
15
- data?: FormData | Record<string, any>;
16
- /** 超时时间,不指定或设为 0 代表不限 */
17
- timeout?: number;
18
- }
19
- type FormattedOptions = Required<Pick<Options, 'url' | 'method' | 'headers' | 'body' | 'timeout'>>;
20
- type PredefinedOptions = Pick<Options, 'urlPrefix' | 'method' | 'headers' | 'timeout'>;
21
- /**
22
- * 建立一个请求发起器,并可预设部分选项。
23
- * 可以继承此类来自定义默认的错误处理逻辑。
24
- *
25
- * 请求失败时的 Failed 对象,其 code 为 HTTP status,没有 status 时为 0
26
- * data 为解析出的响应内容,没有或解析失败则为 undefined
27
- */
28
- export declare class SafeRequestClient {
29
- readonly logger: Logger;
30
- readonly prefefinedOptions: PredefinedOptions;
31
- constructor(options?: PredefinedOptions & {
32
- loggerName?: string;
33
- });
34
- /** 生成一个快捷方式函数,调用它相当于调用 client.request() */
35
- asFunction(): <T>(inputUrl: string, inputOptions?: Options) => Promise<Result<T>>;
36
- request<T>(inputUrl: string, inputOptions?: Options): Promise<Result<T>>;
37
- formatOptions(input: Options): Promise<FormattedOptions>;
38
- /** 请求发起前调用此方法补充 Headers 内容 */
39
- protected getHeaders(options: FormattedOptions, inputOptions: Options): Record<string, string> | undefined | Promise<Record<string, string> | undefined>;
40
- protected parseResponse<T>(options: FormattedOptions, response: Response): Promise<Result<T>>;
41
- /** 若请求未成功发起,会触发此回调来生成失败信息 */
42
- protected onRequestError(error: Error, url: string): import("./lang/result.js").Failed<undefined>;
43
- /** 请求成功发起,但服务端返回失败状态(如 500),会触发此回调来生成失败信息 */
44
- protected onResponseError(url: string, response: Response, responseData: unknown): Promise<import("./lang/result.js").Failed<unknown>>;
45
- /** 服务端返回内容解析失败时,会触发此回调来生成失败信息 */
46
- protected onParseFailed(error: Error, response: Response, url: string): import("./lang/result.js").Failed<undefined>;
47
- /** 处理超时 */
48
- protected onTimeout(url: string): import("./lang/result.js").Failed<undefined>;
49
- }
50
- /**
51
- * 模块自带一个可直接调用发起请求的函数,跳过初始化实例
52
- */
53
- export declare const safeRequest: <T>(inputUrl: string, inputOptions?: Options) => Promise<Result<T>>;
package/safe-request.js DELETED
@@ -1,140 +0,0 @@
1
- import { sleep } from './lang/async.js';
2
- import { failed, handleException } from './lang/result.js';
3
- import { getLogger } from './logging/index.js';
4
- import { combineUrl } from './url.js';
5
- /**
6
- * 建立一个请求发起器,并可预设部分选项。
7
- * 可以继承此类来自定义默认的错误处理逻辑。
8
- *
9
- * 请求失败时的 Failed 对象,其 code 为 HTTP status,没有 status 时为 0
10
- * data 为解析出的响应内容,没有或解析失败则为 undefined
11
- */
12
- export class SafeRequestClient {
13
- logger;
14
- prefefinedOptions;
15
- constructor(options = {}) {
16
- this.logger = getLogger(options.loggerName ?? 'request');
17
- this.prefefinedOptions = options;
18
- }
19
- /** 生成一个快捷方式函数,调用它相当于调用 client.request() */
20
- asFunction() {
21
- return async (inputUrl, inputOptions) => this.request(inputUrl, inputOptions);
22
- }
23
- async request(inputUrl, inputOptions) {
24
- const options = await this.formatOptions({
25
- url: inputUrl,
26
- ...(inputOptions ?? {}),
27
- });
28
- const { url, method, headers, body, timeout } = options;
29
- try {
30
- // 发起请求
31
- const request = fetch(url, { method, headers, body });
32
- let response;
33
- try {
34
- response = await (typeof timeout === 'number'
35
- ? Promise.race([request, sleep(timeout)])
36
- : request);
37
- }
38
- catch (error) {
39
- // 处理“请求发起失败”
40
- return this.onRequestError(error, url);
41
- }
42
- // 处理超时
43
- if (response === undefined) {
44
- return this.onTimeout(url);
45
- }
46
- // 处理“服务端返回失败状态”
47
- if (!response.status.toString().startsWith('2')) {
48
- // 此时服务端仍可能输出一些内容,试着解析出来
49
- const responseDataRes = await this.parseResponse(options, response);
50
- const responseData = responseDataRes.success ? responseDataRes.data : undefined;
51
- return await this.onResponseError(url, response, responseData);
52
- }
53
- // 解析响应内容
54
- return await this.parseResponse(options, response);
55
- }
56
- catch (error) {
57
- this.logger.error('Unexpected error', error);
58
- return failed('Request handle failed.');
59
- }
60
- }
61
- async formatOptions(input) {
62
- const predefined = this.prefefinedOptions;
63
- const { urlPrefix = predefined.urlPrefix ?? '', url: rawUrl, query = {}, method = predefined.method ?? 'GET', headers: rawHeaders = {}, body: rawBody = null, data, timeout = predefined.timeout ?? 0, } = input;
64
- const headers = {
65
- ...(predefined.headers ?? {}),
66
- ...rawHeaders,
67
- };
68
- let body = rawBody;
69
- if (data !== undefined) {
70
- if (method === 'GET') {
71
- Object.assign(query, data);
72
- }
73
- else {
74
- body = data instanceof FormData ? data : JSON.stringify(data);
75
- headers['Content-Type'] = 'application/json; charset=utf-8';
76
- }
77
- }
78
- const url = combineUrl(urlPrefix + (rawUrl ?? ''), query);
79
- const options = {
80
- method,
81
- url,
82
- headers,
83
- body,
84
- timeout,
85
- };
86
- Object.assign(options.headers, await this.getHeaders(options, input));
87
- return options;
88
- }
89
- /** 请求发起前调用此方法补充 Headers 内容 */
90
- getHeaders(
91
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
92
- options,
93
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
94
- inputOptions) {
95
- return undefined;
96
- }
97
- async parseResponse(options, response) {
98
- let result;
99
- result = await handleException(response.json());
100
- if (result.success)
101
- return result;
102
- const contentType = (response.headers.get('Content-Type') ?? '').toLowerCase().trim();
103
- if (contentType.startsWith('text/') || contentType === '') {
104
- result = (await handleException(response.text()));
105
- if (result.success)
106
- return result;
107
- }
108
- result = (await handleException(response.blob()));
109
- return result;
110
- }
111
- /** 若请求未成功发起,会触发此回调来生成失败信息 */
112
- onRequestError(error, url) {
113
- this.logger.error('Request Failed', { url, error });
114
- return failed('Request Failed', 0, undefined);
115
- }
116
- /** 请求成功发起,但服务端返回失败状态(如 500),会触发此回调来生成失败信息 */
117
- // eslint-disable-next-line @typescript-eslint/require-await
118
- async onResponseError(url, response, responseData) {
119
- this.logger.error('Response Error Status', {
120
- url,
121
- status: response.status,
122
- data: responseData,
123
- });
124
- return failed(`Response Error Status - ${response.status}`, response.status, responseData);
125
- }
126
- /** 服务端返回内容解析失败时,会触发此回调来生成失败信息 */
127
- onParseFailed(error, response, url) {
128
- this.logger.error('Response Parse Failed', { url, response, error });
129
- return failed('Response Parse Failed', response.status, undefined);
130
- }
131
- /** 处理超时 */
132
- onTimeout(url) {
133
- this.logger.warn('Request Timeout', url);
134
- return failed('Request Timeout', 0, undefined);
135
- }
136
- }
137
- /**
138
- * 模块自带一个可直接调用发起请求的函数,跳过初始化实例
139
- */
140
- export const safeRequest = new SafeRequestClient().asFunction();