HttpClient.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import { API_BASE_URL } from '../api';
  2. import { LOCAL_SIGN } from '../constants/Localkey';
  3. import { localStorageGet } from '../utils/Helper';
  4. import { getComParams } from './CommonParams';
  5. import {
  6. ApiEndpoint,
  7. ApiEnvelope,
  8. ApiParams,
  9. HttpError,
  10. HttpMethod,
  11. } from './RequestTypes';
  12. export interface ApiCodec {
  13. encode(json: string): string;
  14. decode(payload: string): string;
  15. }
  16. export interface HttpClientOptions {
  17. baseUrl: string;
  18. timeoutMs?: number;
  19. retryCount?: number;
  20. retryDelayMs?: number;
  21. compress?: boolean;
  22. codec?: ApiCodec;
  23. }
  24. export class HttpClient {
  25. private readonly timeoutMs: number;
  26. private readonly retryCount: number;
  27. private readonly retryDelayMs: number;
  28. constructor(private readonly options: HttpClientOptions) {
  29. this.timeoutMs = options.timeoutMs ?? 10_000;
  30. this.retryCount = options.retryCount ?? 2;
  31. this.retryDelayMs = options.retryDelayMs ?? 1_000;
  32. }
  33. get<T>(path: string, query?: ApiParams): Promise<ApiEnvelope<T>> {
  34. return this.request<T>('GET', path, query);
  35. }
  36. post<T>(path: string, body?: ApiParams): Promise<ApiEnvelope<T>> {
  37. return this.request<T>('POST', path, body);
  38. }
  39. call<T>(endpoint: ApiEndpoint, params?: ApiParams): Promise<ApiEnvelope<T>> {
  40. return this.request<T>(endpoint.method, endpoint.path, params);
  41. }
  42. async request<T>(method: HttpMethod, path: string, params: ApiParams = {}): Promise<ApiEnvelope<T>> {
  43. let lastError: unknown;
  44. for (let attempt = 0; attempt <= this.retryCount; attempt += 1) {
  45. try {
  46. return await this.send<T>(method, path, params);
  47. } catch (error) {
  48. lastError = error;
  49. if (attempt >= this.retryCount) {
  50. throw error;
  51. }
  52. await this.delay(this.retryDelayMs);
  53. }
  54. }
  55. throw lastError;
  56. }
  57. private send<T>(method: HttpMethod, path: string, params: ApiParams): Promise<ApiEnvelope<T>> {
  58. const comParams = getComParams();
  59. Object.assign(params, comParams);
  60. const url = this.resolveUrl(path);
  61. const useCompression = Boolean(this.options.compress && this.options.codec);
  62. const rawJson = JSON.stringify(params);
  63. const payload = useCompression ? { lz: this.options.codec!.encode(rawJson) } : params;
  64. const requestUrl = method === 'GET' ? this.appendQuery(url, payload) : url;
  65. return new Promise((resolve, reject) => {
  66. const xhr = new XMLHttpRequest();
  67. xhr.open(method, requestUrl, true);
  68. xhr.timeout = this.timeoutMs;
  69. xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
  70. if (method === 'POST') {
  71. xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
  72. }
  73. xhr.onload = () => {
  74. if (xhr.status < 200 || xhr.status >= 300) {
  75. reject(new HttpError(`HTTP ${xhr.status}`, xhr.status, requestUrl));
  76. return;
  77. }
  78. try {
  79. let text = xhr.responseText;
  80. if (useCompression && text && !text.replace(/^\s+/, '').startsWith('{')) {
  81. text = this.options.codec!.decode(text);
  82. }
  83. resolve(JSON.parse(text) as ApiEnvelope<T>);
  84. } catch (error) {
  85. reject(new HttpError(
  86. `Invalid API response: ${error instanceof Error ? error.message : String(error)}`,
  87. xhr.status,
  88. requestUrl,
  89. ));
  90. }
  91. };
  92. xhr.onerror = () => reject(new HttpError('Network request failed', xhr.status, requestUrl));
  93. xhr.ontimeout = () => reject(new HttpError('Network request timed out', 0, requestUrl));
  94. xhr.onabort = () => reject(new HttpError('Network request aborted', 0, requestUrl));
  95. xhr.send(method === 'POST' ? JSON.stringify(payload) : null);
  96. });
  97. }
  98. private resolveUrl(path: string): string {
  99. return `${this.options.baseUrl.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
  100. }
  101. private appendQuery(url: string, params: ApiParams): string {
  102. const query: string[] = [];
  103. for (const key of Object.keys(params)) {
  104. const value = params[key];
  105. if (value === undefined || value === null || value === '') continue;
  106. const encoded = typeof value === 'object' ? JSON.stringify(value) : String(value);
  107. query.push(`${encodeURIComponent(key)}=${encodeURIComponent(encoded)}`);
  108. }
  109. return query.length > 0 ? `${url}?${query.join('&')}` : url;
  110. }
  111. private delay(ms: number): Promise<void> {
  112. return new Promise((resolve) => setTimeout(resolve, ms));
  113. }
  114. }
  115. /**
  116. * 全局 HTTP 请求对象
  117. */
  118. export const Http = new HttpClient({
  119. baseUrl: API_BASE_URL,
  120. timeoutMs: 10_000,
  121. retryCount: 2,
  122. retryDelayMs: 1_000,
  123. });