request.ts 7.1 KB

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