@be-link/http 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1,6 +1,319 @@
1
1
  import axios from 'axios';
2
2
  import CryptoJS from 'crypto-js';
3
3
 
4
+ /**
5
+ * Axios 适配器
6
+ *
7
+ * 使用 Axios 实现 HttpAdapter 接口
8
+ *
9
+ * @module adapters/AxiosAdapter
10
+ */
11
+ /**
12
+ * Axios 适配器
13
+ *
14
+ * 将 Axios 封装为统一的 HttpAdapter 接口
15
+ */
16
+ class AxiosAdapter {
17
+ /**
18
+ * 创建 Axios 适配器实例
19
+ *
20
+ * @param config - 适配器配置
21
+ */
22
+ constructor(config) {
23
+ this.instance = axios.create({
24
+ baseURL: config.baseURL,
25
+ timeout: config.timeout || 30000,
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ ...(config.headers || {}),
29
+ },
30
+ });
31
+ }
32
+ /**
33
+ * 发起 HTTP 请求
34
+ *
35
+ * @param config - 请求配置
36
+ * @returns Promise 响应数据
37
+ */
38
+ async request(config) {
39
+ const axiosConfig = {
40
+ url: config.url,
41
+ method: config.method || 'GET',
42
+ data: config.data,
43
+ headers: config.headers,
44
+ timeout: config.timeout,
45
+ params: config.params,
46
+ };
47
+ const response = await this.instance.request(axiosConfig);
48
+ return {
49
+ data: response.data,
50
+ status: response.status,
51
+ headers: response.headers,
52
+ };
53
+ }
54
+ /**
55
+ * 获取 Axios 实例
56
+ *
57
+ * 用于直接访问 Axios 实例进行高级操作
58
+ *
59
+ * @returns Axios 实例
60
+ */
61
+ getAxiosInstance() {
62
+ return this.instance;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Taro 适配器
68
+ *
69
+ * 使用 Taro.request 实现 HttpAdapter 接口
70
+ * 适用于 Taro 小程序项目
71
+ *
72
+ * @module adapters/TaroAdapter
73
+ */
74
+ /**
75
+ * Taro 适配器
76
+ *
77
+ * 将 Taro.request 封装为统一的 HttpAdapter 接口
78
+ */
79
+ class TaroAdapter {
80
+ /**
81
+ * 创建 Taro 适配器实例
82
+ *
83
+ * @param config - 适配器配置
84
+ */
85
+ constructor(config) {
86
+ this.baseURL = config.baseURL;
87
+ this.timeout = config.timeout || 30000;
88
+ this.defaultHeaders = {
89
+ 'Content-Type': 'application/json',
90
+ ...(config.headers || {}),
91
+ };
92
+ this.taroRequest = config.taroRequest;
93
+ }
94
+ /**
95
+ * 发起 HTTP 请求
96
+ *
97
+ * @param config - 请求配置
98
+ * @returns Promise 响应数据
99
+ */
100
+ async request(config) {
101
+ const fullUrl = this.resolveUrl(config.url, config.params);
102
+ const res = await this.taroRequest({
103
+ url: fullUrl,
104
+ method: (config.method || 'GET'),
105
+ data: config.data,
106
+ header: {
107
+ ...this.defaultHeaders,
108
+ ...(config.headers || {}),
109
+ },
110
+ timeout: config.timeout || this.timeout,
111
+ });
112
+ return {
113
+ data: res.data,
114
+ status: res.statusCode,
115
+ headers: res.header,
116
+ };
117
+ }
118
+ /**
119
+ * 解析完整 URL
120
+ *
121
+ * 处理相对路径和查询参数
122
+ *
123
+ * @param url - 请求 URL
124
+ * @param params - 查询参数
125
+ * @returns 完整 URL
126
+ */
127
+ resolveUrl(url, params) {
128
+ // 如果是绝对路径,直接使用;否则拼接 baseURL
129
+ const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url}`;
130
+ // 如果没有查询参数,直接返回
131
+ if (!params || Object.keys(params).length === 0) {
132
+ return fullUrl;
133
+ }
134
+ // 序列化查询参数
135
+ const queryParts = [];
136
+ for (const key of Object.keys(params)) {
137
+ const value = params[key];
138
+ if (value !== undefined && value !== null) {
139
+ queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
140
+ }
141
+ }
142
+ if (queryParts.length === 0) {
143
+ return fullUrl;
144
+ }
145
+ const queryString = queryParts.join('&');
146
+ return `${fullUrl}${fullUrl.includes('?') ? '&' : '?'}${queryString}`;
147
+ }
148
+ }
149
+ /**
150
+ * 创建 Taro Fetch 包装器
151
+ *
152
+ * 将 Taro.request 包装为类似 fetch 的函数
153
+ * 用于 TimeSyncService 进行时间同步
154
+ *
155
+ * @param taroRequest - Taro.request 函数引用
156
+ * @returns FetchFunction 类型的函数
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * import Taro from '@tarojs/taro';
161
+ * import { createTaroFetch } from '@be-link/http';
162
+ *
163
+ * const fetchFn = createTaroFetch(Taro.request);
164
+ * const timeSyncService = new TimeSyncService(config, fetchFn);
165
+ * ```
166
+ */
167
+ function createTaroFetch(taroRequest) {
168
+ return async (url, options) => {
169
+ const method = (options?.method?.toUpperCase() || 'GET');
170
+ const res = await taroRequest({
171
+ url,
172
+ method,
173
+ // POST 请求需要传递 data,避免 "Body cannot be empty" 错误
174
+ data: method === 'POST' ? {} : undefined,
175
+ });
176
+ return {
177
+ ok: res.statusCode >= 200 && res.statusCode < 300,
178
+ status: res.statusCode,
179
+ json: async () => res.data,
180
+ };
181
+ };
182
+ }
183
+
184
+ /**
185
+ * 通用拦截器核心逻辑
186
+ *
187
+ * 提供与具体 HTTP 库无关的请求/响应处理函数
188
+ * 支持 Axios 和 Taro.request 适配器复用
189
+ *
190
+ * @module interceptors/core
191
+ */
192
+ /**
193
+ * 处理请求配置
194
+ *
195
+ * 在请求发出前对配置进行处理,包括:
196
+ * 1. 确保时间同步
197
+ * 2. 设置默认 Content-Type
198
+ * 3. 添加用户 ID 到请求头
199
+ * 4. 加密并添加 Token 到请求头
200
+ * 5. 调用自定义请求拦截器
201
+ *
202
+ * @param config - 请求配置
203
+ * @param options - 全局请求选项
204
+ * @param timeSyncService - 时间同步服务(可选)
205
+ * @param encryptionService - 加密服务(可选)
206
+ * @returns 处理后的请求配置
207
+ */
208
+ async function processRequest(config, options, timeSyncService, encryptionService) {
209
+ // 获取 header 名称,使用默认值
210
+ const tokenHeaderName = options.tokenHeaderName || 'X-BeLink-Token';
211
+ const userIdHeaderName = options.userIdHeaderName || 'X-BeLink-UserId';
212
+ // 确保 headers 对象存在
213
+ config.headers = config.headers || {};
214
+ // 步骤 1: 确保时间同步
215
+ // 在请求发出前检查并同步服务器时间,确保后续 Token 加密使用正确的时间戳
216
+ if (timeSyncService) {
217
+ await timeSyncService.ensureSync();
218
+ }
219
+ // 步骤 2: 设置默认 Content-Type
220
+ // 如果请求未指定 Content-Type,默认使用 JSON 格式
221
+ if (!config.headers['Content-Type']) {
222
+ config.headers['Content-Type'] = 'application/json';
223
+ }
224
+ // 步骤 3: 添加用户 ID
225
+ // 调用用户配置的 getUserId 函数获取用户 ID,添加到请求头
226
+ if (options.getUserId) {
227
+ const userId = options.getUserId();
228
+ if (userId) {
229
+ config.headers[userIdHeaderName] = userId;
230
+ }
231
+ }
232
+ // 步骤 4: 添加 Token
233
+ // 根据是否配置了加密服务,决定是否对 Token 进行加密
234
+ if (options.getToken) {
235
+ const token = options.getToken();
236
+ if (token) {
237
+ config.headers[tokenHeaderName] = encryptionService ? encryptionService.encryptToken(token) : token;
238
+ }
239
+ }
240
+ return config;
241
+ }
242
+ /**
243
+ * 处理响应数据
244
+ *
245
+ * 在收到响应后对数据进行处理,调用自定义响应拦截器
246
+ *
247
+ * @param data - 响应数据
248
+ * @param options - 全局请求选项
249
+ * @returns 处理后的响应数据
250
+ */
251
+ function processResponse(data, options) {
252
+ // 调用自定义响应拦截器
253
+ // 允许用户对响应数据进行进一步处理
254
+ if (options.onResponse) {
255
+ return options.onResponse(data);
256
+ }
257
+ return data;
258
+ }
259
+ /**
260
+ * 处理错误
261
+ *
262
+ * 在请求出错时进行处理,支持自定义错误处理和业务错误检查
263
+ *
264
+ * @param error - 错误对象
265
+ * @param options - 全局请求选项
266
+ * @throws 处理后的错误
267
+ */
268
+ function processError(error, options) {
269
+ // 优先调用自定义错误处理
270
+ // 如果用户配置了 onError,由用户自行处理错误
271
+ if (options.onError) {
272
+ const result = options.onError(error);
273
+ // 如果 onError 返回了值而不是抛出错误,我们继续抛出
274
+ throw result ?? error;
275
+ }
276
+ // 获取响应数据(如果有)
277
+ const responseData = error?.response?.data;
278
+ // 检查业务错误
279
+ // 业务错误:HTTP 状态码可能是 200,但响应体中 success === false
280
+ if (responseData && typeof responseData === 'object' && !responseData.success) {
281
+ // 创建业务错误对象
282
+ const bizError = new Error(responseData.message || '接口请求失败');
283
+ // 附加原始响应信息,方便调试
284
+ bizError.response = error.response;
285
+ bizError.data = responseData;
286
+ throw bizError;
287
+ }
288
+ // 其他错误(网络错误、超时等)直接抛出
289
+ throw error;
290
+ }
291
+ /**
292
+ * 检查响应状态并处理业务错误
293
+ *
294
+ * @param data - 响应数据
295
+ * @param status - HTTP 状态码
296
+ * @returns 处理后的响应数据
297
+ */
298
+ function checkResponseStatus(data, status) {
299
+ // 检查 HTTP 状态码
300
+ if (status < 200 || status >= 300) {
301
+ const error = new Error(`HTTP Error: ${status}`);
302
+ error.status = status;
303
+ error.data = data;
304
+ throw error;
305
+ }
306
+ // 检查业务错误(如果响应是标准格式)
307
+ const responseData = data;
308
+ if (responseData && typeof responseData === 'object' && 'success' in responseData && !responseData.success) {
309
+ const bizError = new Error(responseData.message || '接口请求失败');
310
+ bizError.data = responseData;
311
+ bizError.errorType = responseData.errorType;
312
+ throw bizError;
313
+ }
314
+ return data;
315
+ }
316
+
4
317
  /**
5
318
  * 时间同步服务
6
319
  *
@@ -21,7 +334,7 @@ const STORAGE_KEYS = {
21
334
  CLIENT_TIME: 'belink_client_time',
22
335
  };
23
336
  /**
24
- * 检测是否为浏览器环境
337
+ * 检测是否为浏览器环境(支持 localStorage)
25
338
  */
26
339
  function isBrowser() {
27
340
  return typeof window !== 'undefined' && typeof localStorage !== 'undefined';
@@ -67,8 +380,21 @@ function getPerformanceNow() {
67
380
  class TimeSyncService {
68
381
  /**
69
382
  * 创建时间同步服务实例
383
+ *
384
+ * @param config - 时间同步配置
385
+ * @param fetchFn - 自定义 fetch 函数(可选),用于支持 Taro 等非浏览器环境
386
+ *
387
+ * @example
388
+ * ```ts
389
+ * // 使用默认 fetch
390
+ * const timeSync = new TimeSyncService({ syncUrl: 'https://api.example.com/time' });
391
+ *
392
+ * // 使用 Taro.request
393
+ * import { createTaroFetch } from '@be-link/http';
394
+ * const timeSync = new TimeSyncService(config, createTaroFetch(Taro.request));
395
+ * ```
70
396
  */
71
- constructor(config) {
397
+ constructor(config, fetchFn) {
72
398
  /**
73
399
  * 同步时的服务器时间(内存存储)
74
400
  * 用于计算调整后的时间
@@ -89,6 +415,22 @@ class TimeSyncService {
89
415
  syncGapTime: 50 * 1000, // 默认 50 秒
90
416
  ...config,
91
417
  };
418
+ // 记录是否使用了自定义 fetchFn
419
+ this.hasCustomFetch = !!fetchFn;
420
+ // 使用传入的 fetchFn 或默认的 fetch
421
+ this.fetchFn = fetchFn || this.defaultFetch.bind(this);
422
+ }
423
+ /**
424
+ * 默认的 fetch 实现
425
+ * 使用浏览器原生 fetch API
426
+ */
427
+ async defaultFetch(url, options) {
428
+ const response = await fetch(url, options);
429
+ return {
430
+ ok: response.ok,
431
+ status: response.status,
432
+ json: () => response.json(),
433
+ };
92
434
  }
93
435
  /**
94
436
  * 确保时间已同步
@@ -102,7 +444,13 @@ class TimeSyncService {
102
444
  * - 避免多个并发请求同时触发多次时间同步
103
445
  */
104
446
  async ensureSync() {
105
- if (!this.config.enabled || !isBrowser()) {
447
+ // 检查是否启用
448
+ if (!this.config.enabled) {
449
+ return;
450
+ }
451
+ // 环境检测:如果没有自定义 fetchFn 且不是浏览器环境,跳过同步
452
+ // 如果有自定义 fetchFn(如 Taro.request),则允许在任何环境执行
453
+ if (!this.hasCustomFetch && !isBrowser()) {
106
454
  return;
107
455
  }
108
456
  // 如果已有同步请求正在进行,等待它完成
@@ -132,7 +480,12 @@ class TimeSyncService {
132
480
  * - 同步完成后清除锁,允许后续同步请求
133
481
  */
134
482
  async sync() {
135
- if (!isBrowser() || !this.config.syncUrl) {
483
+ // 检查 syncUrl 是否配置
484
+ if (!this.config.syncUrl) {
485
+ return;
486
+ }
487
+ // 环境检测:如果没有自定义 fetchFn 且不是浏览器环境,跳过同步
488
+ if (!this.hasCustomFetch && !isBrowser()) {
136
489
  return;
137
490
  }
138
491
  // 如果已有同步请求正在进行,等待它完成
@@ -157,7 +510,8 @@ class TimeSyncService {
157
510
  try {
158
511
  // 记录请求发起时的 performance.now()
159
512
  const requestStartTime = getPerformanceNow();
160
- const response = await fetch(this.config.syncUrl, { method: 'POST' });
513
+ // 使用注入的 fetch 函数
514
+ const response = await this.fetchFn(this.config.syncUrl, { method: 'POST' });
161
515
  if (!response.ok) {
162
516
  console.error('[TimeSyncService] Sync failed with status:', response.status);
163
517
  this.clear();
@@ -175,11 +529,13 @@ class TimeSyncService {
175
529
  // 存储到内存(使用 performance.now())
176
530
  this.syncServerTime = serverTime + networkDelay;
177
531
  this.syncPerformanceTime = requestEndTime;
178
- // 同时存储到 localStorage(用于调试和日志)
179
- const serverTimeKey = getStorageKey(this.config.storagePrefix, STORAGE_KEYS.SERVER_TIME);
180
- const clientTimeKey = getStorageKey(this.config.storagePrefix, STORAGE_KEYS.CLIENT_TIME);
181
- localStorage.setItem(serverTimeKey, String(this.syncServerTime));
182
- localStorage.setItem(clientTimeKey, String(Date.now()));
532
+ // 同时存储到 localStorage(用于调试和日志),仅在浏览器环境下
533
+ if (isBrowser()) {
534
+ const serverTimeKey = getStorageKey(this.config.storagePrefix, STORAGE_KEYS.SERVER_TIME);
535
+ const clientTimeKey = getStorageKey(this.config.storagePrefix, STORAGE_KEYS.CLIENT_TIME);
536
+ localStorage.setItem(serverTimeKey, String(this.syncServerTime));
537
+ localStorage.setItem(clientTimeKey, String(Date.now()));
538
+ }
183
539
  }
184
540
  catch (error) {
185
541
  console.error('[TimeSyncService] Failed to sync time:', error);
@@ -384,185 +740,16 @@ class EncryptionService {
384
740
  }
385
741
  }
386
742
 
387
- /**
388
- * 请求拦截器
389
- *
390
- * 在请求发出前对请求进行处理,包括:
391
- * - 时间同步
392
- * - 设置默认请求头
393
- * - 添加用户 ID
394
- * - Token 加密和添加
395
- * - 调用自定义拦截器
396
- *
397
- * @module interceptors/request
398
- */
399
- /**
400
- * 设置请求拦截器
401
- *
402
- * 为 Axios 实例添加请求拦截器,处理请求前的通用逻辑
403
- *
404
- * 拦截器执行顺序:
405
- * 1. 确保时间同步(如果配置了 TimeSyncService)
406
- * 2. 设置默认 Content-Type
407
- * 3. 添加用户 ID 到请求头
408
- * 4. 加密并添加 Token 到请求头
409
- * 5. 调用自定义请求拦截器
410
- *
411
- * @param instance - Axios 实例
412
- * @param options - 请求配置选项
413
- * @param timeSyncService - 时间同步服务实例(可选)
414
- * @param encryptionService - 加密服务实例(可选)
415
- *
416
- * @example
417
- * ```ts
418
- * const instance = axios.create({ baseURL: 'https://api.example.com' });
419
- *
420
- * setRequestInterceptor(
421
- * instance,
422
- * {
423
- * baseURL: 'https://api.example.com',
424
- * getToken: () => localStorage.getItem('token'),
425
- * getUserId: () => localStorage.getItem('userId'),
426
- * },
427
- * timeSyncService,
428
- * encryptionService,
429
- * );
430
- * ```
431
- */
432
- function setRequestInterceptor(instance, options, timeSyncService, encryptionService) {
433
- // 获取 header 名称,使用默认值
434
- const tokenHeaderName = options.tokenHeaderName || 'X-BeLink-Token';
435
- const userIdHeaderName = options.userIdHeaderName || 'X-BeLink-UserId';
436
- instance.interceptors.request.use(async (config) => {
437
- // 步骤 1: 确保时间同步
438
- // 在请求发出前检查并同步服务器时间,确保后续 Token 加密使用正确的时间戳
439
- if (timeSyncService) {
440
- await timeSyncService.ensureSync();
441
- }
442
- // 步骤 2: 设置默认 Content-Type
443
- // 如果请求未指定 Content-Type,默认使用 JSON 格式
444
- if (!config.headers['Content-Type']) {
445
- config.headers['Content-Type'] = 'application/json';
446
- }
447
- // 步骤 3: 添加用户 ID
448
- // 调用用户配置的 getUserId 函数获取用户 ID,添加到请求头
449
- if (options.getUserId) {
450
- const userId = options.getUserId();
451
- if (userId) {
452
- config.headers[userIdHeaderName] = userId;
453
- }
454
- }
455
- // 步骤 4: 添加 Token
456
- // 根据是否配置了加密服务,决定是否对 Token 进行加密
457
- if (options.getToken) {
458
- const token = options.getToken();
459
- if (token) {
460
- config.headers[tokenHeaderName] = encryptionService ? encryptionService.encryptToken(token) : token;
461
- }
462
- }
463
- // 步骤 5: 调用自定义请求拦截器
464
- // 允许用户进一步自定义请求配置
465
- if (options.onRequest) {
466
- return options.onRequest(config);
467
- }
468
- return config;
469
- });
470
- }
471
-
472
- /**
473
- * 响应拦截器
474
- *
475
- * 在收到响应后对响应进行处理,包括:
476
- * - 提取响应数据
477
- * - 调用自定义响应拦截器
478
- * - 统一错误处理
479
- *
480
- * @module interceptors/response
481
- */
482
- /**
483
- * 设置响应拦截器
484
- *
485
- * 为 Axios 实例添加响应拦截器,处理响应后的通用逻辑
486
- *
487
- * 成功响应处理:
488
- * 1. 提取响应数据(response.data)
489
- * 2. 调用自定义响应拦截器(如果配置)
490
- * 3. 返回处理后的数据
491
- *
492
- * 错误响应处理:
493
- * 1. 调用自定义错误处理(如果配置)
494
- * 2. 检查业务错误(success === false)
495
- * 3. 抛出错误
496
- *
497
- * @param instance - Axios 实例
498
- * @param options - 请求配置选项
499
- *
500
- * @example
501
- * ```ts
502
- * const instance = axios.create({ baseURL: 'https://api.example.com' });
503
- *
504
- * setResponseInterceptor(instance, {
505
- * baseURL: 'https://api.example.com',
506
- * onResponse: (data) => {
507
- * console.log('响应数据:', data);
508
- * return data;
509
- * },
510
- * onError: (error) => {
511
- * if (error.response?.status === 401) {
512
- * // 处理未授权错误
513
- * window.location.href = '/login';
514
- * }
515
- * throw error;
516
- * },
517
- * });
518
- * ```
519
- */
520
- function setResponseInterceptor(instance, options) {
521
- instance.interceptors.response.use(
522
- // 成功响应处理
523
- (response) => {
524
- // 提取响应数据(Axios 的响应数据在 response.data 中)
525
- const data = response.data;
526
- // 调用自定义响应拦截器
527
- // 允许用户对响应数据进行进一步处理
528
- if (options.onResponse) {
529
- return options.onResponse(data);
530
- }
531
- return data;
532
- },
533
- // 错误响应处理
534
- (error) => {
535
- // 优先调用自定义错误处理
536
- // 如果用户配置了 onError,由用户自行处理错误
537
- if (options.onError) {
538
- return options.onError(error);
539
- }
540
- // 获取响应数据(如果有)
541
- const responseData = error?.response?.data;
542
- // 检查业务错误
543
- // 业务错误:HTTP 状态码可能是 200,但响应体中 success === false
544
- if (responseData && typeof responseData === 'object' && !responseData.success) {
545
- // 创建业务错误对象
546
- const bizError = new Error(responseData.message || '接口请求失败');
547
- // 附加原始响应信息,方便调试
548
- bizError.response = error.response;
549
- bizError.data = responseData;
550
- throw bizError;
551
- }
552
- // 其他错误(网络错误、超时等)直接抛出
553
- throw error;
554
- });
555
- }
556
-
557
743
  /**
558
744
  * BeLinkHttp 请求客户端
559
745
  *
560
746
  * 单例模式的 HTTP 请求客户端
561
747
  * 支持时间同步和 Token 加密
748
+ * 支持 Axios 和 Taro.request 两种请求引擎
562
749
  *
563
750
  * @module BeLinkHttp
564
751
  *
565
- * @example
752
+ * @example Axios 模式(默认)
566
753
  * ```ts
567
754
  * import { beLinkHttp } from '@be-link/http';
568
755
  *
@@ -577,25 +764,49 @@ function setResponseInterceptor(instance, options) {
577
764
  * // 发起请求
578
765
  * const data = await beLinkHttp.get('/api/users');
579
766
  * ```
767
+ *
768
+ * @example Taro 模式
769
+ * ```ts
770
+ * import { beLinkHttp } from '@be-link/http';
771
+ * import Taro from '@tarojs/taro';
772
+ *
773
+ * // 初始化
774
+ * beLinkHttp.init({
775
+ * adapter: 'taro',
776
+ * taroRequest: Taro.request,
777
+ * baseURL: 'https://api.example.com',
778
+ * getToken: () => Taro.getStorageSync('token'),
779
+ * });
780
+ *
781
+ * // 发起请求(使用方式完全一致)
782
+ * const data = await beLinkHttp.get('/api/users');
783
+ * ```
580
784
  */
581
785
  /**
582
786
  * BeLinkHttp 单例类
583
787
  *
584
788
  * 提供统一的 HTTP 请求接口,支持:
789
+ * - 多适配器(Axios / Taro)
585
790
  * - 时间同步
586
791
  * - Token 加密
587
792
  * - 请求/响应拦截
588
793
  */
589
794
  class BeLinkHttp {
590
795
  constructor() {
591
- /** Axios 实例 */
592
- this.instance = null;
796
+ /** HTTP 适配器实例 */
797
+ this.adapter = null;
593
798
  /** 时间同步服务 */
594
799
  this.timeSyncService = null;
595
800
  /** 加密服务 */
596
801
  this.encryptionService = null;
802
+ /** 请求配置选项 */
803
+ this.options = null;
597
804
  /** 是否已初始化 */
598
805
  this.initialized = false;
806
+ /** 动态设置的 Token */
807
+ this._token = null;
808
+ /** 动态设置的 UserId */
809
+ this._userId = null;
599
810
  }
600
811
  /**
601
812
  * 初始化请求客户端
@@ -604,7 +815,7 @@ class BeLinkHttp {
604
815
  *
605
816
  * @param options - 请求配置选项
606
817
  *
607
- * @example
818
+ * @example Axios 模式
608
819
  * ```ts
609
820
  * beLinkHttp.init({
610
821
  * baseURL: 'https://api.example.com',
@@ -620,36 +831,61 @@ class BeLinkHttp {
620
831
  * getUserId: () => localStorage.getItem('userId'),
621
832
  * });
622
833
  * ```
834
+ *
835
+ * @example Taro 模式
836
+ * ```ts
837
+ * import Taro from '@tarojs/taro';
838
+ *
839
+ * beLinkHttp.init({
840
+ * adapter: 'taro',
841
+ * taroRequest: Taro.request,
842
+ * baseURL: 'https://api.example.com',
843
+ * getToken: () => Taro.getStorageSync('token'),
844
+ * });
845
+ * ```
623
846
  */
624
847
  init(options) {
625
- const { baseURL, timeout = 30000, headers = {}, timeSync, encryption } = options;
626
- // 创建 Axios 实例
627
- this.instance = axios.create({
628
- baseURL,
629
- timeout,
630
- headers: {
631
- 'Content-Type': 'application/json',
632
- ...headers,
633
- },
634
- });
848
+ this.options = options;
849
+ const { baseURL, timeout = 30000, headers = {}, timeSync, encryption, adapter = 'axios' } = options;
850
+ // 创建适配器
851
+ if (adapter === 'taro') {
852
+ if (!options.taroRequest) {
853
+ throw new Error('[BeLinkHttp] 使用 taro 适配器时必须传入 taroRequest');
854
+ }
855
+ this.adapter = new TaroAdapter({
856
+ baseURL,
857
+ timeout,
858
+ headers,
859
+ taroRequest: options.taroRequest,
860
+ });
861
+ }
862
+ else {
863
+ this.adapter = new AxiosAdapter({
864
+ baseURL,
865
+ timeout,
866
+ headers,
867
+ });
868
+ }
635
869
  // 创建时间同步服务
636
870
  if (timeSync?.syncUrl) {
637
- this.timeSyncService = new TimeSyncService(timeSync);
871
+ let fetchFn;
872
+ // 如果使用 Taro 适配器,创建 Taro Fetch 包装器
873
+ if (adapter === 'taro' && options.taroRequest) {
874
+ fetchFn = createTaroFetch(options.taroRequest);
875
+ }
876
+ this.timeSyncService = new TimeSyncService(timeSync, fetchFn);
638
877
  }
639
878
  // 创建加密服务
640
879
  if (encryption?.key && encryption?.iv) {
641
880
  this.encryptionService = new EncryptionService(encryption, this.timeSyncService);
642
881
  }
643
- // 设置拦截器
644
- setRequestInterceptor(this.instance, options, this.timeSyncService, this.encryptionService);
645
- setResponseInterceptor(this.instance, options);
646
882
  this.initialized = true;
647
883
  }
648
884
  /**
649
885
  * 检查是否已初始化
650
886
  */
651
887
  checkInitialized() {
652
- if (!this.initialized || !this.instance) {
888
+ if (!this.initialized || !this.adapter) {
653
889
  throw new Error('[BeLinkHttp] 请先调用 init() 方法进行初始化');
654
890
  }
655
891
  }
@@ -657,7 +893,7 @@ class BeLinkHttp {
657
893
  * GET 请求
658
894
  *
659
895
  * @param url - 请求 URL
660
- * @param config - 可选的 Axios 请求配置
896
+ * @param config - 可选的请求配置
661
897
  * @returns Promise 响应数据
662
898
  *
663
899
  * @example
@@ -667,15 +903,14 @@ class BeLinkHttp {
667
903
  * ```
668
904
  */
669
905
  async get(url, config) {
670
- this.checkInitialized();
671
- return this.instance.get(url, config);
906
+ return this.request({ ...config, url, method: 'GET' });
672
907
  }
673
908
  /**
674
909
  * POST 请求
675
910
  *
676
911
  * @param url - 请求 URL
677
912
  * @param data - 请求体数据
678
- * @param config - 可选的 Axios 请求配置
913
+ * @param config - 可选的请求配置
679
914
  * @returns Promise 响应数据
680
915
  *
681
916
  * @example
@@ -684,15 +919,14 @@ class BeLinkHttp {
684
919
  * ```
685
920
  */
686
921
  async post(url, data, config) {
687
- this.checkInitialized();
688
- return this.instance.post(url, data, config);
922
+ return this.request({ ...config, url, method: 'POST', data });
689
923
  }
690
924
  /**
691
925
  * PUT 请求
692
926
  *
693
927
  * @param url - 请求 URL
694
928
  * @param data - 请求体数据
695
- * @param config - 可选的 Axios 请求配置
929
+ * @param config - 可选的请求配置
696
930
  * @returns Promise 响应数据
697
931
  *
698
932
  * @example
@@ -701,15 +935,14 @@ class BeLinkHttp {
701
935
  * ```
702
936
  */
703
937
  async put(url, data, config) {
704
- this.checkInitialized();
705
- return this.instance.put(url, data, config);
938
+ return this.request({ ...config, url, method: 'PUT', data });
706
939
  }
707
940
  /**
708
941
  * PATCH 请求
709
942
  *
710
943
  * @param url - 请求 URL
711
944
  * @param data - 请求体数据
712
- * @param config - 可选的 Axios 请求配置
945
+ * @param config - 可选的请求配置
713
946
  * @returns Promise 响应数据
714
947
  *
715
948
  * @example
@@ -718,14 +951,13 @@ class BeLinkHttp {
718
951
  * ```
719
952
  */
720
953
  async patch(url, data, config) {
721
- this.checkInitialized();
722
- return this.instance.patch(url, data, config);
954
+ return this.request({ ...config, url, method: 'PATCH', data });
723
955
  }
724
956
  /**
725
957
  * DELETE 请求
726
958
  *
727
959
  * @param url - 请求 URL
728
- * @param config - 可选的 Axios 请求配置
960
+ * @param config - 可选的请求配置
729
961
  * @returns Promise 响应数据
730
962
  *
731
963
  * @example
@@ -734,19 +966,18 @@ class BeLinkHttp {
734
966
  * ```
735
967
  */
736
968
  async delete(url, config) {
737
- this.checkInitialized();
738
- return this.instance.delete(url, config);
969
+ return this.request({ ...config, url, method: 'DELETE' });
739
970
  }
740
971
  /**
741
972
  * 通用请求方法
742
973
  *
743
- * @param config - Axios 请求配置
974
+ * @param config - 请求配置
744
975
  * @returns Promise 响应数据
745
976
  *
746
977
  * @example
747
978
  * ```ts
748
979
  * const result = await beLinkHttp.request({
749
- * method: 'post',
980
+ * method: 'POST',
750
981
  * url: '/api/users',
751
982
  * data: { name: '张三' },
752
983
  * headers: { 'X-Custom': 'value' },
@@ -755,7 +986,25 @@ class BeLinkHttp {
755
986
  */
756
987
  async request(config) {
757
988
  this.checkInitialized();
758
- return this.instance.request(config);
989
+ try {
990
+ // 创建带有动态 token/userId 获取函数的 options
991
+ const optionsWithDynamicAuth = {
992
+ ...this.options,
993
+ getToken: () => this.getToken(),
994
+ getUserId: () => this.getUserId(),
995
+ };
996
+ // 处理请求(应用拦截器逻辑)
997
+ const processedConfig = await processRequest({ ...config }, optionsWithDynamicAuth, this.timeSyncService, this.encryptionService);
998
+ // 发起请求
999
+ const response = await this.adapter.request(processedConfig);
1000
+ // 检查响应状态
1001
+ const checkedData = checkResponseStatus(response.data, response.status);
1002
+ // 处理响应
1003
+ return processResponse(checkedData, this.options);
1004
+ }
1005
+ catch (error) {
1006
+ return processError(error, this.options);
1007
+ }
759
1008
  }
760
1009
  /**
761
1010
  * 获取时间同步服务实例
@@ -776,10 +1025,90 @@ class BeLinkHttp {
776
1025
  /**
777
1026
  * 获取 Axios 实例
778
1027
  *
779
- * @returns Axios 实例,未初始化时返回 null
1028
+ * 仅在使用 axios 适配器时有效
1029
+ *
1030
+ * @returns Axios 实例,非 axios 适配器或未初始化时返回 null
780
1031
  */
781
1032
  getAxiosInstance() {
782
- return this.instance;
1033
+ if (this.adapter instanceof AxiosAdapter) {
1034
+ return this.adapter.getAxiosInstance();
1035
+ }
1036
+ return null;
1037
+ }
1038
+ /**
1039
+ * 获取当前适配器类型
1040
+ *
1041
+ * @returns 适配器类型,未初始化时返回 null
1042
+ */
1043
+ getAdapterType() {
1044
+ if (!this.initialized || !this.options) {
1045
+ return null;
1046
+ }
1047
+ return this.options.adapter || 'axios';
1048
+ }
1049
+ /**
1050
+ * 设置 Token
1051
+ *
1052
+ * 动态更新请求时使用的 Token,会覆盖 init 时传入的 getToken 返回值
1053
+ *
1054
+ * @param token - 新的 Token 值,传入 null 则清除
1055
+ *
1056
+ * @example
1057
+ * ```ts
1058
+ * // 登录后设置 Token
1059
+ * beLinkHttp.setToken('new-token-value');
1060
+ *
1061
+ * // 登出时清除 Token
1062
+ * beLinkHttp.setToken(null);
1063
+ * ```
1064
+ */
1065
+ setToken(token) {
1066
+ this._token = token;
1067
+ }
1068
+ /**
1069
+ * 设置 UserId
1070
+ *
1071
+ * 动态更新请求时使用的 UserId,会覆盖 init 时传入的 getUserId 返回值
1072
+ *
1073
+ * @param userId - 新的 UserId 值,传入 null 则清除
1074
+ *
1075
+ * @example
1076
+ * ```ts
1077
+ * // 登录后设置 UserId
1078
+ * beLinkHttp.setUserId('user-123');
1079
+ *
1080
+ * // 登出时清除 UserId
1081
+ * beLinkHttp.setUserId(null);
1082
+ * ```
1083
+ */
1084
+ setUserId(userId) {
1085
+ this._userId = userId;
1086
+ }
1087
+ /**
1088
+ * 获取当前 Token
1089
+ *
1090
+ * 优先返回通过 setToken 设置的值,否则调用 init 时传入的 getToken 函数
1091
+ *
1092
+ * @returns 当前 Token 值
1093
+ */
1094
+ getToken() {
1095
+ if (this._token !== null) {
1096
+ return this._token;
1097
+ }
1098
+ return this.options?.getToken?.() ?? null;
1099
+ }
1100
+ /**
1101
+ * 获取当前 UserId
1102
+ *
1103
+ * 优先返回通过 setUserId 设置的值,否则调用 init 时传入的 getUserId 函数
1104
+ *
1105
+ * @returns 当前 UserId 值
1106
+ */
1107
+ getUserId() {
1108
+ if (this._userId !== null) {
1109
+ return this._userId;
1110
+ }
1111
+ return this.options?.getUserId?.() ?? null;
783
1112
  }
784
1113
  /**
785
1114
  * 重置客户端
@@ -787,10 +1116,13 @@ class BeLinkHttp {
787
1116
  * 清除所有配置和实例,需要重新调用 init() 初始化
788
1117
  */
789
1118
  reset() {
790
- this.instance = null;
1119
+ this.adapter = null;
791
1120
  this.timeSyncService = null;
792
1121
  this.encryptionService = null;
1122
+ this.options = null;
793
1123
  this.initialized = false;
1124
+ this._token = null;
1125
+ this._userId = null;
794
1126
  }
795
1127
  }
796
1128
  /**
@@ -811,4 +1143,4 @@ class BeLinkHttp {
811
1143
  */
812
1144
  const beLinkHttp = new BeLinkHttp();
813
1145
 
814
- export { BeLinkHttp, beLinkHttp, beLinkHttp as default };
1146
+ export { AxiosAdapter, BeLinkHttp, TaroAdapter, beLinkHttp, createTaroFetch, beLinkHttp as default };