index.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import type { SimpleAlovaOptions } from './types';
  2. import { useAppConfig } from '@vben/hooks';
  3. import { useAccessStore } from '@vben/stores';
  4. import { createServerTokenAuthentication } from 'alova/client';
  5. import JsonBigint from 'json-bigint';
  6. import { createAlovaClient } from './alova';
  7. /** 超出 JS 安全整数范围的 id 等字段在 JSON 解析时保留为字符串,避免精度丢失 */
  8. const parseJsonBody = JsonBigint({ storeAsString: true }).parse;
  9. const { requestBaseURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
  10. const BINARY_DOWNLOAD_CONTENT_TYPES = new Set([
  11. 'application/octet-stream',
  12. 'application/vnd.ms-excel',
  13. 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  14. ]);
  15. function decodeContentDispositionFileName(value: string): string {
  16. try {
  17. return decodeURIComponent(value.trim());
  18. } catch {
  19. return value.trim();
  20. }
  21. }
  22. function parseContentDispositionFileName(
  23. disposition?: string | null,
  24. fallbackUrl?: string,
  25. ): string {
  26. if (disposition) {
  27. const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
  28. if (utf8Match?.[1]) {
  29. return decodeContentDispositionFileName(utf8Match[1]);
  30. }
  31. const fileNameMatch = disposition.match(/filename=["']?([^"';]+)["']?/i);
  32. if (fileNameMatch?.[1]) {
  33. return decodeContentDispositionFileName(fileNameMatch[1]);
  34. }
  35. }
  36. const urlFileName = fallbackUrl?.match(/[^/?#]+(?=$|[?#])/i)?.[0];
  37. return urlFileName ? decodeContentDispositionFileName(urlFileName) : 'download';
  38. }
  39. function isBinaryDownloadContentType(contentType?: string): boolean {
  40. if (!contentType) return false;
  41. return (
  42. BINARY_DOWNLOAD_CONTENT_TYPES.has(contentType) ||
  43. contentType.startsWith('application/vnd.openxmlformats-officedocument.')
  44. );
  45. }
  46. export default function createRequestClient(options?: SimpleAlovaOptions) {
  47. const store = options?.tokenStore ?? useAccessStore();
  48. const transform = options?.transform ?? ((body) => body);
  49. const interceptor = options?.interceptor ?? {
  50. async onSuccess(response, method) {
  51. if (response.status > 400) {
  52. const message = response.statusText;
  53. // eslint-disable-next-line no-throw-literal
  54. throw { message };
  55. }
  56. /* prettier-ignore */
  57. if (method.meta?.notParseResponse) return response.clone();
  58. /* prettier-ignore */
  59. const [contentType] = response.headers.get('content-type')?.split(';') ?? [];
  60. if (contentType === 'application/json') {
  61. const text = await response.text();
  62. const body = transform(text ? parseJsonBody(text) : {}, method);
  63. if (method.meta?.notParseResponseBody) return body;
  64. if (body.code === 0) return body.data;
  65. throw body;
  66. }
  67. if (isBinaryDownloadContentType(contentType)) {
  68. return {
  69. fileName: parseContentDispositionFileName(
  70. response.headers.get('content-disposition'),
  71. method.url,
  72. ),
  73. source: await response.blob(),
  74. };
  75. }
  76. return response;
  77. },
  78. };
  79. const instance = createAlovaClient({
  80. id: options?.id,
  81. baseURL:
  82. options?.baseURL ??
  83. requestBaseURL[options?.id?.toString().toLocaleLowerCase() ?? 'url'] ??
  84. requestBaseURL.url,
  85. tokenAuthentication: createServerTokenAuthentication({
  86. refreshTokenOnSuccess: {
  87. async isExpired(response) {
  88. return response.status === 401;
  89. },
  90. async handler(response) {
  91. if (store.refreshToken) {
  92. console.log(response);
  93. }
  94. },
  95. metaMatches: {
  96. authRole: 'refreshToken',
  97. refreshToken: true,
  98. },
  99. },
  100. login: {
  101. async handler(response, method) {
  102. const data = await interceptor.onSuccess?.(response.clone(), method);
  103. store.loginExpired = false;
  104. store.accessToken = data.accessToken;
  105. store.refreshToken = data.refreshToken;
  106. },
  107. metaMatches: {
  108. authRole: 'login',
  109. login: true,
  110. },
  111. },
  112. logout: {
  113. async handler() {
  114. store.loginExpired = true;
  115. store.accessToken = null;
  116. store.refreshToken = null;
  117. },
  118. metaMatches: {
  119. authRole: 'logout',
  120. logout: true,
  121. },
  122. },
  123. assignToken(method) {
  124. method.config.headers.Authorization ??= store.accessToken;
  125. },
  126. visitorMeta: {
  127. authRole: 'none',
  128. visitor: true,
  129. },
  130. }),
  131. cacheLogger: import.meta.env.DEV,
  132. });
  133. for (const [type, _interceptor] of Object.entries(interceptor)) {
  134. instance.interceptor(<any>type.slice(2).toLowerCase(), _interceptor);
  135. }
  136. return instance;
  137. }
  138. export * from './alova';
  139. export * from './composables';
  140. export * from './types';
  141. export * from 'alova/client';