expo-gaode-map-web-api 1.1.4-next.1 → 1.1.4-next.3

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/README.md CHANGED
@@ -55,19 +55,36 @@ npm install expo-gaode-map-web-api
55
55
  import { ExpoGaodeMapModule } from 'expo-gaode-map-navigation';
56
56
 
57
57
  ExpoGaodeMapModule.initSDK({
58
- androidKey: 'your-android-key',
59
- iosKey: 'your-ios-key',
60
58
  webKey: 'your-web-api-key', // 关键:供 Web API 包读取
61
59
  });
62
60
  ```
63
61
 
64
- ### 3. 无参构造并使用
62
+ ### 3. 创建实例并使用
63
+
64
+ 你可以通过以下两种方式创建 API 实例:
65
+
66
+ #### 方式 A:无参构造(推荐)
67
+ 先在基础模块初始化时配置 `webKey`,随后直接无参构造。这种方式便于统一管理 Key。
68
+
65
69
  ```ts
66
70
  import { GaodeWebAPI } from 'expo-gaode-map-web-api';
67
71
 
68
- // 无参:从基础模块运行时解析 webKey
72
+ // 无参:从基础模块运行时自动解析 webKey
69
73
  const api = new GaodeWebAPI();
74
+ ```
75
+
76
+ #### 方式 B:显式传入 Key
77
+ 如果你不想依赖基础模块的初始化,或者需要使用不同的 Key,可以在构造函数中显式传入。
78
+
79
+ ```ts
80
+ import { GaodeWebAPI } from 'expo-gaode-map-web-api';
81
+
82
+ // 显式传入:直接使用提供的 Web 服务 Key
83
+ const api = new GaodeWebAPI({ key: 'your-web-api-key' });
84
+ ```
70
85
 
86
+ ### 4. 调用服务接口
87
+ ```ts
71
88
  // 逆地理编码:坐标 → 地址
72
89
  const result = await api.geocode.regeocode('116.481028,39.989643');
73
90
  console.log(result.regeocode.formatted_address);
@@ -660,6 +677,15 @@ try {
660
677
  - 导航一体化模块(推荐渲染地图并下发 key):`expo-gaode-map-navigation`
661
678
  - 核心地图模块:`expo-gaode-map`
662
679
 
680
+ ## 📚 文档与资源
681
+ - [在线文档](https://tomwq.github.io/expo-gaode-map/api/web-api.html)
682
+ - [GitHub 仓库](https://github.com/TomWq/expo-gaode-map/packages/web-api)
683
+ - [示例项目(地图)](https://github.com/TomWq/expo-gaode-map-example)
684
+ - [高德地图开放平台](https://lbs.amap.com/)
685
+ - [Expo Modules API](https://docs.expo.dev/modules/overview/)
686
+
687
+
663
688
  ## License
664
689
 
665
- MIT
690
+ MIT License
691
+
package/build/index.js CHANGED
@@ -150,15 +150,16 @@ class GaodeWebAPI {
150
150
  * ```
151
151
  */
152
152
  constructor(config = {}) {
153
- const webKey = resolveWebKey();
153
+ // 优先使用传入的 key,其次尝试从 SDK 配置中解析
154
+ const webKey = config.key || resolveWebKey();
154
155
  if (!webKey) {
155
- throw new Error('[expo-gaode-map-web-api] 必须先通过 ExpoGaodeMapModule.initSDK({ webKey }) 初始化并提供 Web API Key。\n' +
156
- '请在应用启动时调用:\n' +
156
+ throw new Error('[expo-gaode-map-web-api] 缺少 Web API Key。您可以通过以下两种方式之一提供:\n' +
157
+ '1. 在构造函数中显式传入:new GaodeWebAPI({ key: "your-web-api-key" });\n' +
158
+ '2. 或者先通过 ExpoGaodeMapModule.initSDK({ webKey }) 初始化并提供 Web API Key:\n' +
157
159
  " import { ExpoGaodeMapModule } from 'expo-gaode-map';\n" +
158
- ' ExpoGaodeMapModule.initSDK({ webKey: \"your-web-api-key\", iosKey, androidKey });\n' +
159
- '随后再创建 GaodeWebAPI 实例使用 Web API 能力。');
160
+ ' ExpoGaodeMapModule.initSDK({ webKey: \"your-web-api-key\", iosKey, androidKey });');
160
161
  }
161
- // 强制使用核心模块中的 webKey,避免直接在此处绕过初始化约束
162
+ // 使用解析出的 key 或传入的 key
162
163
  const effectiveConfig = { ...config, key: webKey };
163
164
  this.client = new client_1.GaodeWebAPIClient(effectiveConfig);
164
165
  this.geocode = new GeocodeService_1.GeocodeService(this.client);
@@ -61,7 +61,9 @@ export declare class GeocodeService {
61
61
  * const result = await geocode.geocode('阜通东大街6号', '北京');
62
62
  * ```
63
63
  */
64
- geocode(address: string, city?: string): Promise<GeocodeResponse>;
64
+ geocode(address: string, city?: string, options?: {
65
+ signal?: AbortSignal;
66
+ }): Promise<GeocodeResponse>;
65
67
  /**
66
68
  * 批量逆地理编码
67
69
  *
@@ -71,6 +73,7 @@ export declare class GeocodeService {
71
73
  *
72
74
  * @example
73
75
  * ```typescript
76
+ * // 方式1:使用字符串数组
74
77
  * const result = await geocode.batchRegeocode([
75
78
  * '116.481028,39.989643',
76
79
  * '116.434446,39.90816'
@@ -93,5 +96,7 @@ export declare class GeocodeService {
93
96
  * ], '北京');
94
97
  * ```
95
98
  */
96
- batchGeocode(addresses: string[], city?: string): Promise<GeocodeResponse>;
99
+ batchGeocode(addresses: string[], city?: string, options?: {
100
+ signal?: AbortSignal;
101
+ }): Promise<GeocodeResponse>;
97
102
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.GeocodeService = void 0;
7
+ const validators_1 = require("../utils/validators");
7
8
  /**
8
9
  * 地理编码服务
9
10
  */
@@ -50,12 +51,15 @@ class GeocodeService {
50
51
  locationStr = `${location.longitude},${location.latitude}`;
51
52
  }
52
53
  // 构建请求参数
54
+ const { signal, ...rest } = options || {};
53
55
  const params = {
54
56
  location: locationStr,
55
- ...options,
57
+ ...rest,
56
58
  };
59
+ // 校验坐标
60
+ (0, validators_1.validateCoordinate)(locationStr);
57
61
  // 发起请求
58
- return this.client.request('/v3/geocode/regeo', params);
62
+ return this.client.request('/v3/geocode/regeo', { params, signal });
59
63
  }
60
64
  /**
61
65
  * 地理编码
@@ -76,12 +80,12 @@ class GeocodeService {
76
80
  * const result = await geocode.geocode('阜通东大街6号', '北京');
77
81
  * ```
78
82
  */
79
- async geocode(address, city) {
83
+ async geocode(address, city, options) {
80
84
  const params = {
81
85
  address,
82
86
  city,
83
87
  };
84
- return this.client.request('/v3/geocode/geo', params);
88
+ return this.client.request('/v3/geocode/geo', { params, signal: options?.signal });
85
89
  }
86
90
  /**
87
91
  * 批量逆地理编码
@@ -92,6 +96,7 @@ class GeocodeService {
92
96
  *
93
97
  * @example
94
98
  * ```typescript
99
+ * // 方式1:使用字符串数组
95
100
  * const result = await geocode.batchRegeocode([
96
101
  * '116.481028,39.989643',
97
102
  * '116.434446,39.90816'
@@ -99,12 +104,20 @@ class GeocodeService {
99
104
  * ```
100
105
  */
101
106
  async batchRegeocode(locations, options) {
107
+ // 检查是否有任何输入包含分隔符
108
+ if (locations.some(loc => loc.includes('|'))) {
109
+ throw new Error('Invalid location: Individual locations cannot contain the "|" separator.');
110
+ }
111
+ const locationStr = locations.join('|');
112
+ // 校验坐标
113
+ (0, validators_1.validateCoordinates)(locationStr);
114
+ const { signal, ...rest } = options || {};
102
115
  const params = {
103
- location: locations.join('|'),
116
+ location: locationStr,
104
117
  batch: true,
105
- ...options,
118
+ ...rest,
106
119
  };
107
- return this.client.request('/v3/geocode/regeo', params);
120
+ return this.client.request('/v3/geocode/regeo', { params, signal });
108
121
  }
109
122
  /**
110
123
  * 批量地理编码
@@ -121,13 +134,17 @@ class GeocodeService {
121
134
  * ], '北京');
122
135
  * ```
123
136
  */
124
- async batchGeocode(addresses, city) {
137
+ async batchGeocode(addresses, city, options) {
138
+ // 检查是否有任何输入包含分隔符
139
+ if (addresses.some(addr => addr.includes('|'))) {
140
+ throw new Error('Invalid address: Individual addresses cannot contain the "|" separator.');
141
+ }
125
142
  const params = {
126
143
  address: addresses.join('|'),
127
144
  batch: true,
128
145
  city,
129
146
  };
130
- return this.client.request('/v3/geocode/geo', params);
147
+ return this.client.request('/v3/geocode/geo', { params, signal: options?.signal });
131
148
  }
132
149
  }
133
150
  exports.GeocodeService = GeocodeService;
@@ -5,6 +5,7 @@
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.InputTipsService = void 0;
8
+ const validators_1 = require("../utils/validators");
8
9
  /**
9
10
  * 输入提示服务
10
11
  * 提供根据用户输入的关键词查询返回建议列表
@@ -53,11 +54,15 @@ class InputTipsService {
53
54
  * ```
54
55
  */
55
56
  async getTips(keywords, options) {
57
+ const { signal, ...rest } = options || {};
58
+ if (rest.location) {
59
+ (0, validators_1.validateCoordinate)(rest.location);
60
+ }
56
61
  const params = {
57
62
  keywords,
58
- ...options,
63
+ ...rest,
59
64
  };
60
- return this.client.request('/v3/assistant/inputtips', params);
65
+ return this.client.request('/v3/assistant/inputtips', { params, signal });
61
66
  }
62
67
  /**
63
68
  * 获取 POI 输入提示
@@ -113,7 +113,9 @@ export declare class POIService {
113
113
  * const result = await poi.getDetail('B000A8VE1H|B0FFKEPXS2', 'business,photos');
114
114
  * ```
115
115
  */
116
- getDetail(id: string, show_fields?: string, version?: 'v3' | 'v5'): Promise<POISearchResponse>;
116
+ getDetail(id: string, show_fields?: string, version?: 'v3' | 'v5', options?: {
117
+ signal?: AbortSignal;
118
+ }): Promise<POISearchResponse>;
117
119
  /**
118
120
  * 批量查询POI详情
119
121
  *
@@ -129,5 +131,7 @@ export declare class POIService {
129
131
  * ], 'business,photos');
130
132
  * ```
131
133
  */
132
- batchGetDetail(ids: string[], show_fields?: string, version?: 'v3' | 'v5'): Promise<POISearchResponse>;
134
+ batchGetDetail(ids: string[], show_fields?: string, version?: 'v3' | 'v5', options?: {
135
+ signal?: AbortSignal;
136
+ }): Promise<POISearchResponse>;
133
137
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.POIService = void 0;
7
+ const validators_1 = require("../utils/validators");
7
8
  /**
8
9
  * POI 搜索服务
9
10
  */
@@ -41,13 +42,13 @@ class POIService {
41
42
  * ```
42
43
  */
43
44
  async search(keywords, options) {
44
- const { version = 'v5', ...rest } = options || {};
45
+ const { version = 'v5', signal, ...rest } = options || {};
45
46
  const params = {
46
47
  keywords,
47
48
  ...rest,
48
49
  };
49
50
  const path = `/${version}/place/text`;
50
- return this.client.request(path, params);
51
+ return this.client.request(path, { params, signal });
51
52
  }
52
53
  /**
53
54
  * 周边搜索
@@ -90,13 +91,15 @@ class POIService {
90
91
  else {
91
92
  locationStr = `${location.longitude},${location.latitude}`;
92
93
  }
93
- const { version = 'v5', ...rest } = options || {};
94
+ // 校验坐标
95
+ (0, validators_1.validateCoordinate)(locationStr);
96
+ const { version = 'v5', signal, ...rest } = options || {};
94
97
  const params = {
95
98
  location: locationStr,
96
99
  ...rest,
97
100
  };
98
101
  const path = `/${version}/place/around`;
99
- return this.client.request(path, params);
102
+ return this.client.request(path, { params, signal });
100
103
  }
101
104
  /**
102
105
  * 多边形搜索
@@ -118,13 +121,14 @@ class POIService {
118
121
  * ```
119
122
  */
120
123
  async searchPolygon(polygon, options) {
121
- const { version = 'v5', ...rest } = options || {};
124
+ (0, validators_1.validateCoordinates)(polygon, '|');
125
+ const { version = 'v5', signal, ...rest } = options || {};
122
126
  const params = {
123
127
  polygon,
124
128
  ...rest,
125
129
  };
126
130
  const path = `/${version}/place/polygon`;
127
- return this.client.request(path, params);
131
+ return this.client.request(path, { params, signal });
128
132
  }
129
133
  /**
130
134
  * POI详情查询
@@ -144,13 +148,13 @@ class POIService {
144
148
  * const result = await poi.getDetail('B000A8VE1H|B0FFKEPXS2', 'business,photos');
145
149
  * ```
146
150
  */
147
- async getDetail(id, show_fields, version = 'v5') {
151
+ async getDetail(id, show_fields, version = 'v5', options) {
148
152
  const params = {
149
153
  id,
150
154
  show_fields,
151
155
  };
152
156
  const path = `/${version}/place/detail`;
153
- return this.client.request(path, params);
157
+ return this.client.request(path, { params, signal: options?.signal });
154
158
  }
155
159
  /**
156
160
  * 批量查询POI详情
@@ -167,7 +171,7 @@ class POIService {
167
171
  * ], 'business,photos');
168
172
  * ```
169
173
  */
170
- async batchGetDetail(ids, show_fields, version = 'v5') {
174
+ async batchGetDetail(ids, show_fields, version = 'v5', options) {
171
175
  if (ids.length > 10) {
172
176
  throw new Error('批量查询最多支持10个POI ID');
173
177
  }
@@ -176,7 +180,7 @@ class POIService {
176
180
  show_fields,
177
181
  };
178
182
  const path = `/${version}/place/detail`;
179
- return this.client.request(path, params);
183
+ return this.client.request(path, { params, signal: options?.signal });
180
184
  }
181
185
  }
182
186
  exports.POIService = POIService;
@@ -89,7 +89,7 @@ export declare class RouteService {
89
89
  *
90
90
  * @example
91
91
  * ```typescript
92
- * const result = await api.route.electr icBike(
92
+ * const result = await api.route.electricBike(
93
93
  * '116.481028,39.989643',
94
94
  * '116.434446,39.90816'
95
95
  * );
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RouteService = void 0;
4
+ const validators_1 = require("../utils/validators");
4
5
  /**
5
6
  * 路径规划服务
6
7
  * 提供驾车、步行、骑行、电动车、公交等多种出行方式的路径规划
@@ -13,10 +14,15 @@ class RouteService {
13
14
  * 坐标转换辅助方法
14
15
  */
15
16
  formatCoordinate(coord) {
17
+ let str;
16
18
  if (typeof coord === 'string') {
17
- return coord;
19
+ str = coord;
18
20
  }
19
- return `${coord.longitude},${coord.latitude}`;
21
+ else {
22
+ str = `${coord.longitude},${coord.latitude}`;
23
+ }
24
+ (0, validators_1.validateCoordinate)(str);
25
+ return str;
20
26
  }
21
27
  /**
22
28
  * 驾车路径规划
@@ -48,7 +54,7 @@ class RouteService {
48
54
  * ```
49
55
  */
50
56
  async driving(origin, destination, options) {
51
- const { version = 'v5', ...rest } = options || {};
57
+ const { version = 'v5', signal, ...rest } = options || {};
52
58
  const params = {
53
59
  origin: this.formatCoordinate(origin),
54
60
  destination: this.formatCoordinate(destination),
@@ -64,7 +70,7 @@ class RouteService {
64
70
  }
65
71
  }
66
72
  const path = `/${version}/direction/driving`;
67
- return this.client.request(path, params);
73
+ return this.client.request(path, { params, signal });
68
74
  }
69
75
  /**
70
76
  * 步行路径规划
@@ -85,14 +91,14 @@ class RouteService {
85
91
  * ```
86
92
  */
87
93
  async walking(origin, destination, options) {
88
- const { version = 'v5', ...rest } = options || {};
94
+ const { version = 'v5', signal, ...rest } = options || {};
89
95
  const params = {
90
96
  origin: this.formatCoordinate(origin),
91
97
  destination: this.formatCoordinate(destination),
92
98
  ...rest,
93
99
  };
94
100
  const path = `/${version}/direction/walking`;
95
- return this.client.request(path, params);
101
+ return this.client.request(path, { params, signal });
96
102
  }
97
103
  /**
98
104
  * 骑行路径规划
@@ -114,14 +120,14 @@ class RouteService {
114
120
  * ```
115
121
  */
116
122
  async bicycling(origin, destination, options) {
117
- const { version = 'v5', ...rest } = options || {};
123
+ const { version = 'v5', signal, ...rest } = options || {};
118
124
  const params = {
119
125
  origin: this.formatCoordinate(origin),
120
126
  destination: this.formatCoordinate(destination),
121
127
  ...rest,
122
128
  };
123
129
  const path = `/${version}/direction/bicycling`;
124
- return this.client.request(path, params);
130
+ return this.client.request(path, { params, signal });
125
131
  }
126
132
  /**
127
133
  * 电动车路径规划
@@ -132,21 +138,21 @@ class RouteService {
132
138
  *
133
139
  * @example
134
140
  * ```typescript
135
- * const result = await api.route.electr icBike(
141
+ * const result = await api.route.electricBike(
136
142
  * '116.481028,39.989643',
137
143
  * '116.434446,39.90816'
138
144
  * );
139
145
  * ```
140
146
  */
141
147
  async electricBike(origin, destination, options) {
142
- const { version = 'v5', ...rest } = options || {};
148
+ const { version = 'v5', signal, ...rest } = options || {};
143
149
  const params = {
144
150
  origin: this.formatCoordinate(origin),
145
151
  destination: this.formatCoordinate(destination),
146
152
  ...rest,
147
153
  };
148
154
  const path = `/${version}/direction/electrobike`;
149
- return this.client.request(path, params);
155
+ return this.client.request(path, { params, signal });
150
156
  }
151
157
  /**
152
158
  * 公交路径规划(新版 V5 API)
@@ -198,7 +204,7 @@ class RouteService {
198
204
  * ```
199
205
  */
200
206
  async transit(origin, destination, city1, city2, options) {
201
- const { version = 'v5', ...rest } = options || {};
207
+ const { version = 'v5', signal, ...rest } = options || {};
202
208
  const params = {
203
209
  origin: this.formatCoordinate(origin),
204
210
  destination: this.formatCoordinate(destination),
@@ -207,7 +213,7 @@ class RouteService {
207
213
  ...rest,
208
214
  };
209
215
  const path = `/${version}/direction/transit/integrated`;
210
- return this.client.request(path, params);
216
+ return this.client.request(path, { params, signal });
211
217
  }
212
218
  }
213
219
  exports.RouteService = RouteService;
@@ -63,6 +63,10 @@ export interface RegeocodeParams {
63
63
  * 此参数只在output参数设置为JSON时有效
64
64
  */
65
65
  callback?: string;
66
+ /**
67
+ * AbortSignal 用于取消请求
68
+ */
69
+ signal?: AbortSignal;
66
70
  }
67
71
  /**
68
72
  * 地址组成元素
@@ -291,6 +295,10 @@ export interface GeocodeParams {
291
295
  * callback值是用户定义的函数名称,此参数只在output参数设置为JSON时有效
292
296
  */
293
297
  callback?: string;
298
+ /**
299
+ * AbortSignal 用于取消请求
300
+ */
301
+ signal?: AbortSignal;
294
302
  }
295
303
  /**
296
304
  * 地理编码结果
@@ -24,6 +24,10 @@ export interface InputTipsParams {
24
24
  * - busline: 返回公交线路数据类型
25
25
  */
26
26
  datatype?: 'all' | 'poi' | 'bus' | 'busline' | string;
27
+ /**
28
+ * AbortSignal 用于取消请求
29
+ */
30
+ signal?: AbortSignal;
27
31
  }
28
32
  /**
29
33
  * 输入提示项
@@ -51,6 +51,10 @@ export interface POISearchParams {
51
51
  * @default 'v5'
52
52
  */
53
53
  version?: 'v3' | 'v5';
54
+ /**
55
+ * AbortSignal 用于取消请求
56
+ */
57
+ signal?: AbortSignal;
54
58
  }
55
59
  /**
56
60
  * 周边搜索参数
@@ -121,6 +125,10 @@ export interface POIAroundParams {
121
125
  * @default 'v5'
122
126
  */
123
127
  version?: 'v3' | 'v5';
128
+ /**
129
+ * AbortSignal 用于取消请求
130
+ */
131
+ signal?: AbortSignal;
124
132
  }
125
133
  /**
126
134
  * 多边形搜索参数
@@ -168,6 +176,10 @@ export interface POIPolygonParams {
168
176
  * @default 'v5'
169
177
  */
170
178
  version?: 'v3' | 'v5';
179
+ /**
180
+ * AbortSignal 用于取消请求
181
+ */
182
+ signal?: AbortSignal;
171
183
  }
172
184
  /**
173
185
  * POI 详情
@@ -336,4 +348,8 @@ export interface POIDetailParams {
336
348
  * @default 'v5'
337
349
  */
338
350
  version?: 'v3' | 'v5';
351
+ /**
352
+ * AbortSignal 用于取消请求
353
+ */
354
+ signal?: AbortSignal;
339
355
  }
@@ -34,6 +34,10 @@ export interface BaseRouteParams {
34
34
  * @default 'v5'
35
35
  */
36
36
  version?: 'v3' | 'v5';
37
+ /**
38
+ * AbortSignal 用于取消请求
39
+ */
40
+ signal?: AbortSignal;
37
41
  }
38
42
  /**
39
43
  * 驾车路径规划策略(新版 V5 API)
@@ -2,6 +2,10 @@
2
2
  * 高德地图 Web API HTTP 客户端
3
3
  */
4
4
  import { type ErrorInfo } from './errorCodes';
5
+ /**
6
+ * 从核心包解析 getWebKey(运行时解析,避免类型导出时序问题)
7
+ */
8
+ export declare function resolveWebKey(): string | undefined;
5
9
  /**
6
10
  * 高德地图 API 错误类
7
11
  */
@@ -54,6 +58,25 @@ export interface ClientConfig {
54
58
  baseURL?: string;
55
59
  /** 请求超时时间(毫秒),默认:10000 */
56
60
  timeout?: number;
61
+ /** 最大重试次数,默认:3 */
62
+ maxRetries?: number;
63
+ /** 重试延迟(毫秒),默认:1000 */
64
+ retryDelay?: number;
65
+ /** 是否启用缓存,默认:false */
66
+ enableCache?: boolean;
67
+ /** 缓存容量,默认:100 */
68
+ cacheCapacity?: number;
69
+ }
70
+ /**
71
+ * 请求选项
72
+ */
73
+ export interface RequestOptions {
74
+ /** 请求参数 */
75
+ params?: Record<string, any>;
76
+ /** AbortSignal 用于取消请求 */
77
+ signal?: AbortSignal;
78
+ /** 是否使用缓存(仅当全局启用缓存时有效),默认:true */
79
+ useCache?: boolean;
57
80
  }
58
81
  /**
59
82
  * 高德地图 Web API HTTP 客户端
@@ -62,11 +85,22 @@ export declare class GaodeWebAPIClient {
62
85
  private key;
63
86
  private baseURL;
64
87
  private timeout;
88
+ private maxRetries;
89
+ private retryDelay;
90
+ private cache?;
65
91
  constructor(config: ClientConfig);
66
92
  /**
67
93
  * 发起 HTTP 请求
68
94
  */
69
- request<T>(path: string, params?: Record<string, any>): Promise<T>;
95
+ request<T>(path: string, options?: RequestOptions): Promise<T>;
96
+ /**
97
+ * 判断是否为可重试的错误码
98
+ */
99
+ private shouldRetry;
100
+ /**
101
+ * 判断是否为可重试的异常
102
+ */
103
+ private isRetryableError;
70
104
  /**
71
105
  * 更新 API Key
72
106
  */
@@ -4,7 +4,12 @@
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.GaodeWebAPIClient = exports.GaodeAPIError = void 0;
7
+ exports.resolveWebKey = resolveWebKey;
7
8
  const errorCodes_1 = require("./errorCodes");
9
+ const lru_1 = require("./lru");
10
+ /**
11
+ * 从核心包解析 getWebKey(运行时解析,避免类型导出时序问题)
12
+ */
8
13
  function resolveWebKey() {
9
14
  // 1) 尝试从核心地图包读取
10
15
  try {
@@ -80,11 +85,17 @@ class GaodeWebAPIClient {
80
85
  this.key = config.key || resolveWebKey() || '';
81
86
  this.baseURL = config.baseURL || 'https://restapi.amap.com';
82
87
  this.timeout = config.timeout || 10000;
88
+ this.maxRetries = config.maxRetries ?? 3;
89
+ this.retryDelay = config.retryDelay ?? 1000;
90
+ if (config.enableCache) {
91
+ this.cache = new lru_1.LRUCache(config.cacheCapacity ?? 100);
92
+ }
83
93
  }
84
94
  /**
85
95
  * 发起 HTTP 请求
86
96
  */
87
- async request(path, params = {}) {
97
+ async request(path, options = {}) {
98
+ const { params = {}, signal, useCache = true } = options;
88
99
  // 构建 URL
89
100
  const url = new URL(path, this.baseURL);
90
101
  // 添加 key 参数
@@ -95,38 +106,119 @@ class GaodeWebAPIClient {
95
106
  url.searchParams.append(key, String(value));
96
107
  }
97
108
  });
98
- // 创建 AbortController 用于超时控制
99
- const controller = new AbortController();
100
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
101
- try {
102
- // 发起请求
103
- const response = await fetch(url.toString(), {
104
- method: 'GET',
105
- signal: controller.signal,
106
- });
107
- clearTimeout(timeoutId);
108
- // 检查 HTTP 状态
109
- if (!response.ok) {
110
- throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
109
+ // 检查缓存
110
+ const cacheKey = url.toString();
111
+ if (this.cache && useCache) {
112
+ const cached = this.cache.get(cacheKey);
113
+ if (cached) {
114
+ return cached;
111
115
  }
112
- // 解析 JSON
113
- const data = await response.json();
114
- // 检查 API 状态
115
- if (data.status !== '1' && !(0, errorCodes_1.isSuccess)(data.infocode)) {
116
- throw new GaodeAPIError(data.status, data.info || 'Unknown error', data.infocode || '0');
117
- }
118
- return data;
119
116
  }
120
- catch (error) {
121
- clearTimeout(timeoutId);
122
- if (error instanceof Error) {
123
- if (error.name === 'AbortError') {
124
- throw new Error(`Request timeout after ${this.timeout}ms`);
117
+ let lastError = null;
118
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
119
+ // 如果外部 signal 已经中止,则直接抛出
120
+ if (signal?.aborted) {
121
+ throw new Error('Request aborted');
122
+ }
123
+ // 创建 AbortController 用于超时控制
124
+ const controller = new AbortController();
125
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
126
+ // 处理 signal 合并:如果外部 signal 触发,也要 abort 内部 controller
127
+ const onAbort = () => controller.abort();
128
+ if (signal) {
129
+ signal.addEventListener('abort', onAbort);
130
+ }
131
+ try {
132
+ // 发起请求
133
+ const response = await fetch(url.toString(), {
134
+ method: 'GET',
135
+ signal: controller.signal,
136
+ });
137
+ clearTimeout(timeoutId);
138
+ if (signal) {
139
+ signal.removeEventListener('abort', onAbort);
140
+ }
141
+ // 检查 HTTP 状态
142
+ if (!response.ok) {
143
+ throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
144
+ }
145
+ // 解析 JSON
146
+ const data = await response.json();
147
+ // 检查 API 状态
148
+ if (data.status !== '1' && !(0, errorCodes_1.isSuccess)(data.infocode)) {
149
+ // 检查是否为可重试的错误码 (QPS超限等)
150
+ if (this.shouldRetry(data.infocode) && attempt < this.maxRetries) {
151
+ const error = new GaodeAPIError(data.status, data.info || 'Unknown error', data.infocode || '0');
152
+ // 抛出错误以触发重试逻辑
153
+ throw error;
154
+ }
155
+ throw new GaodeAPIError(data.status, data.info || 'Unknown error', data.infocode || '0');
156
+ }
157
+ // 写入缓存
158
+ if (this.cache && useCache) {
159
+ this.cache.set(cacheKey, data);
160
+ }
161
+ return data;
162
+ }
163
+ catch (error) {
164
+ clearTimeout(timeoutId);
165
+ if (signal) {
166
+ signal.removeEventListener('abort', onAbort);
167
+ }
168
+ lastError = error instanceof Error ? error : new Error(String(error));
169
+ // 如果是 AbortError (无论是超时还是手动取消)
170
+ if (lastError.name === 'AbortError') {
171
+ // 如果是手动取消,不重试
172
+ if (signal?.aborted) {
173
+ throw new Error('Request aborted');
174
+ }
175
+ // 如果是超时,且还有重试机会,则继续
176
+ if (attempt < this.maxRetries) {
177
+ // 继续下一次循环
178
+ }
179
+ else {
180
+ throw new Error(`Request timeout after ${this.timeout}ms`);
181
+ }
182
+ }
183
+ // 如果不是最后一次尝试,且是可重试的错误(网络错误或特定API错误)
184
+ if (attempt < this.maxRetries && this.isRetryableError(lastError)) {
185
+ // 等待一段时间后重试(指数退避)
186
+ const delay = this.retryDelay * Math.pow(2, attempt);
187
+ await new Promise(resolve => setTimeout(resolve, delay));
188
+ continue;
125
189
  }
126
- throw error;
190
+ throw lastError;
127
191
  }
128
- throw new Error('Unknown error occurred');
129
192
  }
193
+ throw lastError || new Error('Unknown error occurred');
194
+ }
195
+ /**
196
+ * 判断是否为可重试的错误码
197
+ */
198
+ shouldRetry(infocode) {
199
+ const retryableCodes = [
200
+ '10004', // ACCESS_TOO_FREQUENT
201
+ '10014', // QPS_HAS_EXCEEDED_THE_LIMIT
202
+ '10015', // GATEWAY_TIMEOUT
203
+ '10016', // SERVER_IS_BUSY
204
+ '10017', // RESOURCE_UNAVAILABLE
205
+ '10019', // CQPS_HAS_EXCEEDED_THE_LIMIT
206
+ '10020', // CKQPS_HAS_EXCEEDED_THE_LIMIT
207
+ '10021', // CUQPS_HAS_EXCEEDED_THE_LIMIT
208
+ ];
209
+ return retryableCodes.includes(infocode);
210
+ }
211
+ /**
212
+ * 判断是否为可重试的异常
213
+ */
214
+ isRetryableError(error) {
215
+ // GaodeAPIError 已经在 shouldRetry 中判断过了,这里只处理 GaodeAPIError 且 shouldRetry 为 true 的情况
216
+ if (error instanceof GaodeAPIError) {
217
+ return this.shouldRetry(error.code);
218
+ }
219
+ // 网络错误通常没有 status 属性或者 status 为 undefined (fetch 失败)
220
+ // 这里简单认为非 API 业务逻辑错误都可以尝试重试(除了 AbortError 已经在上面处理了)
221
+ return true;
130
222
  }
131
223
  /**
132
224
  * 更新 API Key
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 简单的 LRU (Least Recently Used) 缓存实现
3
+ */
4
+ export declare class LRUCache<K, V> {
5
+ private capacity;
6
+ private cache;
7
+ constructor(capacity: number);
8
+ get(key: K): V | undefined;
9
+ set(key: K, value: V): void;
10
+ clear(): void;
11
+ size(): number;
12
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LRUCache = void 0;
4
+ /**
5
+ * 简单的 LRU (Least Recently Used) 缓存实现
6
+ */
7
+ class LRUCache {
8
+ constructor(capacity) {
9
+ this.capacity = capacity;
10
+ this.cache = new Map();
11
+ }
12
+ get(key) {
13
+ if (!this.cache.has(key)) {
14
+ return undefined;
15
+ }
16
+ // 刷新访问顺序:先删除再重新添加
17
+ const value = this.cache.get(key);
18
+ this.cache.delete(key);
19
+ this.cache.set(key, value);
20
+ return value;
21
+ }
22
+ set(key, value) {
23
+ if (this.cache.has(key)) {
24
+ // 如果已存在,更新值并刷新位置
25
+ this.cache.delete(key);
26
+ }
27
+ else if (this.cache.size >= this.capacity) {
28
+ // 如果已满,删除最久未使用的项(Map 的第一个项)
29
+ const firstKey = this.cache.keys().next().value;
30
+ if (firstKey !== undefined) {
31
+ this.cache.delete(firstKey);
32
+ }
33
+ }
34
+ this.cache.set(key, value);
35
+ }
36
+ clear() {
37
+ this.cache.clear();
38
+ }
39
+ size() {
40
+ return this.cache.size;
41
+ }
42
+ }
43
+ exports.LRUCache = LRUCache;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 校验经纬度坐标格式
3
+ * 格式要求:经度,纬度 (例如:116.481028,39.989643)
4
+ */
5
+ export declare function validateCoordinate(coordinate: string): void;
6
+ /**
7
+ * 校验多个坐标格式(用于批量请求或多边形)
8
+ * @param coordinates 坐标字符串
9
+ * @param separator 分隔符,默认 "|"
10
+ */
11
+ export declare function validateCoordinates(coordinates: string, separator?: string): void;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateCoordinate = validateCoordinate;
4
+ exports.validateCoordinates = validateCoordinates;
5
+ /**
6
+ * 校验经纬度坐标格式
7
+ * 格式要求:经度,纬度 (例如:116.481028,39.989643)
8
+ */
9
+ function validateCoordinate(coordinate) {
10
+ const pattern = /^-?((1[0-7]\d|\d{1,2})(\.\d+)?|180(\.0+)?),-?([0-8]\d(\.\d+)?|90(\.0+)?|\d(\.\d+)?)$/;
11
+ if (!pattern.test(coordinate)) {
12
+ throw new Error(`Invalid coordinate format: "${coordinate}". Expected format: "longitude,latitude" (e.g., "116.481028,39.989643")`);
13
+ }
14
+ }
15
+ /**
16
+ * 校验多个坐标格式(用于批量请求或多边形)
17
+ * @param coordinates 坐标字符串
18
+ * @param separator 分隔符,默认 "|"
19
+ */
20
+ function validateCoordinates(coordinates, separator = '|') {
21
+ const coords = coordinates.split(separator);
22
+ coords.forEach(coord => validateCoordinate(coord));
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-gaode-map-web-api",
3
- "version": "1.1.4-next.1",
3
+ "version": "1.1.4-next.3",
4
4
  "description": "高德地图 Web API 服务 - 搜索、路径规划、地理编码(纯 JavaScript 实现),配合 expo-gaode-map 使用",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -9,13 +9,12 @@
9
9
  "README.md"
10
10
  ],
11
11
  "scripts": {
12
- "build": "tsc",
13
- "dev": "tsc --watch",
12
+ "build": "tsc -p tsconfig.build.json",
13
+ "dev": "tsc -p tsconfig.build.json --watch",
14
14
  "clean": "rm -rf build",
15
15
  "lint": "node -e \"const { spawnSync } = require('child_process'); const r = spawnSync('tsc', ['--noEmit', '--pretty', 'false'], { stdio: 'inherit' }); process.exit(r.status ?? 1);\" --",
16
16
  "test": "bun test",
17
- "prepare": "npm run build",
18
- "postinstall": "node -e \"try{require.resolve('expo-gaode-map');process.exit(0)}catch(e1){try{require.resolve('expo-gaode-map-navigation');process.exit(0)}catch(e2){console.error('[expo-gaode-map-web-api] 需要安装基础地图组件:expo-gaode-map 或 expo-gaode-map-navigation 中的任意一个。\\bun add expo-gaode-map 或 bun add expo-gaode-map-navigation');process.exit(1)}}\""
17
+ "prepare": "npm run build"
19
18
  },
20
19
  "keywords": [
21
20
  "react-native",
@@ -38,6 +37,7 @@
38
37
  "dependencies": {},
39
38
  "devDependencies": {
40
39
  "@types/react": "~19.1.0",
40
+ "bun-types": "^1.3.6",
41
41
  "typescript": "^5.9.3"
42
42
  },
43
43
  "peerDependencies": {