import { NoopObject, addTrailingSlash, queryString, removeHeadingSlash, } from "@dage/utils"; import { createHeader } from "./Header"; import { stringReplace, variableReplace } from "./replace"; import { DageRequest, DageRequestConfig, DageResponse, Interceptor, PaginationParams, PaginationResponse, } from "./types"; import { detectContentType, headersToObject, methodsHasBody, serializeBody, serializeResponseBody, } from "./utils"; import { ResponseError } from "./error"; export interface ServiceAdapter { baseURL: string; fetch: typeof fetch; /** * 拦截器 */ interceptor?: Interceptor; /** * 全局变量 */ globalVariables?: Record; } const DEFAULT_INTERCEPTOR: Interceptor = (_, next) => { return next(); }; export function createService() { let adapter: ServiceAdapter; let initialized = false; /** * 是否初始化完成 * @returns */ function isInitialized() { return initialized; } /** * 获取 Base URL */ function getBaseURL() { if (!initialized) { throw new Error("未初始化"); } return adapter.baseURL; } /** * 解析响应 */ function parseResponse( response: DageResponse | PaginationResponse ): DageResponse | PaginationResponse { if (response == null) { throw { message: "未能识别响应", code: -1, }; } if (response.errorMessage) { throw new ResponseError({ ...response, errorMessage: response.errorMessage ?? "请求失败", errorCode: response.errorCode ?? -1, }); } return response; } /** * 初始化 * @param options */ function initial(options: ServiceAdapter) { if (initialized && process.env.NODE_ENV === "development") { console.error("[@dage/service] 已经初始化过了, 不需要重复初始化"); } adapter = options; adapter.baseURL = addTrailingSlash(options.baseURL); initialized = true; } async function normalizeRequest( url: string, body?: P, config: DageRequestConfig = NoopObject ): Promise | DageResponse> { if (!initialized) { throw new Error("请先调用 initial 初始化"); } const method = config.method ?? "POST"; const searchParams: Record = {}; const reqBody = body ?? NoopObject; // 路由参数替换 url = stringReplace(url, reqBody); // 查询字符串处理 const appendSearchParams = ( obj: Record, params = searchParams ) => { Object.assign(params, obj); }; if (config.searchParams) { appendSearchParams(config.searchParams); } // 报头处理 const headers: Record = createHeader(); if (config.headers) { Object.keys(config.headers).forEach((key) => { headers[key] = config.headers![key]; }); } if (methodsHasBody(method) && !headers["Content-Type"]) { const contentType = detectContentType(reqBody); if (contentType) { headers["Content-type"] = contentType; } } const requestPayload: DageRequest = { name: url, method, body: reqBody, searchParams, headers, meta: config.meta ?? NoopObject, }; // 变量替换 if (adapter.globalVariables) { variableReplace(requestPayload.body, adapter.globalVariables); variableReplace(requestPayload.searchParams, adapter.globalVariables); variableReplace(requestPayload.headers, adapter.globalVariables); } const interceptor = adapter.interceptor ?? DEFAULT_INTERCEPTOR; // next 方法被拦截器调用的次数,如果调用次数过多,说明程序存在bug,不排除有无限循环的可能 let nextCallTime = 0; const response = await interceptor(requestPayload, async () => { nextCallTime++; // 最多 3 次 if (nextCallTime > 3) { throw new Error( "拦截器调用 next 次数过多, 请检查代码,可能存在无限循环" ); } if (requestPayload.body && requestPayload.method === "GET") { appendSearchParams(requestPayload.body, requestPayload.searchParams); } // 构建 href let href = url.startsWith("http") ? url : adapter.baseURL + removeHeadingSlash(url); if (Object.keys(requestPayload.searchParams).length) { const temp = queryString.parseUrl(href); href = queryString.stringifyUrl({ url: temp.url, query: { ...temp.query, ...requestPayload.searchParams }, fragmentIdentifier: temp.fragmentIdentifier, }); } /** * 主体处理 */ const finalBody = requestPayload.body && methodsHasBody(requestPayload.method) ? serializeBody(requestPayload.headers, requestPayload.body) : undefined; const res = await adapter.fetch(href, { method: requestPayload.method, headers: requestPayload.headers, body: finalBody, mode: "cors", }); if (!res.ok) { console.error(res); throw new Error(`[@dage/service]请求 ${url} 失败: ${res.status}`); } const resData = await serializeResponseBody(res, config); // @ts-expect-error const clone: DageResponse = { ...resData, // 原始请求信息 __raw__: { statusCode: res.status, data: resData, header: headersToObject(res.headers), }, }; return clone as DageResponse | PaginationResponse; }); // 解析协议 return parseResponse(response); } /** * 请求方法,默认使用 POST 方法 */ async function request( url: string, body?: P, config?: DageRequestConfig ): Promise { return (await normalizeRequest(url, body, config)).data; } const requestByPost = request; async function requestByGet( url: string, body?: P, config?: DageRequestConfig ): Promise { return (await normalizeRequest(url, body, { method: "GET", ...config })) .data; } /** * 列表接口请求, 默认为 POST 请求 */ async function requestPagination( name: string, body?: P, config?: DageRequestConfig ): Promise> { return (await normalizeRequest(name, body, { method: "POST", ...config, })) as PaginationResponse; } return { getBaseURL, parseResponse, initial, isInitialized, request, requestByPost, requestByGet, requestPagination, }; } const DEFAULT_SERVICE = createService(); /** * 获取 Base URL * @returns */ export const getBaseURL = DEFAULT_SERVICE.getBaseURL; /** * 解析响应 * @param response * @returns */ export const parseResponse = DEFAULT_SERVICE.parseResponse; /** * 是否初始化完成 * @returns */ export const isInitialized = DEFAULT_SERVICE.isInitialized; /** * 初始化 * @param options */ export const initial = DEFAULT_SERVICE.initial; export const request = DEFAULT_SERVICE.request; export const requestByPost = DEFAULT_SERVICE.requestByPost; export const requestByGet = DEFAULT_SERVICE.requestByGet; export const requestPagination = DEFAULT_SERVICE.requestPagination;