request.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. import {
  2. NoopObject,
  3. addTrailingSlash,
  4. queryString,
  5. removeHeadingSlash,
  6. } from "@dage/utils";
  7. import { createHeader } from "./Header";
  8. import { stringReplace, variableReplace } from "./replace";
  9. import {
  10. DageRequest,
  11. DageRequestConfig,
  12. DageResponse,
  13. Interceptor,
  14. PaginationParams,
  15. PaginationResponse,
  16. } from "./types";
  17. import {
  18. detectContentType,
  19. headersToObject,
  20. methodsHasBody,
  21. serializeBody,
  22. serializeResponseBody,
  23. } from "./utils";
  24. import { ResponseError } from "./error";
  25. export interface ServiceAdapter {
  26. baseURL: string;
  27. fetch: typeof fetch;
  28. /**
  29. * 拦截器
  30. */
  31. interceptor?: Interceptor;
  32. /**
  33. * 全局变量
  34. */
  35. globalVariables?: Record<string, any>;
  36. }
  37. const DEFAULT_INTERCEPTOR: Interceptor = (_, next) => {
  38. return next();
  39. };
  40. export function createService() {
  41. let adapter: ServiceAdapter;
  42. let initialized = false;
  43. /**
  44. * 是否初始化完成
  45. * @returns
  46. */
  47. function isInitialized() {
  48. return initialized;
  49. }
  50. /**
  51. * 获取 Base URL
  52. */
  53. function getBaseURL() {
  54. if (!initialized) {
  55. throw new Error("未初始化");
  56. }
  57. return adapter.baseURL;
  58. }
  59. /**
  60. * 解析响应
  61. */
  62. function parseResponse(
  63. response: DageResponse | PaginationResponse
  64. ): DageResponse | PaginationResponse {
  65. if (response == null) {
  66. throw {
  67. message: "未能识别响应",
  68. code: -1,
  69. };
  70. }
  71. if (response.errorMessage) {
  72. throw new ResponseError({
  73. ...response,
  74. errorMessage: response.errorMessage ?? "请求失败",
  75. errorCode: response.errorCode ?? -1,
  76. });
  77. }
  78. return response;
  79. }
  80. /**
  81. * 初始化
  82. * @param options
  83. */
  84. function initial(options: ServiceAdapter) {
  85. if (initialized && process.env.NODE_ENV === "development") {
  86. console.error("[@dage/service] 已经初始化过了, 不需要重复初始化");
  87. }
  88. adapter = options;
  89. adapter.baseURL = addTrailingSlash(options.baseURL);
  90. initialized = true;
  91. }
  92. async function normalizeRequest<R = any, P extends {} = any>(
  93. url: string,
  94. body?: P,
  95. config: DageRequestConfig = NoopObject
  96. ): Promise<PaginationResponse<R> | DageResponse<R>> {
  97. if (!initialized) {
  98. throw new Error("请先调用 initial 初始化");
  99. }
  100. const method = config.method ?? "POST";
  101. const searchParams: Record<string, string> = {};
  102. const reqBody = body ?? NoopObject;
  103. // 路由参数替换
  104. url = stringReplace(url, reqBody);
  105. // 查询字符串处理
  106. const appendSearchParams = (
  107. obj: Record<string, string>,
  108. params = searchParams
  109. ) => {
  110. Object.assign(params, obj);
  111. };
  112. if (config.searchParams) {
  113. appendSearchParams(config.searchParams);
  114. }
  115. // 报头处理
  116. const headers: Record<string, string> = createHeader();
  117. if (config.headers) {
  118. Object.keys(config.headers).forEach((key) => {
  119. headers[key] = config.headers![key];
  120. });
  121. }
  122. if (methodsHasBody(method) && !headers["Content-Type"]) {
  123. const contentType = detectContentType(reqBody);
  124. if (contentType) {
  125. headers["Content-type"] = contentType;
  126. }
  127. }
  128. const requestPayload: DageRequest = {
  129. name: url,
  130. method,
  131. body: reqBody,
  132. searchParams,
  133. headers,
  134. meta: config.meta ?? NoopObject,
  135. };
  136. // 变量替换
  137. if (adapter.globalVariables) {
  138. variableReplace(requestPayload.body, adapter.globalVariables);
  139. variableReplace(requestPayload.searchParams, adapter.globalVariables);
  140. variableReplace(requestPayload.headers, adapter.globalVariables);
  141. }
  142. const interceptor = adapter.interceptor ?? DEFAULT_INTERCEPTOR;
  143. // next 方法被拦截器调用的次数,如果调用次数过多,说明程序存在bug,不排除有无限循环的可能
  144. let nextCallTime = 0;
  145. const response = await interceptor(requestPayload, async () => {
  146. nextCallTime++;
  147. // 最多 3 次
  148. if (nextCallTime > 3) {
  149. throw new Error(
  150. "拦截器调用 next 次数过多, 请检查代码,可能存在无限循环"
  151. );
  152. }
  153. if (requestPayload.body && requestPayload.method === "GET") {
  154. appendSearchParams(requestPayload.body, requestPayload.searchParams);
  155. }
  156. // 构建 href
  157. let href = url.startsWith("http")
  158. ? url
  159. : adapter.baseURL + removeHeadingSlash(url);
  160. if (Object.keys(requestPayload.searchParams).length) {
  161. const temp = queryString.parseUrl(href);
  162. href = queryString.stringifyUrl({
  163. url: temp.url,
  164. query: { ...temp.query, ...requestPayload.searchParams },
  165. fragmentIdentifier: temp.fragmentIdentifier,
  166. });
  167. }
  168. /**
  169. * 主体处理
  170. */
  171. const finalBody =
  172. requestPayload.body && methodsHasBody(requestPayload.method)
  173. ? serializeBody(requestPayload.headers, requestPayload.body)
  174. : undefined;
  175. const res = await adapter.fetch(href, {
  176. method: requestPayload.method,
  177. headers: requestPayload.headers,
  178. body: finalBody,
  179. mode: "cors",
  180. });
  181. if (!res.ok) {
  182. console.error(res);
  183. throw new Error(`[@dage/service]请求 ${url} 失败: ${res.status}`);
  184. }
  185. const resData = await serializeResponseBody(res, config);
  186. // @ts-expect-error
  187. const clone: DageResponse = {
  188. ...resData,
  189. // 原始请求信息
  190. __raw__: {
  191. statusCode: res.status,
  192. data: resData,
  193. header: headersToObject(res.headers),
  194. },
  195. };
  196. return clone as DageResponse | PaginationResponse;
  197. });
  198. // 解析协议
  199. return parseResponse(response);
  200. }
  201. /**
  202. * 请求方法,默认使用 POST 方法
  203. */
  204. async function request<R = any, P extends {} = any>(
  205. url: string,
  206. body?: P,
  207. config?: DageRequestConfig
  208. ): Promise<R> {
  209. return (await normalizeRequest(url, body, config)).data;
  210. }
  211. const requestByPost = request;
  212. async function requestByGet<R = any, P extends {} = any>(
  213. url: string,
  214. body?: P,
  215. config?: DageRequestConfig
  216. ): Promise<R> {
  217. return (await normalizeRequest(url, body, { method: "GET", ...config }))
  218. .data;
  219. }
  220. /**
  221. * 列表接口请求, 默认为 POST 请求
  222. */
  223. async function requestPagination<R = any, P extends PaginationParams = any>(
  224. name: string,
  225. body?: P,
  226. config?: DageRequestConfig
  227. ): Promise<PaginationResponse<R>> {
  228. return (await normalizeRequest(name, body, {
  229. method: "POST",
  230. ...config,
  231. })) as PaginationResponse<R>;
  232. }
  233. return {
  234. getBaseURL,
  235. parseResponse,
  236. initial,
  237. isInitialized,
  238. request,
  239. requestByPost,
  240. requestByGet,
  241. requestPagination,
  242. };
  243. }
  244. const DEFAULT_SERVICE = createService();
  245. /**
  246. * 获取 Base URL
  247. * @returns
  248. */
  249. export const getBaseURL = DEFAULT_SERVICE.getBaseURL;
  250. /**
  251. * 解析响应
  252. * @param response
  253. * @returns
  254. */
  255. export const parseResponse = DEFAULT_SERVICE.parseResponse;
  256. /**
  257. * 是否初始化完成
  258. * @returns
  259. */
  260. export const isInitialized = DEFAULT_SERVICE.isInitialized;
  261. /**
  262. * 初始化
  263. * @param options
  264. */
  265. export const initial = DEFAULT_SERVICE.initial;
  266. export const request = DEFAULT_SERVICE.request;
  267. export const requestByPost = DEFAULT_SERVICE.requestByPost;
  268. export const requestByGet = DEFAULT_SERVICE.requestByGet;
  269. export const requestPagination = DEFAULT_SERVICE.requestPagination;