import logger from "@egg/logger" import { Logger } from "winston" interface NetRequestParams { url: string method: string queryParams?: any payload?: any additionalHeaders?: Record } interface NetErrorDetail { response: Response | null code: number message: string data?: any } export class NetError extends Error { public response: Response | null public code: number public message: string public data?: any constructor({ response, code, message, data }: NetErrorDetail) { super(message) this.response = response this.code = code this.message = message this.data = data } } /** * 网络工具类,提供发送HTTP请求的方法。 */ class NetToolBase { protected prefix: string protected headers: Record protected getHeaders: () => Promise> protected logger: Logger protected requestId: string /** * 创建一个网络工具类实例。 * * @param {Object} params - 构造函数参数。 * @param {string} [params.prefix] - URL前缀。 * @param {Record} [params.headers] - 默认请求头。 * @param {Function} [params.getHeaders] - 获取请求头的方法。 * @param {string} [params.requestId] - 请求ID。 */ constructor({ prefix, headers, getHeaders, requestId, }: { prefix?: string headers?: Record getHeaders?: () => Promise> requestId?: string } = {}) { this.prefix = prefix || "" this.headers = headers || {} this.getHeaders = getHeaders || (async () => ({})) this.requestId = requestId || "" this.logger = logger.child({ requestId }) } /** * 创建一个网络工具类实例。 * * @param {Object} params - 构造函数参数。 * @param {string} [params.prefix] - URL前缀。 * @param {Record} [params.headers] - 默认请求头。 * @param {Function} [params.getHeaders] - 获取请求头的方法。 * @param {string} [params.requestId] - 请求ID。 * @returns 一个网络工具类实例。 */ child({ prefix, headers, getHeaders, requestId, }: { prefix?: string headers?: Record getHeaders?: () => Promise> requestId?: string } = {}) { return new NetToolBase({ prefix: prefix || this.prefix, headers: headers || this.headers, getHeaders: getHeaders || this.getHeaders, requestId: requestId || this.requestId, }) } /** * 记录响应详情并返回响应日志对象。 * @param {string} url - 请求地址。 * @param {string} method - 请求使用的HTTP方法。 * @param {Record} headers - 请求头。 * @param {any} requestBody - 请求体。 * @param {any} responseBody - 响应体。 * @param {number} requestTime - 请求时间。 * @param {Response} [response] - 响应对象。 * @returns 响应日志对象。 */ private logResponse( url: string, method: string, headers: Record, requestBody: any, responseBody: any, requestTime: number, response?: Response ) { const responseLog = { ok: response?.ok, status: response?.status, statusText: response?.statusText, url, method: method, requestHeaders: headers, responseHeaders: response?.headers, requestBody, responseBody, requestTime, responseTime: new Date().getTime(), } this.logger.http(JSON.stringify(responseLog, null, 2)) return responseLog } /** * 发送网络请求并返回一个解析为响应数据的Promise。 * @param url - 要发送请求的URL。 * @param method - 请求使用的HTTP方法。 * @param queryParams - 要包含在URL中的查询参数。 * @param payload - 请求的有效负载数据。 * @param additionalHeaders - 要包含在请求中的附加头。 * @returns 一个解析为响应数据的Promise。 * @throws 如果网络响应不成功或存在解析错误,则抛出错误。 */ protected async request({ url, method, queryParams, payload, additionalHeaders, }: NetRequestParams): Promise { // 拼接完整的URL let fullUrl = `${this.prefix}${url}` if (queryParams) { if (typeof queryParams === "string") { fullUrl = `${fullUrl}?${queryParams}` } else { const queryString = new URLSearchParams(queryParams).toString() if (queryString) fullUrl = `${fullUrl}?${queryString}` } } // 设置请求头 const headers = { ...this.headers, ...(await this.getHeaders()), ...(additionalHeaders || {}), } as Record // 设置请求Header if (!(payload instanceof FormData)) { headers["Content-Type"] = "application/json" } // 处理请求数据 const body = payload instanceof FormData ? payload : JSON.stringify(payload) // 获取响应数据 let resData: any = null let resText: string = "" let res: Response // 开始计时 const requestTime = new Date().getTime() try { res = await fetch(fullUrl, { method, body, headers, }) try { resText = await res.text() resData = JSON.parse(resText) } catch { /* empty */ } } catch (error: any) { // 网络请求异常,如请求超时、DNS解析失败、CORS请求被阻止等 this.logResponse( fullUrl, method, headers, payload, resData || resText, requestTime ) throw new NetError({ response: null, code: 1, message: error.message || "网络请求异常", }) } // 记录响应 this.logResponse( fullUrl, method, headers, payload, resData || resText, requestTime, res ) // http 错误码异常 if (!res.ok) { throw new NetError({ response: res, code: resData.code || res.status, message: resData.message || resData.msg || resText || res.statusText, data: resData.data, }) } // http 错误码正常,但解析异常 if (!resData) { throw new NetError({ response: res, code: 1, message: "解析响应数据异常", }) } // 响应数据异常 if ("code" in resData && resData.code !== 0) { throw new NetError({ response: res, code: resData.code, message: resData.message || resData.msg || "网络请求失败", }) } return resData as T } /** * 发送GET请求并返回一个解析为响应数据的Promise。 * * @param url - 要发送请求的URL。 * @param queryParams - 要包含在URL中的查询参数。 * @param additionalHeaders - 要包含在请求中的附加头。 * @returns 一个解析为响应数据的Promise。 */ protected get( url: string, queryParams?: any, additionalHeaders?: any ): Promise { return this.request({ url, method: "get", queryParams, additionalHeaders }) } /** * 发送POST请求并返回一个解析为响应数据的Promise。 * * @param url - 要发送请求的URL。 * @param payload - 请求的有效负载数据。 * @param queryParams - 要包含在URL中的查询参数。 * @param additionalHeaders - 要包含在请求中的附加头。 * @returns 一个解析为响应数据的Promise。 */ protected post( url: string, payload?: any, queryParams?: any, additionalHeaders?: any ): Promise { return this.request({ url, method: "post", payload, queryParams, additionalHeaders, }) } /** * 发送PUT请求并返回一个解析为响应数据的Promise。 * * @param url - 要发送请求的URL。 * @param payload - 请求的有效负载数据。 * @param queryParams - 要包含在URL中的查询参数。 * @param additionalHeaders - 要包含在请求中的附加头。 * @returns 一个解析为响应数据的Promise。 */ protected put( url: string, payload: any, queryParams?: any, additionalHeaders?: any ): Promise { return this.request({ url, method: "put", payload, queryParams, additionalHeaders, }) } /** * 发送DELETE请求并返回一个解析为响应数据的Promise。 * * @param url - 要发送请求的URL。 * @param payload - 请求的有效负载数据。 * @param queryParams - 要包含在URL中的查询参数。 * @param additionalHeaders - 要包含在请求中的附加头。 * @returns 一个解析为响应数据的Promise。 */ protected del( url: string, payload: any, queryParams?: any, additionalHeaders?: any ): Promise { return this.request({ url, method: "delete", payload, queryParams, additionalHeaders, }) } /** * 发送PATCH请求并返回一个解析为响应数据的Promise。 * * @param url - 要发送请求的URL。 * @param payload - 请求的有效负载数据。 * @param queryParams - 要包含在URL中的查询参数。 * @param additionalHeaders - 要包含在请求中的附加头。 * @returns 一个解析为响应数据的Promise。 */ protected patch( url: string, payload: any, queryParams?: any, additionalHeaders?: any ): Promise { return this.request({ url, method: "patch", payload, queryParams, additionalHeaders, }) } } class NetTool extends NetToolBase { public request({ url, method, queryParams, payload, additionalHeaders, }: NetRequestParams): Promise { return super.request({ url, method, queryParams, payload, additionalHeaders, }) } public get( url: string, queryParams?: any, additionalHeaders?: any ): Promise { return super.get(url, queryParams, additionalHeaders) } public post( url: string, payload?: any, queryParams?: any, additionalHeaders?: any ): Promise { return super.post(url, payload, queryParams, additionalHeaders) } public put( url: string, payload: any, queryParams?: any, additionalHeaders?: any ): Promise { return super.put(url, payload, queryParams, additionalHeaders) } public del( url: string, payload: any, queryParams?: any, additionalHeaders?: any ): Promise { return super.del(url, payload, queryParams, additionalHeaders) } public patch( url: string, payload: any, queryParams?: any, additionalHeaders?: any ): Promise { return super.patch(url, payload, queryParams, additionalHeaders) } /** * 创建一个表示400 Bad Request的响应对象。 * * @param message - 错误消息。 * @returns 一个表示400 Bad Request的响应对象。 */ badRequest(message: string) { this.logger.error(`return a bad request response: ${message}`) return Response.json( { code: 400, message, requestId: this.requestId }, { status: 400 } ) } /** * 创建一个表示404 Not Found的响应对象。 * * @param message - 错误消息。 * @returns 一个表示404 Not Found的响应对象。 */ notFound(message: string) { this.logger.error(`return a not found response: ${message}`) return Response.json( { code: 404, message, requestId: this.requestId }, { status: 404 } ) } /** * 创建一个表示500 Internal Server Error的响应对象。 * * @param message - 错误消息。 * @param data - 错误数据。 * @returns 一个表示500 Internal Server Error的响应对象。 */ serverError(message: string, data?: any) { this.logger.error(`return a server error response: ${message}`) return Response.json( { code: 500, message, data, requestId: this.requestId }, { status: 500 } ) } /** * 创建一个表示200 OK的响应对象。 * * @param data - 响应数据。 * @returns 一个表示200 OK的响应对象。 */ ok(data?: any) { this.logger.info(`return a ok response: ${JSON.stringify(data)}`) return Response.json({ code: 0, message: "success", data, requestId: this.requestId, }) } /** * 创建一个表示200 OK的健康检查响应对象。 * * @param message - 响应消息。 * @returns 一个表示200 OK的健康检查响应对象。 */ healthCheck(message = "success") { return Response.json({ code: 0, message }) } } export { NetTool, NetToolBase }