├── .gitignore
├── .prettierignore
├── .vscode
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.en-US.md
├── README.md
├── __tests__
├── plugins
│ └── merge.spec.ts
├── use-plugin.spec.ts
└── utils
│ ├── calc-hash.spec.ts
│ ├── create-abort-chain.spec.ts
│ ├── create-cache.spec.ts
│ ├── create-filter.spec.ts
│ ├── delay.spec.ts
│ └── url.spec.ts
├── build.config.ts
├── dist
├── core.cjs
├── core.d.cts
├── core.d.mts
├── core.d.ts
├── core.mjs
├── index.cjs
├── index.d.cts
├── index.d.mts
├── index.d.ts
└── index.mjs
├── package.json
├── prettier.config.json
├── src
├── core.ts
├── index.ts
├── intf
│ └── index.ts
├── plugins
│ ├── auth.ts
│ ├── cache.ts
│ ├── cancel.ts
│ ├── debounce.ts
│ ├── envs.ts
│ ├── loading.ts
│ ├── merge.ts
│ ├── mock.ts
│ ├── mp.ts
│ ├── normalize.ts
│ ├── only-send.ts
│ ├── path-params.ts
│ ├── retry.ts
│ ├── sentry-capture.ts
│ ├── sign.ts
│ ├── throttle.ts
│ └── transform.ts
├── use-plugin.ts
└── utils
│ ├── calc-hash.ts
│ ├── create-abort-chain.ts
│ ├── create-cache.ts
│ ├── create-filter.ts
│ ├── delay.ts
│ └── url.ts
├── tsconfig.json
├── tsconfig.test.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | *.local
12 |
13 | dist
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | node_modules/
27 | .coverage/
28 | .temp/
29 | .cache/
30 | __notes__/
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .coverage/
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "prettier.configPath": "./prettier.config.json"
3 | }
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | > 看情况写写, 一般除了大版本更改, 我是不会改这个的 ~,~
4 |
5 | ## v0.5.1
6 |
7 | - (TODO) 计划下一版本, 完善单测用例
8 | - 增加按需引用
9 | - 增加 `auth` 请求前鉴权插件
10 | - 完善ts类型推断
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Halo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.en-US.md:
--------------------------------------------------------------------------------
1 |
中文 | English
2 |
3 | > Extend more plugin capabilities (such as debounce and throttle, etc.) for axios with minimal impact.
4 |
5 | > Tips: The English version of the document may not be updated in a timely manner, please refer to [README.md](./README.md) for accuracy
6 |
7 | ## Features
8 |
9 | - [Lightweight] The core package size is 1.37kb/gziped, complete package size is 5.88kb/gziped, supports TreeShaking and single plugin reference.
10 | - [Multiple instance support] Plugin cache variables are associated with axios instances, and multiple axios instances do not interfere with each other.
11 | - [Low intrusiveness] Plugins are extended by wrapping, without affecting the existing configuration of the instance, and without destroying the api of axios.
12 | - [Low integration cost] Compared with other plugins, there is no need to make a lot of changes to integrate the plugin, and there is no learning cost.
13 | - [Rich plugin selection] Compared with other plugins based on `axios.interceptors`, this library provides more diversified plugin options.
14 | - [Extendable] Provides the `IPlugin` interface, which only needs to follow the interface specification to extend more plugin capabilities on its own.
15 |
16 | ## Usage
17 |
18 | - install
19 |
20 | ```bash
21 | yarn add axios axios-adapters
22 | # or
23 | npm install axios axios-adapters
24 | ```
25 |
26 | - use plugin
27 |
28 | ```typescript
29 | import axios from 'axios'
30 | import { useAxiosPlugin, mock, loading } from 'axios-plugins'
31 |
32 | // 1. create axios instance
33 | export const request = axios.create({
34 | /* ... */
35 | })
36 |
37 | // 2. append plugin support
38 |
39 | useAxiosPlugin(axios) // default axios
40 | // or
41 | useAxiosPlugin(request) // new instance
42 |
43 | // 3. use plugin
44 |
45 | useAxiosPlugin(axios).plugin(mock())
46 |
47 | // 4. Example: use plugin extension parameters during the request process
48 |
49 | request.post('/api', {}, { mock: true })
50 |
51 | // 5. If you need to use 'request()' to request, then you need to use the 'wrap()' method to overwrite the original instance
52 | const request2 = useAxiosPlugin(request).wrap()
53 | ```
54 |
55 | - on need import
56 |
57 | ```typescript
58 | import axios from 'axios'
59 | // + Modify the dependency import to the following way
60 | import { useAxiosPlugin } from 'axios-plugins/core'
61 | import { loading } from 'axios-plugins/plugins/loading'
62 |
63 | // 1. create axios instance
64 | export const request = axios.create({
65 | /* ... */
66 | })
67 |
68 | // 2. use plugin
69 | useAxiosPlugin(request).plugin(loading())
70 | ```
71 |
72 | ## Plugins
73 |
74 | | Plugin | Description |
75 | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
76 | | [debounce](#debounce) | When duplicate requests are made within a certain time, the later request will wait for the last request to complete before executing |
77 | | [throttle](#throttle) | When duplicate requests are made within a certain time, the later request will be discarded |
78 | | [merge](#merge) | When duplicate requests are made within a certain time, the requests will only be made once, and each requester will receive the same response |
79 | | retry | When a request fails (errors), retry n times. If all retries fail, an exception will be thrown |
80 | | cancel | Provides `cancelAll()` method to cancel all ongoing requests |
81 | | cache | Stores the response content of the request and returns it for the next request (within cache expiration time) |
82 | | envs | Normalizes axios environment configuration tool |
83 | | loading | Provides a unified control capability for global loading to reduce the workload of independent loading control for each loading method |
84 | | mock | Provides global or single interface request mock capability |
85 | | normalize | Filters out undefined, null, and other parameters generated during the request process |
86 | | pathParams | Expands support for Restful API specification of route parameters |
87 | | sign | Provides request anti-tampering capability. This feature requires collaboration with backend logic implementation |
88 | | sentryCapture | Extension `Sentry.captureException` implementation |
89 | | onlySend | Provides a wrapper method for `navigator.sendBeacon` to submit embedded data when the page is exited. This requires backend support |
90 | | mp | Expands support for network requests from small programs (WeChat, Toutiao, QQ, etc.) and cross-platform frameworks (uni-app, taro) |
91 |
92 | ## Example
93 |
94 | #### debounce
95 |
96 | ```typescript
97 | import { useAxiosPlugin } from 'axios-plugin/core'
98 | import { debounce } from 'axios-plugins/plugins/debounce'
99 |
100 | const request = axios.create({})
101 |
102 | // add plugin
103 | useAxiosPlugin(request).plugin(debounce())
104 |
105 | // set delay judgment time
106 | debounce({ delay: 200 })
107 |
108 | // set filter pattern
109 | debounce({ includes: '/api/', excludes: [] })
110 |
111 | // set duplicate request determination method
112 | debounce({ calcRequstHash: (config) => config.url })
113 |
114 | // on execute request, set higher priority judgment criteria
115 | request.post('/api/xxx', {}, { debounce: true })
116 |
117 | // set different delay judgment times
118 | request.post('/api/xxx', {}, { debounce: { delay: 1000 } })
119 | ```
120 |
121 | #### throttle
122 |
123 | ```typescript
124 | import { useAxiosPlugin } from 'axios-plugin/core'
125 | import { throttle, GiveUpRule } from 'axios-plugins/plugins/throttle'
126 |
127 | const request = axios.create({})
128 | /** 配置 */
129 | // 基础使用
130 | useAxiosPlugin(request).plugin(throttle())
131 |
132 | // set delay judgment time
133 | throttle({ delay: 200 })
134 |
135 | // 设置哪些请求将触发节流策略 (按需设置 `includes`, `excludes`)
136 | throttle({ includes: '/api/', excludes: [] })
137 |
138 | // 设置节流策略处理规则
139 | throttle({ giveUp: GiveUpRule.throw }) // 抛出异常 (默认)
140 | throttle({ giveUp: GiveUpRule.cancel }) // 中断请求, 并返回空值结果
141 | throttle({ giveUp: GiveUpRule.slient }) // 静默, 既不返回成功、也不抛出异常
142 |
143 | // 自定义相同请求判定规则 (忽略参数差异时比较有用)
144 | throttle({ calcRequstHash: (config) => config.url })
145 |
146 | // 在请求时, 设置请求将触发防抖策略 (优先级更高)
147 | request.post('/api/xxx', {}, { throttle: true })
148 |
149 | // set different delay judgment times
150 | request.post('/api/xxx', {}, { throttle: { delay: 1000 } })
151 | // in request, set capture throttle handle rule
152 | request.post('/api/xxx', {}, { throttle: { giveUp: GiveUpRule.cancel } })
153 | // 单个请求设置触发节流后抛出的异常消息
154 | request.post('/api/xxx', {}, { throttle: { throttleErrorMessage: 'xxxxx' } })
155 | ```
156 |
157 | #### merge
158 |
159 | ```typescript
160 | import { useAxiosPlugin } from 'axios-plugin/core'
161 | import { merge } from 'axios-plugins/plugins/merge'
162 |
163 | const request = axios.create({})
164 | /** 配置 */
165 | // 基础使用
166 | useAxiosPlugin(request).plugin(merge())
167 |
168 | // set delay judgment time
169 | merge({ delay: 200 })
170 |
171 | // 设置哪些请求将触发重复请求合并策略 (按需设置 `includes`, `excludes`)
172 | merge({ includes: '/api/', excludes: [] })
173 |
174 | // 自定义相同请求判定规则 (忽略参数差异时比较有用)
175 | merge({ calcRequstHash: (config) => config.url })
176 |
177 | // 在请求时, 设置请求将触发重复请求合并策略 (优先级更高)
178 | request.post('/api/xxx', {}, { merge: true })
179 |
180 | // 单个请求设置不同的触发延时
181 | request.post('/api/xxx', {}, { merge: { delay: 1000 } })
182 | ```
183 |
184 | ##### retry
185 |
186 | ```typescript
187 | import { useAxiosPlugin } from 'axios-plugin/core'
188 | import { retry } from 'axios-plugins/plugins/retry'
189 |
190 | const request = axios.create({})
191 | /** 配置 */
192 | // 基础使用 (必须设置重试次数)
193 | useAxiosPlugin(request).plugin(retry({ max: 3 }))
194 |
195 | // 设置哪些请求失败后将重试 (不建议设置这个option, 建议在请求时指定retry参数)
196 | retry({ includes: [], excludes: [] })
197 |
198 | // 自定义失败请求检查方法 (不建议设置这个option, 建议在添加一个 `axios.interceptors` 或 `transform()` 插件来判断响应结果)
199 | retry({ isExceptionRequest: (config) => false })
200 |
201 | // 在请求时, 设置请求将触发防抖策略 (优先级更高)
202 | request.post('/api/xxx', {}, { retry: 3 })
203 | ```
204 |
205 | ##### cancel
206 |
207 | > TIP: 如果请求指定了 cancelToken, 将会导致此插件失效.
208 |
209 | ```typescript
210 | import { useAxiosPlugin } from 'axios-plugin/core'
211 | import { cancel, cancelAll } from 'axios-plugins/plugins/cancel'
212 |
213 | const request = axios.create({})
214 | // 添加插件
215 | useAxiosPlugin(request).plugin(cancel())
216 |
217 | // > 中止所有在执行的请求
218 | cancelAll(request)
219 | ```
220 |
221 | #### transform
222 |
223 | ```typescript
224 | import { useAxiosPlugin } from 'axios-plugin/core'
225 | import { transform } from 'axios-plugins/plugins/transform'
226 |
227 | const request = axios.create({})
228 | // 添加插件
229 | useAxiosPlugin(request).plugin(
230 | transform({
231 | // + 转换请求参数
232 | request: (config) => {
233 | // TODO
234 | return config
235 | },
236 | // + 转换响应参数
237 | response: (res) => {
238 | // TODO
239 | return res
240 | },
241 | // + 转换异常信息
242 | capture: (e) => {
243 | // TODO
244 | throw e
245 | }
246 | })
247 | )
248 | ```
249 |
250 | #### cache
251 |
252 | ```typescript
253 | import { useAxiosPlugin } from 'axios-plugin/core'
254 | import { cache } from 'axios-plugins/plugins/cache'
255 |
256 | const request = axios.create({})
257 | // 添加插件
258 | useAxiosPlugin(request).plugin(cache())
259 |
260 | // 设置全局缓存失效时间
261 | cache({ expires: Date.now() + 24 * 60 * 60 * 1000 })
262 | // 设置缓存存储位置 (默认: sessionStorage)
263 | cache({ storage: localStorage })
264 | // 设置 storage 中, 缓存cache的字段名
265 | cache({ storageKey: 'axios.cache' })
266 | // 设置自定义的缓存key计算方法
267 | cache({ key: (config) => config.url })
268 |
269 | // 请求时, 指定此接口触发响应缓存
270 | request.post('/api', {}, { cache: true })
271 | // 自定义此接口缓存失效时间
272 | request.post('/api', {}, { cache: { expires: Date.now() } })
273 | // 自定义此接口缓存key
274 | request.post('/api', {}, { cache: { key: '/api' } })
275 | ```
276 |
277 | #### envs
278 |
279 | ```typescript
280 | import { useAxiosPlugin } from 'axios-plugin/core'
281 | import { envs } from 'axios-plugins/plugins/envs'
282 |
283 | const request = axios.create({})
284 | // 添加插件
285 | useAxiosPlugin(request).plugin(
286 | envs([
287 | {
288 | rule: () => process.env.NODE_ENV === 'development',
289 | config: {
290 | baseURL: 'http://dev'
291 | }
292 | },
293 | {
294 | rule: () => process.env.NODE_ENV === 'production',
295 | config: {
296 | baseURL: 'http://prod'
297 | }
298 | }
299 | ])
300 | )
301 | ```
302 |
303 | #### loading
304 |
305 | ```typescript
306 | import { useAxiosPlugin } from 'axios-plugin/core'
307 | import { loading } from 'axios-plugins/plugins/loading'
308 | import { loading as ElLoading } from 'element-plus'
309 |
310 | const request = axios.create({})
311 |
312 | let loadingEl
313 | // 添加插件
314 | useAxiosPlugin(request).plugin(
315 | loading({
316 | onTrigger: (show) => {
317 | if (show) {
318 | loadingEl = ElLoading({
319 | lock: true,
320 | text: 'Loading',
321 | spinner: 'el-icon-loading',
322 | background: 'rgba(0, 0, 0, 0.7)'
323 | })
324 | } else {
325 | loadingEl.close()
326 | }
327 | }
328 | })
329 | )
330 | // 自定义显示延时和隐藏延时 (避免频繁显示或隐藏 loading 效果)
331 | loading({ delay: 200, delayClose: 200 })
332 |
333 | // 指定请求禁用 loading
334 | request.post('/api', {}, { loading: false })
335 | ```
336 |
337 | #### mock
338 |
339 | > 建议借助三方工具(如: apifox, apipost 等) 实现 mock 能力
340 |
341 | ```typescript
342 | import { useAxiosPlugin } from 'axios-plugin/core'
343 | import { mock } from 'axios-plugins/plugins/mock'
344 |
345 | const request = axios.create({})
346 |
347 | useAxiosPlugin(request).plugin(
348 | // 添加插件, 并指定mock服务器地址
349 | mock({ mockUrl: 'http://mock' })
350 | )
351 | // 自定义启用条件 (如果没有使用 webpack, vite 那么此参数是必要的)
352 | mock({ enable: () => false })
353 |
354 | // 使用全局mock
355 | const request1 = axios.create({
356 | mock: true
357 | })
358 |
359 | // 按需mock (单个请求mock)
360 | request.post('/api', {}, { mock: true })
361 |
362 | // 针对不同接口使用不同的mock服务器
363 | request.post('/api', {}, { mock: { mock: true, mockUrl: 'http://mock1' } })
364 | ```
365 |
366 | #### normalize
367 |
368 | ```typescript
369 | import { useAxiosPlugin } from 'axios-plugin/core'
370 | import { normalize } from 'axios-plugins/plugins/normalize'
371 |
372 | const request = axios.create({})
373 |
374 | useAxiosPlugin(request).plugin(
375 | normalize({
376 | // 过滤url
377 | url: {
378 | // 过滤url中, 重复的 `//`, 如: `/api/a//b` -> `/api/a/b`
379 | noDuplicateSlash: true
380 | },
381 | // 设置完整的过滤参数
382 | data: {
383 | /** 过滤 null 值 */
384 | noNull: true,
385 | /** 过滤 undefined 值 */
386 | noUndefined: true,
387 | /** 过滤 nan */
388 | noNaN: true,
389 | /** 是否对对象进行递归 */
390 | deep: true
391 | },
392 | // 设置仅过滤 undefined
393 | params: true
394 | })
395 | )
396 | ```
397 |
398 | ### pathParams
399 |
400 | ```typescript
401 | import { useAxiosPlugin } from 'axios-plugin/core'
402 | import { pathParams } from 'axios-plugins/plugins/pathParams'
403 |
404 | const request = axios.create({})
405 |
406 | // 添加插件
407 | useAxiosPlugin(request).plugin(pathParams())
408 |
409 | // 设置仅从 params 中获取路径参数
410 | pathParams({ form: 'params' })
411 | ```
412 |
413 | #### sign
414 |
415 | ```typescript
416 | import { useAxiosPlugin } from 'axios-plugin/core'
417 | import { sign } from 'axios-plugins/plugins/sign'
418 |
419 | const request = axios.create({})
420 |
421 | // 添加插件
422 | useAxiosPlugin(request).plugin(sign({}))
423 |
424 | // 设置签名生成参数
425 | sign({
426 | // 签名字段
427 | key: 'sign',
428 | // 签名算法
429 | algorithm: 'md5',
430 | // 禁用参数排序
431 | sort: false,
432 | // 禁用过滤空值
433 | filter: false,
434 | // 加盐
435 | salt: 'xxxxx'
436 | })
437 | ```
438 |
439 | #### sentryCapture
440 |
441 | ```typescript
442 | import { useAxiosPlugin } from 'axios-plugin/core'
443 | import { sign } from 'axios-plugins/plugins/sign'
444 | import * as sentry from '@sentry/browser' // or @sentry/vue or @sentry/react ...
445 |
446 | const request = axios.create({})
447 |
448 | // 添加插件
449 | useAxiosPlugin(request).plugin(sentryCapture({ sentry }))
450 | ```
451 |
452 | #### onlySend
453 |
454 | ```typescript
455 | import { useAxiosPlugin } from 'axios-plugin/core'
456 | import { onlySend } from 'axios-plugins/plugins/onlySend'
457 |
458 | const request = axios.create({})
459 |
460 | // 添加插件
461 | useAxiosPlugin(request).plugin(onlySend())
462 |
463 | // 设置浏览器不支持 `navigator.sendBeacon` api时报错
464 | onlySend({ noSupport: 'error' })
465 | ```
466 |
467 | #### mp
468 |
469 | ```typescript
470 | import { useAxiosPlugin } from 'axios-plugin/core'
471 | import { mp } from 'axios-plugins/plugins/mp'
472 |
473 | const request = axios.create({})
474 |
475 | // 添加插件
476 | useAxiosPlugin(request).plugin(mp({ env: 'wx' }))
477 |
478 | // 指定不同的小程序平台
479 | mp({ env: 'tt' }) // 头条、抖音等等
480 | // 添加请求的公共配置
481 | mp({
482 | config: {
483 | /** ... */
484 | }
485 | })
486 | ```
487 |
488 | ## Thanks
489 |
490 | - [axios](https://axios-http.com/)
491 | - [axios-extensions](https://github.com/kuitos/axios-extensions)
492 | - [alova](https://github.com/alovajs/alova/)
493 | - [ahooks](https://ahooks.gitee.io/zh-CN/hooks/use-request/index)
494 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://badge.fury.io/js/axios-plugins)
2 | [](https://npmjs.org/package/axios-plugins)
3 | 
4 |
5 | 中文 | English
6 |
7 | > 用最小的侵入性, 为 axios 扩展更多的插件能力 (防抖、节流 等等)
8 | > 更新日志在 [CHANGELOG.md](./CHANGELOG.md)
9 |
10 | ## 特性
11 |
12 | - [轻量级] 核心包体积(1.37kb/gziped), 完整包体积(5.88kb/gziped), 支持 TreeShaking 和 单插件引用
13 | - [多实例支持] 插件缓存变量与 axios 实例相关联, 多个 axios 实例间互不影响
14 | - [低侵入性] 插件通过包装方式扩展, 不对实例现有配置产生影响, 不破坏 axios 的 api.
15 | - [低集成成本] 相比与其他插件来说, 不需要为集成插件做大量改动, 也没有什么学习成本.
16 | - [丰富的插件选择] 相比于其他基于 `axios.interceptors` 实现的插件来说, 这个库提供了更加丰富的插件选择
17 | - [可扩展] 提供了 `IPlugin` 接口, 只需要遵循接口规范, 即可自行扩展更多的插件能力
18 | - [无破坏性] 不需要像其他插件一样, 集成此插件可以不破坏现有代码结构
19 |
20 | ## 特别强调
21 |
22 | **插件遵循先进后出原则, 建议根据实际业务调整插件注册顺序.**
23 |
24 | ## 使用
25 |
26 | - 安装
27 |
28 | ```bash
29 | yarn add axios axios-plugins
30 | # 或
31 | npm install axios axios-plugins
32 | ```
33 |
34 | - 使用插件
35 |
36 | ```typescript
37 | import axios from 'axios'
38 | import { useAxiosPlugin, mock, loading } from 'axios-plugins'
39 |
40 | // 1. 定义axios实例 或 使用项目现有的axios实例
41 | export const request = axios.create({
42 | /* ... */
43 | })
44 |
45 | // 2. 添加插件支持
46 |
47 | useAxiosPlugin(axios) // axios 默认实例, 添加插件支持
48 | useAxiosPlugin(request) // axios 一般实例, 添加插件支持
49 |
50 | // 3. 使用插件
51 |
52 | useAxiosPlugin(axios)
53 | // 添加 mock 插件
54 | .plugin(mock())
55 |
56 | // 4. 请求过程中, 使用插件扩展参数
57 |
58 | request.post('/api', {}, { mock: true }) // 指定接口请求时, 向mock服务器发起请求
59 |
60 | // 5. 如果需要支持 `request()` 方式调用, 需要通过 `wrap()` 方法覆盖原实例
61 | const request2 = useAxiosPlugin(request)
62 | .plugin(mock()) // 添加插件
63 | .wrap() // wrap 函数包装 axios 实例
64 | ```
65 |
66 | - 按需引用
67 |
68 | > TIPS: 如果你正在使用 `vite` 或其他带有 TreeShaking 能力的编译器, 直接使用默认导出即可
69 |
70 | ```typescript
71 | import axios from 'axios'
72 | // + 将依赖导入修改成如下方式, 导入 `core` 和 需要使用的插件
73 | import { useAxiosPlugin } from 'axios-plugins/core'
74 | import { loading } from 'axios-plugins/plugins/loading'
75 |
76 | // 1. 定义axios实例 或 使用项目现有的axios实例
77 | export const request = axios.create({
78 | /* ... */
79 | })
80 |
81 | // 2. 添加插件
82 | useAxiosPlugin(request).plugin(loading())
83 | ```
84 |
85 | - 创建自定义插件
86 |
87 | ```typescript
88 | import axios from 'axios'
89 | import { useAxiosPlugin, IPlugin } from 'axios-plugins'
90 |
91 | /**
92 | * 定义插件
93 | *
94 | * @param {options} 插件参数
95 | * @returns IPlugin
96 | */
97 | const plug = (options: unknown): IPlugin => {
98 | // @ 定义url路径过滤器
99 | const filter = createUrlFilter(options.includes, options.excludes)
100 | return {
101 | name: '插件名',
102 | lifecycle: {
103 | /** 通过不同的hooks的组合, 扩展更多的插件能力 */
104 | }
105 | }
106 | }
107 | // 使用
108 | useAxiosPlugin(axios).plugin(plug({}))
109 | ```
110 |
111 | ## 插件
112 |
113 | | plugin | 名称 | 建议使用 | 描述 |
114 | | ------------------------------- | ---------------- | -------- | -------------------------------------------------------------------------------------- |
115 | | [debounce](#debounce) | 防抖 | | 在一段时间内发起的重复请求, 后执行的请求将等待上次请求完成后再执行 |
116 | | [throttle](#throttle) | 节流 | | 在一段时间内发起的重复请求, 后执行的请求将被抛弃 |
117 | | [merge](#merge) | 重复请求合并 | ★★★ | 在一段时间内发起的重复请求, 仅请求一次, 并将请求结果分别返回给不同的发起者 |
118 | | [retry](#retry) | 失败重试 | | 当请求失败(出错)后, 重试 n 次, 当全部失败时, 再抛出异常 |
119 | | [cancel](#cancel) | 取消(中止)请求 | | 提供 `cancelAll()` 方法, 中止当前在进行的所有请求 |
120 | | [transform](#transform) | 转换请求/响应 | ★★ | 替代`axios.interceptors`的使用, 用于统一管理 axios 请求过程 |
121 | | [cache](#cache) | 响应缓存 | | 存储请求响应内容, 在下次请求时返回 (需要在缓存时效内) |
122 | | [envs](#envs) | 多环境配置 | | 规范化 axios 多环境配置工具 |
123 | | [mock](#mock) | 模拟(调试用) | ★★★ | 提供全局或单个接口请求 mock 能力 |
124 | | [normalize](#normalize) | 参数规范化 | ★ | 过滤请求过程中产生的 undefined, null 等参数 |
125 | | [pathParams](#pathParams) | 路由参数处理 | ★ | 扩展对 Restful API 规范的路由参数支持 |
126 | | [sign](#sign) | 参数签名 | | 提供请求防篡改能力, 这个功能需要搭配后端逻辑实现 |
127 | | [loading](#loading) | 全局 loading | ★ | 提供全局 loading 统一控制能力, 减少每个加载方法都需要独立 loading 控制的工作量 |
128 | | [sentryCapture](#sentryCapture) | sentry 错误上报 | | 提供 sentry 捕获请求异常并上报的简单实现. |
129 | | [onlySend](#onlySend) | 仅发送 | | 提供 `navigator.sendBeacon` 方法封装, 实现页面离开时的埋点数据提交, 但这个需要后端支持 |
130 | | [mp](#mp) | 小程序请求适配器 | | 扩展对小程序(微信、头条、qq 等)、跨平台框架(uni-app, taro)网络请求的支持 |
131 | | [auth](#auth) | 请求前鉴权 | | 在发送请求前, 检查用户是否登录, 等待用户登录后再发起请求 (0.4.1 新增) |
132 |
133 | ## 插件使用示例
134 |
135 | > TIPS: 考虑篇幅原因, README 文档仅展示一些基础使用内容
136 |
137 | #### debounce
138 |
139 | ```typescript
140 | import { useAxiosPlugin } from 'axios-plugin/core'
141 | import { debounce } from 'axios-plugins/plugins/debounce'
142 |
143 | const request = axios.create({})
144 | /** 配置 */
145 | // 基础使用
146 | useAxiosPlugin(request).plugin(debounce())
147 |
148 | // 设置防抖策略判定延时参数 (相同接口请求, 在第一个请求完成后 200 毫秒内再次发起, 将触发防抖规则)
149 | debounce({ delay: 200 })
150 |
151 | // 设置哪些请求将触发防抖策略 (按需设置 `includes`, `excludes`)
152 | debounce({ includes: '/api/', excludes: [] })
153 |
154 | // 自定义相同请求判定规则 (忽略参数差异时比较有用)
155 | debounce({ calcRequstHash: (config) => config.url })
156 |
157 | // 在请求时, 设置请求将触发防抖策略 (优先级更高)
158 | request.post('/api/xxx', {}, { debounce: true })
159 |
160 | // 为单个请求设置不同的触发延时
161 | request.post('/api/xxx', {}, { debounce: { delay: 1000 } })
162 | ```
163 |
164 | #### throttle
165 |
166 | ```typescript
167 | import { useAxiosPlugin } from 'axios-plugin/core'
168 | import { throttle, GiveUpRule } from 'axios-plugins/plugins/throttle'
169 |
170 | const request = axios.create({})
171 | /** 配置 */
172 | // 基础使用
173 | useAxiosPlugin(request).plugin(throttle())
174 |
175 | // 设置节流策略判定延时参数 (相同接口请求, 在第一个请求完成后 200 毫秒内再次发起时, 将触发节流策略)
176 | throttle({ delay: 200 })
177 |
178 | // 设置哪些请求将触发节流策略 (按需设置 `includes`, `excludes`)
179 | throttle({ includes: '/api/', excludes: [] })
180 |
181 | // 设置节流策略处理规则
182 | throttle({ giveUp: GiveUpRule.throw }) // 抛出异常 (默认)
183 | throttle({ giveUp: GiveUpRule.cancel }) // 中断请求, 并返回空值结果
184 | throttle({ giveUp: GiveUpRule.slient }) // 静默, 既不返回成功、也不抛出异常
185 |
186 | // 自定义相同请求判定规则 (忽略参数差异时比较有用)
187 | throttle({ calcRequstHash: (config) => config.url })
188 |
189 | // 在请求时, 设置请求将触发防抖策略 (优先级更高)
190 | request.post('/api/xxx', {}, { throttle: true })
191 |
192 | // 单个请求设置不同的触发延时
193 | request.post('/api/xxx', {}, { throttle: { delay: 1000 } })
194 | // 单个请求设置不同的节流处理规则
195 | request.post('/api/xxx', {}, { throttle: { giveUp: GiveUpRule.cancel } })
196 | // 单个请求设置触发节流后抛出的异常消息
197 | request.post('/api/xxx', {}, { throttle: { throttleErrorMessage: '/api 短时间内重复执行了' } })
198 | ```
199 |
200 | #### merge
201 |
202 | ```typescript
203 | import { useAxiosPlugin } from 'axios-plugin/core'
204 | import { merge } from 'axios-plugins/plugins/merge'
205 |
206 | const request = axios.create({})
207 | /** 配置 */
208 | // 基础使用
209 | useAxiosPlugin(request).plugin(merge())
210 |
211 | // 设置重复请求合并策略延时 (相同接口请求, 在200ms内发起的重复请求, 仅请求一次, 并将请求结果分别返回给不同的发起者.)
212 | merge({ delay: 200 })
213 |
214 | // 设置哪些请求将触发重复请求合并策略 (按需设置 `includes`, `excludes`)
215 | merge({ includes: '/api/', excludes: [] })
216 |
217 | // 自定义相同请求判定规则 (忽略参数差异时比较有用)
218 | merge({ calcRequstHash: (config) => config.url })
219 |
220 | // 在请求时, 设置请求将触发重复请求合并策略 (优先级更高)
221 | request.post('/api/xxx', {}, { merge: true })
222 |
223 | // 单个请求设置不同的触发延时
224 | request.post('/api/xxx', {}, { merge: { delay: 1000 } })
225 | ```
226 |
227 | ##### retry
228 |
229 | ```typescript
230 | import { useAxiosPlugin } from 'axios-plugin/core'
231 | import { retry } from 'axios-plugins/plugins/retry'
232 |
233 | const request = axios.create({})
234 | /** 配置 */
235 | // 基础使用 (必须设置重试次数)
236 | useAxiosPlugin(request).plugin(retry({ max: 3 }))
237 |
238 | // 设置哪些请求失败后将重试 (不建议设置这个option, 建议在请求时指定retry参数)
239 | retry({ includes: [], excludes: [] })
240 |
241 | // 自定义失败请求检查方法 (不建议设置这个option, 建议在添加一个 `axios.interceptors` 或 `transform()` 插件来判断响应结果)
242 | retry({ isExceptionRequest: (config) => false })
243 |
244 | // 在请求时, 设置请求将触发防抖策略 (优先级更高)
245 | request.post('/api/xxx', {}, { retry: 3 })
246 | ```
247 |
248 | ##### cancel
249 |
250 | > TIP: 如果请求指定了 cancelToken, 将会导致此插件失效.
251 |
252 | ```typescript
253 | import { useAxiosPlugin } from 'axios-plugin/core'
254 | import { cancel, cancelAll } from 'axios-plugins/plugins/cancel'
255 |
256 | const request = axios.create({})
257 | // 添加插件
258 | useAxiosPlugin(request).plugin(cancel())
259 |
260 | // > 中止所有在执行的请求
261 | cancelAll(request)
262 | ```
263 |
264 | #### transform
265 |
266 | ```typescript
267 | import { useAxiosPlugin } from 'axios-plugin/core'
268 | import { transform } from 'axios-plugins/plugins/transform'
269 |
270 | const request = axios.create({})
271 | // 添加插件
272 | useAxiosPlugin(request).plugin(
273 | transform({
274 | // + 转换请求参数
275 | request: (config) => {
276 | // TODO
277 | return config
278 | },
279 | // + 转换响应参数
280 | response: (res) => {
281 | // TODO
282 | return res
283 | },
284 | // + 转换异常信息
285 | capture: (e) => {
286 | // TODO
287 | throw e
288 | }
289 | })
290 | )
291 | ```
292 |
293 | #### cache
294 |
295 | ```typescript
296 | import { useAxiosPlugin } from 'axios-plugin/core'
297 | import { cache } from 'axios-plugins/plugins/cache'
298 |
299 | const request = axios.create({})
300 | // 添加插件
301 | useAxiosPlugin(request).plugin(cache())
302 |
303 | // 设置全局缓存失效时间
304 | cache({ expires: Date.now() + 24 * 60 * 60 * 1000 })
305 | // 设置缓存存储位置 (默认: sessionStorage)
306 | cache({ storage: localStorage })
307 | // 设置 storage 中, 缓存cache的字段名
308 | cache({ storageKey: 'axios.cache' })
309 | // 设置自定义的缓存key计算方法
310 | cache({ key: (config) => config.url })
311 |
312 | // 请求时, 指定此接口触发响应缓存
313 | request.post('/api', {}, { cache: true })
314 | // 自定义此接口缓存失效时间
315 | request.post('/api', {}, { cache: { expires: Date.now() } })
316 | // 自定义此接口缓存key
317 | request.post('/api', {}, { cache: { key: '/api' } })
318 | ```
319 |
320 | #### envs
321 |
322 | ```typescript
323 | import { useAxiosPlugin } from 'axios-plugin/core'
324 | import { envs } from 'axios-plugins/plugins/envs'
325 |
326 | const request = axios.create({})
327 | // 添加插件
328 | useAxiosPlugin(request).plugin(
329 | envs([
330 | {
331 | rule: () => process.env.NODE_ENV === 'development',
332 | config: {
333 | baseURL: 'http://dev'
334 | }
335 | },
336 | {
337 | rule: () => process.env.NODE_ENV === 'production',
338 | config: {
339 | baseURL: 'http://prod'
340 | }
341 | }
342 | ])
343 | )
344 | ```
345 |
346 | #### loading
347 |
348 | ```typescript
349 | import { useAxiosPlugin } from 'axios-plugin/core'
350 | import { loading } from 'axios-plugins/plugins/loading'
351 | import { loading as ElLoading } from 'element-plus'
352 |
353 | const request = axios.create({})
354 |
355 | let loadingEl
356 | // 添加插件
357 | useAxiosPlugin(request).plugin(
358 | loading({
359 | onTrigger: (show) => {
360 | if (show) {
361 | loadingEl = ElLoading({
362 | lock: true,
363 | text: 'Loading',
364 | spinner: 'el-icon-loading',
365 | background: 'rgba(0, 0, 0, 0.7)'
366 | })
367 | } else {
368 | loadingEl.close()
369 | }
370 | }
371 | })
372 | )
373 | // 自定义显示延时和隐藏延时 (避免频繁显示或隐藏 loading 效果)
374 | loading({ delay: 200, delayClose: 200 })
375 |
376 | // 指定请求禁用 loading
377 | request.post('/api', {}, { loading: false })
378 | ```
379 |
380 | #### mock
381 |
382 | > 建议借助三方工具(如: apifox, apipost 等) 实现 mock 能力
383 |
384 | ```typescript
385 | import { useAxiosPlugin } from 'axios-plugin/core'
386 | import { mock } from 'axios-plugins/plugins/mock'
387 |
388 | const request = axios.create({})
389 |
390 | useAxiosPlugin(request).plugin(
391 | // 添加插件, 并指定mock服务器地址
392 | mock({ mockUrl: 'http://mock' })
393 | )
394 | // 自定义启用条件 (如果没有使用 webpack, vite 那么此参数是必要的)
395 | mock({ enable: () => false })
396 |
397 | // 使用全局mock
398 | const request1 = axios.create({
399 | mock: true
400 | })
401 |
402 | // 按需mock (单个请求mock)
403 | request.post('/api', {}, { mock: true })
404 |
405 | // 针对不同接口使用不同的mock服务器
406 | request.post('/api', {}, { mock: { mock: true, mockUrl: 'http://mock1' } })
407 | ```
408 |
409 | #### normalize
410 |
411 | ```typescript
412 | import { useAxiosPlugin } from 'axios-plugin/core'
413 | import { normalize } from 'axios-plugins/plugins/normalize'
414 |
415 | const request = axios.create({})
416 |
417 | useAxiosPlugin(request).plugin(
418 | normalize({
419 | // 过滤url
420 | url: {
421 | // 过滤url中, 重复的 `//`, 如: `/api/a//b` -> `/api/a/b`
422 | noDuplicateSlash: true
423 | },
424 | // 设置完整的过滤参数
425 | data: {
426 | /** 过滤 null 值 */
427 | noNull: true,
428 | /** 过滤 undefined 值 */
429 | noUndefined: true,
430 | /** 过滤 nan */
431 | noNaN: true,
432 | /** 是否对对象进行递归 */
433 | deep: true
434 | },
435 | // 设置仅过滤 undefined
436 | params: true
437 | })
438 | )
439 | ```
440 |
441 | ### pathParams
442 |
443 | ```typescript
444 | import { useAxiosPlugin } from 'axios-plugins/core'
445 | import { pathParams } from 'axios-plugins/plugins/pathParams'
446 |
447 | const request = axios.create({})
448 |
449 | // 添加插件
450 | useAxiosPlugin(request).plugin(pathParams())
451 |
452 | // 设置仅从 params 中获取路径参数
453 | pathParams({ form: 'params' })
454 | ```
455 |
456 | #### sign
457 |
458 | ```typescript
459 | import { useAxiosPlugin } from 'axios-plugins/core'
460 | import { sign } from 'axios-plugins/plugins/sign'
461 |
462 | const request = axios.create({})
463 |
464 | // 添加插件
465 | useAxiosPlugin(request).plugin(sign({}))
466 |
467 | // 设置签名生成参数
468 | sign({
469 | // 签名字段
470 | key: 'sign',
471 | // 签名算法
472 | algorithm: 'md5',
473 | // 禁用参数排序
474 | sort: false,
475 | // 禁用过滤空值
476 | filter: false,
477 | // 加盐
478 | salt: 'xxxxx'
479 | })
480 | ```
481 |
482 | #### sentryCapture
483 |
484 | ```typescript
485 | import { useAxiosPlugin } from 'axios-plugins/core'
486 | import { sign } from 'axios-plugins/plugins/sign'
487 | import * as sentry from '@sentry/browser' // or @sentry/vue or @sentry/react ...
488 |
489 | const request = axios.create({})
490 |
491 | // 添加插件
492 | useAxiosPlugin(request).plugin(sentryCapture({ sentry }))
493 | ```
494 |
495 | #### onlySend
496 |
497 | ```typescript
498 | import { useAxiosPlugin } from 'axios-plugins/core'
499 | import { onlySend } from 'axios-plugins/plugins/onlySend'
500 |
501 | const request = axios.create({})
502 |
503 | // 添加插件
504 | useAxiosPlugin(request).plugin(onlySend())
505 |
506 | // 设置浏览器不支持 `navigator.sendBeacon` api时报错
507 | onlySend({ noSupport: 'error' })
508 | ```
509 |
510 | #### mp
511 |
512 | ```typescript
513 | import { useAxiosPlugin } from 'axios-plugins/core'
514 | import { mp } from 'axios-plugins/plugins/mp'
515 |
516 | const request = axios.create({})
517 |
518 | // 添加插件
519 | useAxiosPlugin(request).plugin(mp({ env: 'wx' }))
520 |
521 | // 指定不同的小程序平台
522 | mp({ env: 'tt' }) // 头条、抖音等等
523 | // 添加请求的公共配置
524 | mp({
525 | config: {
526 | /** ... */
527 | }
528 | })
529 | ```
530 |
531 | #### auth
532 |
533 | ```typescript
534 | import { useAxiosPlugin } from 'axios-plugins/core'
535 | import { auth } from 'axios-plugins/plugins/auth'
536 |
537 | const request = axios.create({})
538 |
539 | // 添加插件
540 | useAxiosPlugin(request).plugin(
541 | auth({
542 | login: async (request): boolean => {
543 | // TODO check login
544 | return true
545 | }
546 | })
547 | )
548 | ```
549 |
550 | ## FAQ
551 |
552 | 1. 关于单元测试
553 |
554 | 目前, 核心方法 `useAxiosPlugin`、`utils/*` 相关方法单元测已经编写完成. `plugins/*` 相关的插件实现单元测试我会逐步补全, 这个目标可能需要一段时间去完成.
555 |
556 | ## 参考及感谢
557 |
558 | - [axios](https://axios-http.com/)
559 | - [axios-extensions](https://github.com/kuitos/axios-extensions)
560 | - [alova](https://github.com/alovajs/alova/)
561 | - [ahooks](https://ahooks.gitee.io/zh-CN/hooks/use-request/index)
562 | - [axios-logger](https://www.npmjs.com/package/axios-logger)
563 |
--------------------------------------------------------------------------------
/__tests__/plugins/merge.spec.ts:
--------------------------------------------------------------------------------
1 | import nock from 'nock'
2 | import axios, { AxiosInstance } from 'axios'
3 | import { useAxiosPlugin } from '../../src/use-plugin'
4 | import { merge } from '../../src'
5 | import { delay } from '../../src/utils/delay'
6 |
7 | describe('测试 `merge()` 插件', () => {
8 | const BASE_URL: string = 'http://test'
9 | let request!: AxiosInstance
10 | beforeEach(() => {
11 | const server = nock(BASE_URL)
12 | let a: number = 0
13 | let b: number = 0
14 | let c: number = 0
15 | server.post('/case').delay(200).reply(200, { result: 'success' }).persist()
16 | server
17 | .post('/case2')
18 | .reply(200, () => (a++, { a }))
19 | .persist()
20 | server
21 | .post('/case3')
22 | .reply(200, () => (b++, { b }))
23 | .persist()
24 | server
25 | .post('/case4')
26 | .reply(200, () => (c++, { c }))
27 | .persist()
28 | axios.defaults.baseURL = BASE_URL
29 | request = useAxiosPlugin(axios.create({ baseURL: BASE_URL }))
30 | .plugin(merge({ includes: true, delay: 200 }))
31 | .wrap()
32 | })
33 |
34 | test('case - 正常请求', async () => {
35 | const res = await request.post('/case')
36 | expect(res.data.result).toBe('success')
37 | expect(Object.keys(request['__shared__'].merge).length).toBe(1)
38 | await delay(300)
39 | expect(Object.keys(request['__shared__'].merge).length).toBe(0)
40 | })
41 |
42 | test('case - 顺序请求(合并)', async () => {
43 | const res = await request.post('/case2')
44 | expect(res.data.a).toBe(1)
45 | const res2 = await request.post('/case2')
46 | expect(res2.data.a).toBe(1)
47 | })
48 |
49 | test('case - 顺序请求(不合并)', async () => {
50 | const res = await request.post('/case3')
51 | expect(res.data.b).toBe(1)
52 | await delay(300)
53 | const res2 = await request.post('/case3')
54 | expect(res2.data.b).toBe(2)
55 | })
56 |
57 | test('case - 并发请求合并', async () => {
58 | const [res1, res2] = await Promise.all([request.post('/case4'), request.post('/case4')])
59 | expect(res1.data.c).toBe(1)
60 | expect(res2.data.c).toBe(1)
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/__tests__/use-plugin.spec.ts:
--------------------------------------------------------------------------------
1 | import nock from 'nock'
2 | import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'
3 | import { ILifecycleHookObject, IPlugin, ISharedCache } from '../src/intf'
4 | import { IGNORE_COVERAGE, useAxiosPlugin } from '../src/use-plugin'
5 | import { SlientError } from '../src/utils/create-abort-chain'
6 |
7 | describe('测试 `useAxiosPlugin()`', () => {
8 | const BASE_URL: string = 'http://test'
9 |
10 | beforeAll(() => {
11 | const server = nock(BASE_URL)
12 | server.get('/case').reply(200).persist()
13 | server.post('/case').reply(200).persist()
14 | server.put('/case').reply(200).persist()
15 | server.head('/case').reply(200).persist()
16 | server.patch('/case').reply(200).persist()
17 | server.merge('/case').reply(200).persist()
18 | server.delete('/case').reply(200).persist()
19 | server.options('/case').reply(200).persist()
20 | server.get('/success').reply(200, { result: 'success' }).persist()
21 | server.get('/failure').reply(500).persist()
22 | axios.defaults.baseURL = BASE_URL
23 | })
24 |
25 | test('case - `useAxiosPlugin()` 返回结果应包含 `plugin()`, `wrap()` 两个方法', () => {
26 | const request = axios.create({})
27 | const plugin = useAxiosPlugin(request)
28 | expect(plugin).toHaveProperty('plugin')
29 | expect(plugin).toHaveProperty('wrap')
30 | })
31 |
32 | test('case - 调用 `useAxiosPlugin()` 后, 映射的 `AxiosException` 扩展实例应继承原有实例的配置', () => {
33 | const request = axios.create({
34 | baseURL: 'http://haha',
35 | method: 'post'
36 | })
37 | const requestInterceptors = (config) => config
38 | const responseInterceptors = (response) => response
39 | const captureAxiosError = (err) => err
40 | request.interceptors.request.use(requestInterceptors, captureAxiosError)
41 | request.interceptors.response.use(responseInterceptors, captureAxiosError)
42 | const oldInterceptors = { ...request.interceptors }
43 | useAxiosPlugin(request)
44 | expect(request.defaults.baseURL).toBe('http://haha')
45 | expect(request.defaults.method).toBe('post')
46 | expect(oldInterceptors).toEqual(request.interceptors)
47 | })
48 |
49 | test('case - 调用 `useAxiosPlugin()` 后, Axios 实例除了禁止覆盖属性外, 其他属性应映射到扩展类', () => {
50 | const request = axios.create({})
51 | useAxiosPlugin(request)
52 | const refer = new axios.Axios({})
53 | for (const key of Object.getOwnPropertyNames(refer)) {
54 | // 检查映射的属性是否存在
55 | expect(request[key]).not.toBeUndefined()
56 | // ? 如果忽略映射的属性, 应保持原样
57 | if (IGNORE_COVERAGE.includes(key)) {
58 | expect(Object.getOwnPropertyDescriptor(request, key)?.writable).toBeTruthy()
59 | } else {
60 | // 否则, 检查属性是否是映射过的属性
61 | expect(Object.getOwnPropertyDescriptor(request, key)?.writable).toBeUndefined()
62 | expect(Object.getOwnPropertyDescriptor(request, key)?.get).toBeTruthy()
63 | }
64 | }
65 | })
66 |
67 | test('case - `useAxiosPlugin()` 调用后, axios 扩展属性类型应当是正确的', async () => {
68 | const request = axios.create({})
69 | useAxiosPlugin(request)
70 | expect(request['__plugins__']).toEqual([])
71 | expect(request['__shared__']).toEqual({})
72 | // 校验 getter/setter 方法是否齐全
73 | const __plugins__ = Object.getOwnPropertyDescriptor(request, '__plugins__')
74 | const __shared__ = Object.getOwnPropertyDescriptor(request, '__shared__')
75 | expect(__plugins__?.get).toBeTruthy()
76 | expect(__plugins__?.set).toBeTruthy()
77 | expect(__shared__?.get).toBeTruthy()
78 | expect(__shared__?.set).toBeTruthy()
79 | })
80 |
81 | test('case - 调用 `useAxiosPlugin().wrap()` 后, 原有的`function wrap()` 应当被覆盖', () => {
82 | const request = axios.create({})
83 | const req2 = useAxiosPlugin(request).wrap()
84 | expect(typeof req2 === 'function').toBeTruthy()
85 | expect(request).not.toBe(req2)
86 | })
87 |
88 | test('case - 调用 `useAxiosPlugin().plug()`, 插件可以正常注册', () => {
89 | const request = axios.create({})
90 | const plug1: IPlugin = { name: 'plug1' }
91 | const plug2: IPlugin = { name: 'plug2' }
92 | useAxiosPlugin(request).plugin(plug1).plugin(plug2)
93 | expect(request['__plugins__']).toEqual([plug1, plug2])
94 | })
95 |
96 | test('case - 当插件注册后, `beforeRegister()` 将被触发一次', () => {
97 | const request = axios.create({})
98 | const plug: IPlugin = {
99 | name: 'plug',
100 | beforeRegister: jest.fn()
101 | }
102 | useAxiosPlugin(request).plugin(plug)
103 | expect(plug.beforeRegister).toHaveBeenCalled()
104 | })
105 | test('case - 如果插件指定了不允许重复注册, 那么当重复注册时抛出异常', () => {
106 | const request = axios.create({})
107 | const plug: IPlugin = {
108 | name: 'plug',
109 | beforeRegister(axios) {
110 | if (axios.__plugins__.includes(plug)) {
111 | throw new Error('插件被重复注册了')
112 | }
113 | }
114 | }
115 | expect(() => useAxiosPlugin(request).plugin(plug).plugin(plug)).toThrowError('插件被重复注册了')
116 | })
117 |
118 | test('case - 如果指定了 `enforce`, 那么插件应该按照正确顺序排序', () => {
119 | const request = axios.create({})
120 | const plug1: IPlugin = { name: 'plug1' }
121 | const plug2: IPlugin = { name: 'plug2' }
122 | const plug3: IPlugin = { name: 'plug3', enforce: 'post' }
123 | const plug4: IPlugin = { name: 'plug4', enforce: 'pre' }
124 | const plug5: IPlugin = { name: 'plug5' }
125 | useAxiosPlugin(request).plugin(plug1).plugin(plug2).plugin(plug3).plugin(plug4).plugin(plug5)
126 | expect(request['__plugins__']).toEqual([plug4, plug1, plug2, plug5, plug3])
127 | })
128 |
129 | test('valid - 验证发起一次请求, 插件是否被触发(任意lifecycle钩子被调用)', async () => {
130 | const plug: IPlugin = {
131 | name: 'plug',
132 | lifecycle: {
133 | completed: jest.fn()
134 | }
135 | }
136 | const request = axios.create({})
137 | useAxiosPlugin(request).plugin(plug)
138 | expect(plug.lifecycle).toHaveProperty('completed')
139 | await request.request({ url: '/case' })
140 | expect(plug.lifecycle?.completed).toHaveBeenCalled()
141 | })
142 |
143 | test('valid - 验证发起多次请求, 插件触发次数是否正确', async () => {
144 | const plug: IPlugin = {
145 | name: 'plug',
146 | lifecycle: {
147 | completed: jest.fn()
148 | }
149 | }
150 | const request = axios.create({})
151 | useAxiosPlugin(request).plugin(plug)
152 | await request.request({ url: '/case' })
153 | await request.request({ url: '/case' })
154 | expect(plug.lifecycle?.completed).toBeCalledTimes(2)
155 | })
156 |
157 | test('valid - 验证请求失败情况下, 插件是否被正确触发', async () => {
158 | const plug: IPlugin = {
159 | name: 'plug',
160 | lifecycle: {
161 | completed: jest.fn()
162 | }
163 | }
164 | const request = axios.create({})
165 | useAxiosPlugin(request).plugin(plug)
166 | let capture: boolean = false
167 | try {
168 | await request.request({ url: '/failure' })
169 | } catch (error) {
170 | capture = true
171 | } finally {
172 | expect(capture).toBeTruthy()
173 | expect(plug.lifecycle?.completed).toBeCalledTimes(1)
174 | }
175 | })
176 |
177 | test('valid - 验证多种方式发起请求, 插件是否被正确触发', async () => {
178 | const plug: IPlugin = {
179 | name: 'plug',
180 | lifecycle: {
181 | completed: jest.fn()
182 | }
183 | }
184 | const request = axios.create({})
185 | useAxiosPlugin(request).plugin(plug)
186 | await request.request({ url: '/case' })
187 | await request.get('/case')
188 | await request.delete('/case')
189 | await request.head('/case')
190 | await request.options('/case')
191 | await request.post('/case')
192 | await request.put('/case')
193 | await request.patch('/case')
194 | await request.postForm('/case')
195 | await request.putForm('/case')
196 | await request.patchForm('/case')
197 | expect(plug.lifecycle?.completed).toBeCalledTimes(11)
198 | })
199 | test('valid - 验证 wrap() 函数包装后, 插件是否被正确触发', async () => {
200 | const plug: IPlugin = {
201 | name: 'plug',
202 | lifecycle: {
203 | completed: jest.fn()
204 | }
205 | }
206 | const req = axios.create({ baseURL: BASE_URL })
207 | const request: AxiosInstance = useAxiosPlugin(req).plugin(plug).wrap()
208 | await request({ url: '/case' })
209 | await request.request({ url: '/case' })
210 | await request.get('/case')
211 | await request.post('/case')
212 | expect(plug.lifecycle?.completed).toBeCalledTimes(4)
213 | })
214 |
215 | test('valid - 验证请求过程中, 插件的钩子是否被正确触发', async () => {
216 | const plug: IPlugin = {
217 | name: 'plug',
218 | lifecycle: {
219 | preRequestTransform: jest.fn((arg0: any) => arg0),
220 | postResponseTransform: jest.fn((arg0: any) => arg0),
221 | completed: jest.fn()
222 | }
223 | }
224 | const request = axios.create({ baseURL: BASE_URL })
225 | useAxiosPlugin(request).plugin(plug)
226 | await request.get('/success')
227 | expect(plug.lifecycle?.preRequestTransform).toBeCalledTimes(1)
228 | expect(plug.lifecycle?.postResponseTransform).toBeCalledTimes(1)
229 | expect(plug.lifecycle?.completed).toBeCalledTimes(1)
230 | })
231 |
232 | test('valid - 当钩子函数为 ILifecycleHookObject 类型时, 可以被正常触发', async () => {
233 | const plug: IPlugin = {
234 | name: 'plug',
235 | lifecycle: {
236 | preRequestTransform: {
237 | runWhen: jest.fn(() => false),
238 | handler: jest.fn((config) => config)
239 | },
240 | postResponseTransform: {
241 | runWhen: jest.fn(() => true),
242 | handler: jest.fn((config) => config)
243 | }
244 | }
245 | }
246 | const request = axios.create({ baseURL: BASE_URL })
247 | useAxiosPlugin(request).plugin(plug)
248 | // 捕获请求异常
249 | await request.get('/success')
250 | // runWhen() return False
251 | expect((plug.lifecycle?.preRequestTransform as ILifecycleHookObject).runWhen).toBeCalled()
252 | expect((plug.lifecycle?.preRequestTransform as ILifecycleHookObject).handler).not.toBeCalled()
253 | // runWhen() return True
254 | expect((plug.lifecycle?.postResponseTransform as ILifecycleHookObject).runWhen).toBeCalled()
255 | expect((plug.lifecycle?.postResponseTransform as ILifecycleHookObject).handler).toBeCalled()
256 | })
257 |
258 | test('valid - 验证请求过程中, 插件的钩子触发顺序是否正确', async () => {
259 | let step: number = 0
260 | const plug: IPlugin = {
261 | name: 'plug',
262 | lifecycle: {
263 | preRequestTransform: (config) => {
264 | step++
265 | expect(step).toBe(1)
266 | return config
267 | },
268 | postResponseTransform: (response) => {
269 | step++
270 | expect(step).toBe(2)
271 | return response
272 | },
273 | completed: () => {
274 | step++
275 | expect(step).toBe(3)
276 | }
277 | }
278 | }
279 | const request = axios.create({ baseURL: BASE_URL })
280 | useAxiosPlugin(request).plugin(plug)
281 | await request.get('/success')
282 | })
283 |
284 | test('valid - 验证请求过程中, 插件的钩子获取到的参数是否正确', async () => {
285 | /** 验证原始参数 */
286 | const checkConfigValue = (origin: AxiosRequestConfig) => {
287 | expect(origin.url).toBe('/success')
288 | expect(origin.data).toEqual({ a: 1 })
289 | }
290 | let ss: Array = []
291 | /** 验证共享内存指针唯一性 */
292 | const checkShared = (shared: ISharedCache): void => {
293 | for (const s of ss) expect(s).toBe(shared)
294 | ss.push(shared)
295 | }
296 | /** 验证 origin 是从 config 上复制的 */
297 | const originIsCopyed = (config: AxiosRequestConfig, origin: AxiosRequestConfig) => {
298 | // origin 为 config 的备份结果
299 | // config 与 origin 指针不同, 值相同
300 | expect(config).not.toBe(origin)
301 | expect(config).toEqual(origin)
302 | }
303 | const plug: IPlugin = {
304 | name: 'plug',
305 | lifecycle: {
306 | preRequestTransform: (config, { shared, origin }) => {
307 | checkConfigValue(config)
308 | checkConfigValue(origin)
309 | originIsCopyed(config, origin)
310 | checkShared(shared)
311 | return config
312 | },
313 | postResponseTransform: (response, { shared, origin }) => {
314 | checkConfigValue(origin)
315 | checkShared(shared)
316 | expect(response.config.url).toBe('/success')
317 | return response
318 | },
319 | completed: ({ shared, origin }) => {
320 | checkConfigValue(origin)
321 | checkShared(shared)
322 | }
323 | }
324 | }
325 | const request = axios.create({ baseURL: BASE_URL })
326 | useAxiosPlugin(request).plugin(plug)
327 | await request.get('/success', { data: { a: 1 } })
328 | })
329 |
330 | test('valid - 验证请求失败情况下, `captureException` 钩子函数是否被正确触发', async () => {
331 | const plug: IPlugin = {
332 | name: 'plug',
333 | lifecycle: {
334 | captureException: jest.fn((e) => {
335 | throw e
336 | }),
337 | completed: jest.fn()
338 | }
339 | }
340 | const request = axios.create({ baseURL: BASE_URL })
341 | useAxiosPlugin(request).plugin(plug)
342 | // 捕获请求异常
343 | await expect(request.get('/failure')).rejects.toThrow(AxiosError)
344 | expect(plug.lifecycle?.captureException).toBeCalledTimes(1)
345 | expect(plug.lifecycle?.completed).toBeCalledTimes(1)
346 | })
347 | test('valid - 验证请求失败情况下, `captureException` 钩子异常处理行为是否符合预期', async () => {
348 | const plug: IPlugin = {
349 | name: 'plug',
350 | lifecycle: {
351 | captureException: (e, { origin }) => {
352 | const { n } = origin.params
353 | switch (n) {
354 | case 1:
355 | return e
356 | case 2:
357 | throw e
358 | case 3:
359 | break
360 | }
361 | }
362 | }
363 | }
364 | const request = axios.create({ baseURL: BASE_URL })
365 | useAxiosPlugin(request).plugin(plug)
366 | // 捕获请求异常
367 | await expect(request.get('/failure', { params: { n: 1 } })).resolves.toThrow(AxiosError)
368 | await expect(request.get('/failure', { params: { n: 2 } })).rejects.toThrow(AxiosError)
369 | await expect(request.get('/failure', { params: { n: 3 } })).resolves.toBeUndefined()
370 | })
371 |
372 | test('valid - 验证插件执行过程出错, `captureException` 钩子能否正确捕获异常', async () => {
373 | const plug: IPlugin = {
374 | name: 'plug',
375 | lifecycle: {
376 | captureException: (e, { origin }) => {
377 | const { n } = origin.params
378 | switch (n) {
379 | case 1:
380 | return e
381 | case 2:
382 | throw e
383 | case 3:
384 | break
385 | }
386 | }
387 | }
388 | }
389 | const request = axios.create({ baseURL: BASE_URL })
390 | useAxiosPlugin(request).plugin(plug)
391 | // 捕获请求异常
392 | await expect(request.get('/failure', { params: { n: 1 } })).resolves.toThrow(AxiosError)
393 | await expect(request.get('/failure', { params: { n: 2 } })).rejects.toThrow(AxiosError)
394 | await expect(request.get('/failure', { params: { n: 3 } })).resolves.toBeUndefined()
395 | })
396 |
397 | test('valid - 验证多插件重复触发 `captureException`, 钩子能否正确捕获异常', async () => {
398 | let n: number = 0
399 | const plug: IPlugin = {
400 | name: 'plug',
401 | lifecycle: {
402 | captureException: (e, { origin }) => {
403 | n++
404 | return n
405 | }
406 | }
407 | }
408 | const plug2: IPlugin = {
409 | name: 'plug',
410 | lifecycle: {
411 | captureException: (e, { origin }) => {
412 | const { n } = origin.params
413 | switch (n) {
414 | case 1:
415 | return e
416 | case 2:
417 | throw e
418 | case 3:
419 | break
420 | }
421 | }
422 | }
423 | }
424 | const request = axios.create({ baseURL: BASE_URL })
425 | useAxiosPlugin(request).plugin(plug)
426 | // 捕获请求异常
427 | await expect(request.get('/failure', { params: { n: 1 } })).resolves.toThrow(AxiosError)
428 | await expect(request.get('/failure', { params: { n: 2 } })).rejects.toThrow(AxiosError)
429 | await expect(request.get('/failure', { params: { n: 3 } })).resolves.toBeUndefined()
430 | })
431 |
432 | test('valid - 验证 transformRequest 阶段的 `abort`, `abortError`, `slient` 的阻塞是否符合预期', async () => {
433 | let n: number = 0
434 | const plug: IPlugin = {
435 | name: 'plug',
436 | lifecycle: {
437 | transformRequest(config, _, { abort, abortError, slient }) {
438 | n++
439 | if (n === 1) abort('abort')
440 | if (n === 2) abortError('abort error')
441 | if (n === 3) {
442 | try {
443 | slient()
444 | } catch (error) {
445 | expect(error instanceof SlientError)
446 | }
447 | }
448 | return config
449 | }
450 | }
451 | }
452 | const request = axios.create({ baseURL: BASE_URL })
453 | useAxiosPlugin(request).plugin(plug)
454 |
455 | // 捕获请求异常
456 | expect(request.get('/success')).resolves.toBe('abort')
457 | expect(request.get('/success')).rejects.toBe('abort error')
458 | request.get('/success')
459 | })
460 |
461 | test('other - 冗余检查, 重复触发 `useAxiosPlugin()` 仅触发一次 `injectPluginHooks()`', () => {
462 | const request = axios.create({})
463 | const plug: IPlugin = { name: 'plug' }
464 | useAxiosPlugin(request).plugin(plug)
465 | expect(request['__plugins__']).toEqual([plug])
466 | useAxiosPlugin(request)
467 | expect(request['__plugins__']).toEqual([plug])
468 | })
469 |
470 | test('other - `useAxiosPlugin()` 插件扩展属性应被隔离, `axios.create()` 创建的新实例不继承插件属性', async () => {
471 | const plug: IPlugin = {
472 | name: 'plug',
473 | lifecycle: {
474 | completed: jest.fn()
475 | }
476 | }
477 | useAxiosPlugin(axios).plugin(plug)
478 | const request = axios.create({})
479 | // 1. 扩展属性不应存在
480 | expect(request['__plugins__']).toBeUndefined()
481 | expect(request['__shared__']).toBeUndefined()
482 | // 2. 类成员变量应保持原样, 而不是扩展的 getter/setter (用 request 方法实验)
483 | expect(Object.getOwnPropertyDescriptor(request, 'request')?.writable).toBeTruthy()
484 | await request.get('/case')
485 | // 3. 检查插件生命周期时间有没有被触发
486 | expect(plug.lifecycle?.completed).not.toHaveBeenCalled()
487 | })
488 |
489 | test('other - `useAxiosPlugin()` 插件扩展属性应被隔离, 不受继承影响', async () => {
490 | const plug: IPlugin = {
491 | name: 'plug',
492 | lifecycle: {
493 | completed: jest.fn()
494 | }
495 | }
496 | useAxiosPlugin(axios).plugin(plug)
497 | const request = new axios.Axios({ baseURL: BASE_URL })
498 | // 1. 扩展属性不应存在
499 | expect(request['__plugins__']).toBeUndefined()
500 | expect(request['__shared__']).toBeUndefined()
501 | await request.get('/case')
502 | // 2. 检查插件生命周期时间有没有被触发
503 | // TIPS: 由于 function wrap() 包裹的关系, 无法通过 `Object.getOwnPropertyDescriptor` 获取新实例是否被影响
504 | // 这里通过插件的钩子是否被处罚, 来验证是否存在影响
505 | expect(plug.lifecycle?.completed).not.toHaveBeenCalled()
506 | })
507 | })
508 |
509 | describe('测试 `IPlugin` 钩子组合特性', () => {
510 | const BASE_URL: string = 'http://test'
511 | beforeAll(() => {
512 | const server = nock(BASE_URL)
513 | server.post('/case1').delay(200).reply(200, { result: 'success' }).persist()
514 | server.post('/case2').query({ a: 123 }).reply(200, { result: 'success' }).persist()
515 | server.post('/case3').reply(200, { result: 'failure', message: '请求出错' }).persist()
516 | axios.defaults.baseURL = BASE_URL
517 | })
518 | test('case - 组合锁机制', async () => {
519 | const plug: IPlugin = {
520 | name: 'plug',
521 | lifecycle: {
522 | preRequestTransform(config, { origin, shared }) {
523 | if (!shared['plug']) {
524 | shared.plug = {}
525 | }
526 | const key: string = origin.url as string
527 | // ? 如果重复请求,
528 | if (shared.plug[key]) {
529 | throw new Error('lock')
530 | }
531 | shared.plug[key] = true
532 | return config
533 | },
534 | completed({ origin, shared }) {
535 | const key: string = origin.url as string
536 | delete shared.plug[key]
537 | }
538 | }
539 | }
540 | const request = axios.create({ baseURL: BASE_URL })
541 | useAxiosPlugin(request).plugin(plug)
542 | // req 1
543 | request.post('/case1').then(() => {
544 | // req 3
545 | expect(() => request.post('/case1')).not.toThrowError()
546 | })
547 | // req 2
548 | expect(() => request.post('/case1')).rejects.toThrowError('lock')
549 | })
550 |
551 | test('case - 累加、累减', async () => {
552 | const plug: IPlugin = {
553 | name: 'plug',
554 | lifecycle: {
555 | preRequestTransform(config, { shared }) {
556 | if (!shared['plug']) {
557 | shared.plug = 0
558 | }
559 | shared.plug++
560 | return config
561 | },
562 | completed({ shared }) {
563 | shared.plug--
564 | }
565 | }
566 | }
567 | const request = axios.create({ baseURL: BASE_URL })
568 | useAxiosPlugin(request).plugin(plug)
569 | await axios.all([
570 | //
571 | request.post('/case1'),
572 | request.post('/case1'),
573 | request.post('/case1')
574 | ])
575 | expect(request['__shared__'].plug).toBe(0)
576 | })
577 |
578 | test('case - 修改请求参数', async () => {
579 | const plug: IPlugin = {
580 | name: 'plug',
581 | lifecycle: {
582 | preRequestTransform(config) {
583 | // 修改请求参数使请求成功
584 | config.params = { a: 123 } as any
585 | return config
586 | }
587 | }
588 | }
589 | const request = axios.create({ baseURL: BASE_URL })
590 | useAxiosPlugin(request).plugin(plug)
591 | const res = await request.post('/case2')
592 | expect(res).toHaveProperty('data', { result: 'success' })
593 | })
594 | test('case - 修改响应结果', async () => {
595 | const plug: IPlugin = {
596 | name: 'plug',
597 | lifecycle: {
598 | postResponseTransform(res) {
599 | return {
600 | ...res,
601 | data: {
602 | replaced: true
603 | }
604 | }
605 | }
606 | }
607 | }
608 | const request = axios.create({ baseURL: BASE_URL })
609 | useAxiosPlugin(request).plugin(plug)
610 | const res = await request.post('/case1')
611 | expect(res.data).toEqual({ replaced: true })
612 | })
613 | test('case - 根据响应内容判断响应结果', async () => {
614 | const plug: IPlugin = {
615 | name: 'plug',
616 | lifecycle: {
617 | postResponseTransform(res) {
618 | if (res.data.result === 'failure') {
619 | throw new Error('请求出错')
620 | }
621 | return res
622 | }
623 | }
624 | }
625 | const request = axios.create({ baseURL: BASE_URL })
626 | useAxiosPlugin(request).plugin(plug)
627 | expect(request.post('/case3')).rejects.toThrow(new Error('请求出错'))
628 | })
629 | })
630 |
--------------------------------------------------------------------------------
/__tests__/utils/calc-hash.spec.ts:
--------------------------------------------------------------------------------
1 | import { AxiosRequestConfig } from 'axios'
2 | import { calcHash, defaultCalcRequestHash } from '../../src/utils/calc-hash'
3 |
4 | describe('`calcHash()`、`defaultCalcRequestHash()` 组合测试', () => {
5 | test('case - 验证包含冗余参数的hash值计算是否正确', () => {
6 | const config: AxiosRequestConfig = {
7 | url: 'https://www.example.com/api/user',
8 | method: 'get',
9 | params: { id: 1 },
10 | data: { name: 'John' },
11 | // headers 不参与 request hash 值计算
12 | headers: {
13 | 'Content-Type': 'appliaction/json'
14 | }
15 | }
16 | expect(defaultCalcRequestHash(config)).not.toBe(calcHash(config))
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/__tests__/utils/create-abort-chain.spec.ts:
--------------------------------------------------------------------------------
1 | import { createAbortChain } from '../../src/utils/create-abort-chain'
2 | import { delay } from '../../src/utils/delay'
3 |
4 | describe('测试 `create-abort-chain()`', () => {
5 | test('case - 钩子被正确触发, 并返回了正确结果', async () => {
6 | const onNext = jest.fn(() => {
7 | return {}
8 | })
9 | const onCapture = jest.fn(() => {
10 | return {}
11 | })
12 | const onCompleted = jest.fn()
13 | const result = await createAbortChain()
14 | .next(onNext)
15 | .next(onNext)
16 | .next(() => 10)
17 | .capture(onCapture)
18 | .completed(onCompleted)
19 | .done()
20 | expect(result).toBe(10)
21 | expect(onNext).toBeCalledTimes(2)
22 | expect(onCompleted).toBeCalledTimes(1)
23 | expect(onCapture).toBeCalledTimes(0)
24 | })
25 |
26 | test('case - 应执行所有回调,并在所有回调成功时返回结果', async () => {
27 | const res = await createAbortChain(1)
28 | .next((res) => res + 1)
29 | .next((res) => res * 2)
30 | .next(async (res) => {
31 | await delay(100)
32 | return res * 10
33 | })
34 | .done()
35 | expect(res).toBe(40)
36 | })
37 |
38 | test('case - 未添加 `onCapture`时, 应直接抛出异常', async () => {
39 | expect(() =>
40 | createAbortChain(1)
41 | .next((res) => res + 1)
42 | .next(() => {
43 | throw new Error('error')
44 | })
45 | .next(async (res) => {
46 | await delay(100)
47 | return res * 10
48 | })
49 | .done()
50 | ).rejects.toThrowError('error')
51 | })
52 |
53 | test('case - 不能重复添加 `onCapture`', async () => {
54 | expect(() =>
55 | createAbortChain(1)
56 | .next((res) => res + 1)
57 | .capture((e) => {})
58 | .capture((e) => {})
59 | .done()
60 | ).toThrowError('`onCapture` is registered')
61 | })
62 |
63 | test('case - 不能重复添加 `onCompleted`', async () => {
64 | expect(() =>
65 | createAbortChain(1)
66 | .next((res) => res + 1)
67 | .completed(() => {})
68 | .completed(() => {})
69 | .done()
70 | ).toThrowError('`onCompleted` is registered')
71 | })
72 | test('case - `capture` 钩子正确捕获到异常结果', async () => {
73 | expect(
74 | createAbortChain(1)
75 | .next(() => {
76 | throw new Error('error')
77 | })
78 | .next(() => {
79 | return 2
80 | })
81 | .capture((reason) => {
82 | expect(reason).toEqual(new Error('error'))
83 | return 8
84 | })
85 | .done()
86 | ).resolves.toEqual(8)
87 |
88 | expect(
89 | createAbortChain(1)
90 | .next(() => {
91 | throw new Error('error')
92 | })
93 | .capture((reason) => {
94 | throw reason
95 | })
96 | .done()
97 | ).rejects.toThrowError('error')
98 | })
99 |
100 | test('case - 在调用`abort`时, 应立即停止执行, 并返回结果', async () => {
101 | let fn = jest.fn()
102 | const result = await createAbortChain(1)
103 | .next((res) => {
104 | return res + 1
105 | })
106 | .next((_, { abort }) => {
107 | abort(10)
108 | return fn()
109 | })
110 | .next((res) => res + 1)
111 | .done()
112 | expect(result).toEqual(10)
113 | expect(fn).not.toBeCalled()
114 | })
115 |
116 | test('case - 在调用`abortError`时, 应立即停止执行, 并抛出异常', async () => {
117 | expect(() =>
118 | createAbortChain(1)
119 | .next((res) => {
120 | return res + 1
121 | })
122 | .next((res, { abortError }) => {
123 | abortError(new Error('error'))
124 | return res + 1
125 | })
126 | .next((res) => res + 1)
127 | .done()
128 | ).rejects.toThrowError('error')
129 | })
130 |
131 | test('case - 在调用`abortError`时, 允许抛出任意类型值', async () => {
132 | expect(() =>
133 | createAbortChain(1)
134 | .next((res) => {
135 | return res + 1
136 | })
137 | .next((res, { abortError }) => {
138 | abortError('error is string')
139 | return res + 1
140 | })
141 | .done()
142 | ).rejects.toBe('error is string')
143 | })
144 |
145 | test('case - `abort`,`abortError` 触发时, 不触发后续钩子', async () => {
146 | const onNext = jest.fn(() => {
147 | return {}
148 | })
149 | const onCapture = jest.fn(() => {
150 | return {}
151 | })
152 | expect(() =>
153 | createAbortChain(1)
154 | .next((res) => {
155 | return res + 1
156 | })
157 | .next((res, { abortError }) => {
158 | abortError('error is string')
159 | return res + 1
160 | })
161 | .done()
162 | ).rejects.toBe('error is string')
163 | expect(onNext).not.toBeCalled()
164 | expect(onCapture).not.toBeCalled()
165 | })
166 |
167 | test('case - 在调用`slient`时, 应立即中止执行, 并返回一个未被执行的 Promise', async () => {
168 | const res: Promise = createAbortChain(1)
169 | .next((res) => {
170 | return res + 1
171 | })
172 | .next((res, { slient }) => {
173 | slient()
174 | return res + 1
175 | })
176 | .next((res) => res + 1)
177 | .done()
178 | expect(res instanceof Promise).toBe(true)
179 | jest.useFakeTimers()
180 | jest.advanceTimersByTime(5000) // advance 5 seconds, make sure the promise is unresolved
181 | expect(res).resolves.toBeUndefined()
182 | })
183 | })
184 |
--------------------------------------------------------------------------------
/__tests__/utils/create-cache.spec.ts:
--------------------------------------------------------------------------------
1 | import { createOrGetCache } from '../../src/utils/create-cache'
2 | import { ISharedCache } from '../../src/intf'
3 |
4 | describe('测试 `createOrGetCache()`', () => {
5 | test('case 1 - 当首次执行 createOrGetCache() 时, 返回空对象', () => {
6 | const shared: ISharedCache = {}
7 | const prop = 'test'
8 | expect(createOrGetCache(shared, prop)).toEqual({})
9 | })
10 |
11 | test('case 2 - 当指明 initial 值时, 返回 initial', () => {
12 | const shared: ISharedCache = {}
13 | const prop = 'test'
14 | const initial = { a: 123 }
15 | expect(createOrGetCache(shared, prop, initial)).toEqual(initial)
16 | })
17 |
18 | test('case 3 - 当第二次触发 createOrGetCache() 时, 返回已初始化后的对象', () => {
19 | const shared: ISharedCache = {}
20 | const prop = 'test'
21 | const initial = { a: 123 }
22 | expect(createOrGetCache(shared, prop, initial)).toEqual(initial)
23 | expect(createOrGetCache(shared, prop)).toEqual(initial)
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/__tests__/utils/create-filter.spec.ts:
--------------------------------------------------------------------------------
1 | import { FilterPattern, createUrlFilter } from '../../src/utils/create-filter'
2 |
3 | describe('测试 `createUrlFilter()`', () => {
4 | const testUrl = 'https://www.example.com/api/user/100'
5 |
6 | test('case - 无过滤条件返回 true', () => {
7 | const filter1 = createUrlFilter()
8 | const filter2 = createUrlFilter(null, null)
9 | const filter3 = createUrlFilter(undefined, undefined)
10 | expect(filter1(testUrl)).toBeTruthy()
11 | expect(filter2(testUrl)).toBeFalsy()
12 | expect(filter3(testUrl)).toBeTruthy()
13 | })
14 |
15 | test('case - `includes` 条件匹配时, 返回true', () => {
16 | // 不同条件类型
17 | const includes1: FilterPattern = 'api'
18 | const includes2: FilterPattern = /api/
19 | const includes3: FilterPattern = ['api']
20 | const includes4: FilterPattern = [(path) => path.includes('api')]
21 | const includes5: FilterPattern = [true] // 忽略校验, 直接返回成功.
22 | // 多条件
23 | const includes6: FilterPattern = ['/api', '/xxxxxx']
24 | const includes7: FilterPattern = ['/xxxxxx', /api/]
25 | const filter1 = createUrlFilter(includes1)
26 | const filter2 = createUrlFilter(includes2)
27 | const filter3 = createUrlFilter(includes3)
28 | const filter4 = createUrlFilter(includes4)
29 | const filter5 = createUrlFilter(includes5)
30 | const filter6 = createUrlFilter(includes6)
31 | const filter7 = createUrlFilter(includes7)
32 | expect(filter1(testUrl)).toBeTruthy()
33 | expect(filter2(testUrl)).toBeTruthy()
34 | expect(filter3(testUrl)).toBeTruthy()
35 | expect(filter4(testUrl)).toBeTruthy()
36 | expect(filter5(testUrl)).toBeTruthy()
37 | expect(filter6(testUrl)).toBeTruthy()
38 | expect(filter7(testUrl)).toBeTruthy()
39 | })
40 |
41 | test('case - `excludes` 条件匹配时, 返回 false', () => {
42 | // 不同条件类型
43 | const excludes1: FilterPattern = 'api'
44 | const excludes2: FilterPattern = /api/
45 | const excludes3: FilterPattern = ['api']
46 | // 多条件
47 | const excludes4: FilterPattern = ['/api', '/xxxxxx']
48 | const excludes5: FilterPattern = ['/xxxxxx', /api/]
49 | const excludes6: FilterPattern = [(path) => path.includes('api')]
50 | const filter1 = createUrlFilter(undefined, excludes1)
51 | const filter2 = createUrlFilter(undefined, excludes2)
52 | const filter3 = createUrlFilter(undefined, excludes3)
53 | const filter4 = createUrlFilter(undefined, excludes4)
54 | const filter5 = createUrlFilter(undefined, excludes5)
55 | const filter6 = createUrlFilter(undefined, excludes6)
56 | expect(filter1(testUrl)).toBeFalsy()
57 | expect(filter2(testUrl)).toBeFalsy()
58 | expect(filter3(testUrl)).toBeFalsy()
59 | expect(filter4(testUrl)).toBeFalsy()
60 | expect(filter5(testUrl)).toBeFalsy()
61 | expect(filter6(testUrl)).toBeFalsy()
62 | })
63 |
64 | test('case - 当 `includes` 匹配, `excludes` 不匹配时, 返回 true', () => {
65 | const includes: FilterPattern = '/api'
66 | const excludes: FilterPattern = '/test'
67 | const filter = createUrlFilter(includes, excludes)
68 | expect(filter(testUrl)).toBeTruthy()
69 | })
70 |
71 | test('case - 当 `includes` 不匹配, `excludes` 匹配时, 返回 false', () => {
72 | const includes: FilterPattern = '/test'
73 | const excludes: FilterPattern = '/api'
74 | const filter = createUrlFilter(includes, excludes)
75 | expect(filter(testUrl)).toBeFalsy()
76 | })
77 |
78 | test('case - 当 `includes`、`excludes` 同时匹配时, 返回 false', () => {
79 | const includes: FilterPattern = '/api'
80 | const excludes: FilterPattern = '/api'
81 | const filter = createUrlFilter(includes, excludes)
82 | expect(filter(testUrl)).toBeFalsy()
83 | })
84 |
85 | test('case - 当 `includes` 存在条件时, 只要include不匹配, 就返回 false', () => {
86 | const includes: FilterPattern = '/test'
87 | const excludes: FilterPattern = '/test'
88 | const filter = createUrlFilter(includes, excludes)
89 | expect(filter(testUrl)).toBeFalsy()
90 | })
91 |
92 | test('case - 当 `includes`, `excludes` 规则不满足 FilterPattern 时, 抛出类型异常', () => {
93 | const includes = 123
94 | expect(() => createUrlFilter(includes as any)).toThrow(TypeError)
95 | })
96 | })
97 |
--------------------------------------------------------------------------------
/__tests__/utils/delay.spec.ts:
--------------------------------------------------------------------------------
1 | import { delay, getDelayTime } from '../../src/utils/delay'
2 |
3 | describe('测试 `delay()`', () => {
4 | const expectedDelayTimeSpan: number = 100 // 允许的误差范围 (jest 多任务并行时, 阻塞可能产生问题)
5 | test('case - 如果不传递延时时间,则默认延时时间为 0', async () => {
6 | const start = Date.now()
7 | await delay()
8 | const end = Date.now()
9 | expect(end - start).toBeLessThan(expectedDelayTimeSpan)
10 | })
11 | test('case - 测试延迟时间是否正确', async () => {
12 | const startTime = Date.now()
13 | const delayTime = 500
14 | await delay(delayTime)
15 | const endTime = Date.now()
16 | expect(endTime - startTime).toBeGreaterThan(delayTime - expectedDelayTimeSpan)
17 | expect(endTime - startTime).toBeLessThan(delayTime + expectedDelayTimeSpan)
18 | })
19 |
20 | test('case - 测试Promise是否正确resolve', async () => {
21 | const delayTime = 100
22 | const result = await delay(delayTime)
23 | expect(result).toBe(undefined)
24 | })
25 | })
26 |
27 | describe('测试 `getDelayTime()`', () => {
28 | test('case - 当没有传入任何参数时,应该返回默认的延时时间', () => {
29 | expect(getDelayTime(100)).toBe(100)
30 | })
31 |
32 | test('case - 当传入一个数字参数时,应该返回该数字', () => {
33 | expect(getDelayTime(100, 200)).toBe(200)
34 | })
35 |
36 | test('case - 当传入一个对象参数,且对象中存在 delay 属性时,应该返回 delay 属性值', () => {
37 | expect(getDelayTime(100, { delay: 300 })).toBe(300)
38 | })
39 |
40 | test('case - 当传入多个参数时,只返回第一个符合条件的参数值', () => {
41 | expect(getDelayTime(100, { foo: 'bar' }, 400, { delay: 500 })).toBe(400)
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/__tests__/utils/url.spec.ts:
--------------------------------------------------------------------------------
1 | import { isAbsoluteURL, combineURLs } from '../../src/utils/url'
2 |
3 | // 以下几种异常情况将忽略
4 | // 1. 不详细验证参数类型, 参数类型通过 typescript 类型校验
5 | describe('测试 `isAbsoluteURL()`', () => {
6 | test('case - 如果url包含schema协议, 返回 true', () => {
7 | const url = 'https://www.example.com'
8 | expect(isAbsoluteURL(url)).toBeTruthy()
9 | })
10 |
11 | test('case - 如果url包含schema协议, 但没有具体声明, 返回 true', () => {
12 | const url = '//www.example.com'
13 | expect(isAbsoluteURL(url)).toBeTruthy()
14 | })
15 |
16 | test('case - 如果是相对路径, 返回 false', () => {
17 | const url = '/api/user'
18 | expect(isAbsoluteURL(url)).toBe(false)
19 | })
20 | })
21 |
22 | // 以下几种异常情况将忽略
23 | // 1. 不详细验证参数类型, 参数类型通过 typescript 类型校验
24 | // 2. 不校验url结构
25 | describe('测试 `combineURLs()`', () => {
26 | test('case - 返回拼接后的url', () => {
27 | const baseURL = 'https://www.example.com'
28 | const relativeURL = '/api/user'
29 | expect(combineURLs(baseURL, relativeURL)).toBe('https://www.example.com/api/user')
30 | })
31 |
32 | test('case - 应该移除重复的 `/`', () => {
33 | const baseURL = 'https://www.example.com/'
34 | const relativeURL = '/api/user'
35 | expect(combineURLs(baseURL, relativeURL)).toBe('https://www.example.com/api/user')
36 | })
37 |
38 | test('case - 如果 `relativeURL` 值为空, 那么应当返回 baseURL', () => {
39 | const baseURL = 'https://www.example.com'
40 | expect(combineURLs(baseURL, null as any)).toBe(baseURL)
41 | expect(combineURLs(baseURL, undefined as any)).toBe(baseURL)
42 | })
43 |
44 | test('case - 如果 `baseURL` 值为空, 那么应当抛出异常', () => {
45 | const relativeURL = '/api/user'
46 | expect(() => combineURLs(null as any, relativeURL)).toThrow(TypeError)
47 | expect(() => combineURLs(undefined as any, relativeURL)).toThrow(TypeError)
48 | })
49 | })
50 |
--------------------------------------------------------------------------------
/build.config.ts:
--------------------------------------------------------------------------------
1 | import { defineBuildConfig } from 'unbuild'
2 | import * as np from 'node:path'
3 | import { glob } from 'glob'
4 |
5 | import pkg from './package.json'
6 |
7 | const plugins = glob.sync('./src/plugins/*.ts').map((input: string) => {
8 | return { name: 'plugins/' + np.basename(input, '.ts'), input: input.replace(/\.ts$/, '') }
9 | })
10 |
11 | export default defineBuildConfig({
12 | entries: [{ name: 'index', input: 'src/index' }, { name: 'core', input: 'src/core' }, ...plugins],
13 | clean: true,
14 | declaration: true,
15 | rollup: {
16 | emitCJS: true,
17 | output: {
18 | banner: `
19 | // @ts-nocheck
20 | /**
21 | * ${pkg.name}@${pkg.version}
22 | *
23 | * Copyright (c) ${new Date().getFullYear()} ${pkg.author.name} <${pkg.author.url}>
24 | * Released under ${pkg.license} License
25 | *
26 | * @build ${new Date()}
27 | * @author ${pkg.author.name}(${pkg.author.url})
28 | * @license ${pkg.license}
29 | */
30 | `
31 | .trim()
32 | .split(/\n/g)
33 | .map((s) => s.trim())
34 | .join('\n')
35 | }
36 | }
37 | })
38 |
--------------------------------------------------------------------------------
/dist/core.cjs:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | /**
3 | * axios-plugins@0.5.4
4 | *
5 | * Copyright (c) 2024 halo951
6 | * Released under MIT License
7 | *
8 | * @build Wed Aug 28 2024 19:53:48 GMT+0800 (中国标准时间)
9 | * @author halo951(https://github.com/halo951)
10 | * @license MIT
11 | */
12 | 'use strict';
13 |
14 | const axios = require('axios');
15 | const json = require('klona/json');
16 |
17 | var __defProp$1 = Object.defineProperty;
18 | var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
19 | var __publicField$1 = (obj, key, value) => {
20 | __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
21 | return value;
22 | };
23 | class AbortError extends Error {
24 | constructor(abort) {
25 | super();
26 | __publicField$1(this, "type", "abort");
27 | __publicField$1(this, "abort");
28 | this.abort = abort;
29 | }
30 | }
31 | class SlientError extends Error {
32 | constructor() {
33 | super(...arguments);
34 | __publicField$1(this, "type", "slient");
35 | }
36 | }
37 | const createAbortChain = (initial) => {
38 | const chain = [];
39 | const controller = {
40 | abort(res2) {
41 | throw new AbortError({ success: true, res: res2 });
42 | },
43 | abortError(reason) {
44 | throw new AbortError({ success: false, res: reason });
45 | },
46 | slient() {
47 | throw new SlientError();
48 | }
49 | };
50 | let onCapture;
51 | let onCompleted;
52 | let onAbort;
53 | let res = initial;
54 | return {
55 | /** 下一任务 */
56 | next(event) {
57 | chain.push(event);
58 | return this;
59 | },
60 | /** 捕获异常后触发 */
61 | capture(event) {
62 | if (onCapture) {
63 | throw new Error("`onCapture` is registered");
64 | }
65 | onCapture = event;
66 | return this;
67 | },
68 | /** 执行完成后触发 */
69 | completed(event) {
70 | if (onCompleted) {
71 | throw new Error("`onCompleted` is registered");
72 | }
73 | onCompleted = event;
74 | return this;
75 | },
76 | /**
77 | * 执行中断后触发
78 | *
79 | * @description 增加 `abort` 以解决触发 `Abort` 后造成的后续请求阻塞. (主要体现在: merge 插件)
80 | */
81 | abort(event) {
82 | if (onAbort) {
83 | throw new Error("`onAbort` is registered");
84 | }
85 | onAbort = event;
86 | return this;
87 | },
88 | /** 停止添加并执行 */
89 | async done() {
90 | const run = async () => {
91 | let abortRes;
92 | try {
93 | for (const task of chain) {
94 | res = await task(res, controller);
95 | }
96 | return res;
97 | } catch (reason) {
98 | if (reason instanceof AbortError) {
99 | abortRes = reason;
100 | }
101 | if (onCapture && !(reason instanceof AbortError || reason instanceof SlientError)) {
102 | return await onCapture(reason, controller);
103 | } else {
104 | throw reason;
105 | }
106 | } finally {
107 | if (onCompleted)
108 | await onCompleted(controller);
109 | if (!!abortRes && onAbort)
110 | await onAbort(abortRes);
111 | }
112 | };
113 | try {
114 | return await run();
115 | } catch (reason) {
116 | if (reason instanceof AbortError) {
117 | if (reason.abort.success) {
118 | return reason.abort.res;
119 | } else {
120 | throw reason.abort.res;
121 | }
122 | } else if (reason instanceof SlientError) {
123 | return new Promise(() => {
124 | });
125 | } else {
126 | throw reason;
127 | }
128 | }
129 | }
130 | };
131 | };
132 |
133 | var __defProp = Object.defineProperty;
134 | var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
135 | var __publicField = (obj, key, value) => {
136 | __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
137 | return value;
138 | };
139 | class AxiosExtension extends axios.Axios {
140 | constructor(config, interceptors) {
141 | super(config);
142 | /** 添加的插件集合 */
143 | __publicField(this, "__plugins__", []);
144 | /** 插件共享内存空间 */
145 | __publicField(this, "__shared__", {});
146 | this.interceptors = interceptors;
147 | const originRequest = this.request;
148 | const vm = this;
149 | const getHook = (hookName) => {
150 | return this.__plugins__.map((plug) => {
151 | const hook = plug.lifecycle?.[hookName];
152 | if (typeof hook === "function") {
153 | return {
154 | runWhen: () => true,
155 | handler: hook
156 | };
157 | } else if (hook) {
158 | return hook;
159 | }
160 | }).filter((hook) => !!hook);
161 | };
162 | const hasHook = (hookName) => {
163 | return getHook(hookName).length > 0;
164 | };
165 | const runHook = async (hookName, reverse, arg1, arg2, arg3) => {
166 | let hooks = reverse ? getHook(hookName).reverse() : getHook(hookName);
167 | for (const hook of hooks) {
168 | if (hook.runWhen.call(hook.runWhen, arg1, arg2)) {
169 | arg1 = await hook.handler.call(
170 | hook,
171 | ...arg2 ? [arg1, arg2, arg3] : [arg1, arg3]
172 | );
173 | }
174 | }
175 | return arg1;
176 | };
177 | this.request = async function(config2) {
178 | const origin = json.klona(config2);
179 | const share = { origin, shared: this.__shared__, axios: vm };
180 | return await createAbortChain(config2).next((config3, controller) => runHook("preRequestTransform", false, config3, share, controller)).next(async (config3) => await originRequest.call(vm, config3)).next((response, controller) => runHook("postResponseTransform", true, response, share, controller)).capture(async (e, controller) => {
181 | if (hasHook("captureException")) {
182 | return await runHook("captureException", true, e, share, controller);
183 | } else {
184 | throw e;
185 | }
186 | }).completed(
187 | (controller) => runHook("completed", true, share, void 0, controller)
188 | ).abort((reason) => runHook("aborted", true, reason, share, void 0)).done();
189 | };
190 | this.interceptors.request.use((config2) => {
191 | return runHook("transformRequest", false, config2, this.__shared__, {
192 | abort(res) {
193 | throw new AbortError({ success: true, res });
194 | },
195 | abortError(reason) {
196 | throw new AbortError({ success: false, res: reason });
197 | },
198 | slient() {
199 | throw new SlientError();
200 | }
201 | });
202 | });
203 | }
204 | }
205 | const IGNORE_COVERAGE = ["prototype"];
206 | const injectPluginHooks = (axios) => {
207 | if (axios["__plugins__"]) {
208 | return;
209 | }
210 | const extension = new AxiosExtension(axios.defaults, axios.interceptors);
211 | const properties = Object.getOwnPropertyNames(axios).concat(["__shared__", "__plugins__"]).filter((prop) => extension[prop] && !IGNORE_COVERAGE.includes(prop)).reduce((properties2, prop) => {
212 | properties2[prop] = {
213 | get() {
214 | return extension[prop];
215 | },
216 | set(v) {
217 | extension[prop] = v;
218 | }
219 | };
220 | return properties2;
221 | }, {});
222 | Object.defineProperties(axios, properties);
223 | };
224 | const injectPlugin = (axios, plug) => {
225 | const soryByEnforce = (plugins) => {
226 | return plugins.sort((a, b) => {
227 | if (a.enforce === "pre" || b.enforce === "post") {
228 | return -1;
229 | } else if (a.enforce === "post" || b.enforce === "pre") {
230 | return 1;
231 | } else {
232 | return 0;
233 | }
234 | });
235 | };
236 | if (!plug.lifecycle)
237 | plug.lifecycle = {};
238 | if (axios.__plugins__) {
239 | axios.__plugins__.push(plug);
240 | axios.__plugins__ = soryByEnforce(axios.__plugins__);
241 | }
242 | };
243 | const useAxiosPlugin = (axios) => {
244 | injectPluginHooks(axios);
245 | return {
246 | /** 添加新插件 */
247 | plugin(plug) {
248 | plug.beforeRegister?.(axios);
249 | injectPlugin(axios, plug);
250 | return this;
251 | },
252 | /**
253 | * 包装 `axios({ ... })`
254 | *
255 | * @description 使 `axiox({ ... })` 具备插件能力
256 | */
257 | wrap() {
258 | return new Proxy(axios, {
259 | apply(_target, _thisArg, args) {
260 | return axios.request.call(axios, args[0]);
261 | }
262 | });
263 | }
264 | };
265 | };
266 |
267 | exports.useAxiosPlugin = useAxiosPlugin;
268 |
--------------------------------------------------------------------------------
/dist/core.d.cts:
--------------------------------------------------------------------------------
1 | import { c as IUseAxiosPluginResult } from './shared/axios-plugins.0db5f57e.cjs';
2 | export { A as AxiosInstanceExtension, I as IPlugin } from './shared/axios-plugins.0db5f57e.cjs';
3 | import { AxiosInstance } from 'axios';
4 |
5 | /**
6 | * 使用 axios 扩展插件
7 | *
8 | * @description 通过链式调用方式, 为 `axios` 扩展插件支持.
9 | */
10 | declare const useAxiosPlugin: (axios: AxiosInstance) => IUseAxiosPluginResult;
11 |
12 | export { useAxiosPlugin };
13 |
--------------------------------------------------------------------------------
/dist/core.d.mts:
--------------------------------------------------------------------------------
1 | import { c as IUseAxiosPluginResult } from './shared/axios-plugins.0db5f57e.mjs';
2 | export { A as AxiosInstanceExtension, I as IPlugin } from './shared/axios-plugins.0db5f57e.mjs';
3 | import { AxiosInstance } from 'axios';
4 |
5 | /**
6 | * 使用 axios 扩展插件
7 | *
8 | * @description 通过链式调用方式, 为 `axios` 扩展插件支持.
9 | */
10 | declare const useAxiosPlugin: (axios: AxiosInstance) => IUseAxiosPluginResult;
11 |
12 | export { useAxiosPlugin };
13 |
--------------------------------------------------------------------------------
/dist/core.d.ts:
--------------------------------------------------------------------------------
1 | import { c as IUseAxiosPluginResult } from './shared/axios-plugins.0db5f57e.js';
2 | export { A as AxiosInstanceExtension, I as IPlugin } from './shared/axios-plugins.0db5f57e.js';
3 | import { AxiosInstance } from 'axios';
4 |
5 | /**
6 | * 使用 axios 扩展插件
7 | *
8 | * @description 通过链式调用方式, 为 `axios` 扩展插件支持.
9 | */
10 | declare const useAxiosPlugin: (axios: AxiosInstance) => IUseAxiosPluginResult;
11 |
12 | export { useAxiosPlugin };
13 |
--------------------------------------------------------------------------------
/dist/core.mjs:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | /**
3 | * axios-plugins@0.5.4
4 | *
5 | * Copyright (c) 2024 halo951
6 | * Released under MIT License
7 | *
8 | * @build Wed Aug 28 2024 19:53:48 GMT+0800 (中国标准时间)
9 | * @author halo951(https://github.com/halo951)
10 | * @license MIT
11 | */
12 | import { Axios } from 'axios';
13 | import { klona } from 'klona/json';
14 |
15 | var __defProp$1 = Object.defineProperty;
16 | var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
17 | var __publicField$1 = (obj, key, value) => {
18 | __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
19 | return value;
20 | };
21 | class AbortError extends Error {
22 | constructor(abort) {
23 | super();
24 | __publicField$1(this, "type", "abort");
25 | __publicField$1(this, "abort");
26 | this.abort = abort;
27 | }
28 | }
29 | class SlientError extends Error {
30 | constructor() {
31 | super(...arguments);
32 | __publicField$1(this, "type", "slient");
33 | }
34 | }
35 | const createAbortChain = (initial) => {
36 | const chain = [];
37 | const controller = {
38 | abort(res2) {
39 | throw new AbortError({ success: true, res: res2 });
40 | },
41 | abortError(reason) {
42 | throw new AbortError({ success: false, res: reason });
43 | },
44 | slient() {
45 | throw new SlientError();
46 | }
47 | };
48 | let onCapture;
49 | let onCompleted;
50 | let onAbort;
51 | let res = initial;
52 | return {
53 | /** 下一任务 */
54 | next(event) {
55 | chain.push(event);
56 | return this;
57 | },
58 | /** 捕获异常后触发 */
59 | capture(event) {
60 | if (onCapture) {
61 | throw new Error("`onCapture` is registered");
62 | }
63 | onCapture = event;
64 | return this;
65 | },
66 | /** 执行完成后触发 */
67 | completed(event) {
68 | if (onCompleted) {
69 | throw new Error("`onCompleted` is registered");
70 | }
71 | onCompleted = event;
72 | return this;
73 | },
74 | /**
75 | * 执行中断后触发
76 | *
77 | * @description 增加 `abort` 以解决触发 `Abort` 后造成的后续请求阻塞. (主要体现在: merge 插件)
78 | */
79 | abort(event) {
80 | if (onAbort) {
81 | throw new Error("`onAbort` is registered");
82 | }
83 | onAbort = event;
84 | return this;
85 | },
86 | /** 停止添加并执行 */
87 | async done() {
88 | const run = async () => {
89 | let abortRes;
90 | try {
91 | for (const task of chain) {
92 | res = await task(res, controller);
93 | }
94 | return res;
95 | } catch (reason) {
96 | if (reason instanceof AbortError) {
97 | abortRes = reason;
98 | }
99 | if (onCapture && !(reason instanceof AbortError || reason instanceof SlientError)) {
100 | return await onCapture(reason, controller);
101 | } else {
102 | throw reason;
103 | }
104 | } finally {
105 | if (onCompleted)
106 | await onCompleted(controller);
107 | if (!!abortRes && onAbort)
108 | await onAbort(abortRes);
109 | }
110 | };
111 | try {
112 | return await run();
113 | } catch (reason) {
114 | if (reason instanceof AbortError) {
115 | if (reason.abort.success) {
116 | return reason.abort.res;
117 | } else {
118 | throw reason.abort.res;
119 | }
120 | } else if (reason instanceof SlientError) {
121 | return new Promise(() => {
122 | });
123 | } else {
124 | throw reason;
125 | }
126 | }
127 | }
128 | };
129 | };
130 |
131 | var __defProp = Object.defineProperty;
132 | var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
133 | var __publicField = (obj, key, value) => {
134 | __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
135 | return value;
136 | };
137 | class AxiosExtension extends Axios {
138 | constructor(config, interceptors) {
139 | super(config);
140 | /** 添加的插件集合 */
141 | __publicField(this, "__plugins__", []);
142 | /** 插件共享内存空间 */
143 | __publicField(this, "__shared__", {});
144 | this.interceptors = interceptors;
145 | const originRequest = this.request;
146 | const vm = this;
147 | const getHook = (hookName) => {
148 | return this.__plugins__.map((plug) => {
149 | const hook = plug.lifecycle?.[hookName];
150 | if (typeof hook === "function") {
151 | return {
152 | runWhen: () => true,
153 | handler: hook
154 | };
155 | } else if (hook) {
156 | return hook;
157 | }
158 | }).filter((hook) => !!hook);
159 | };
160 | const hasHook = (hookName) => {
161 | return getHook(hookName).length > 0;
162 | };
163 | const runHook = async (hookName, reverse, arg1, arg2, arg3) => {
164 | let hooks = reverse ? getHook(hookName).reverse() : getHook(hookName);
165 | for (const hook of hooks) {
166 | if (hook.runWhen.call(hook.runWhen, arg1, arg2)) {
167 | arg1 = await hook.handler.call(
168 | hook,
169 | ...arg2 ? [arg1, arg2, arg3] : [arg1, arg3]
170 | );
171 | }
172 | }
173 | return arg1;
174 | };
175 | this.request = async function(config2) {
176 | const origin = klona(config2);
177 | const share = { origin, shared: this.__shared__, axios: vm };
178 | return await createAbortChain(config2).next((config3, controller) => runHook("preRequestTransform", false, config3, share, controller)).next(async (config3) => await originRequest.call(vm, config3)).next((response, controller) => runHook("postResponseTransform", true, response, share, controller)).capture(async (e, controller) => {
179 | if (hasHook("captureException")) {
180 | return await runHook("captureException", true, e, share, controller);
181 | } else {
182 | throw e;
183 | }
184 | }).completed(
185 | (controller) => runHook("completed", true, share, void 0, controller)
186 | ).abort((reason) => runHook("aborted", true, reason, share, void 0)).done();
187 | };
188 | this.interceptors.request.use((config2) => {
189 | return runHook("transformRequest", false, config2, this.__shared__, {
190 | abort(res) {
191 | throw new AbortError({ success: true, res });
192 | },
193 | abortError(reason) {
194 | throw new AbortError({ success: false, res: reason });
195 | },
196 | slient() {
197 | throw new SlientError();
198 | }
199 | });
200 | });
201 | }
202 | }
203 | const IGNORE_COVERAGE = ["prototype"];
204 | const injectPluginHooks = (axios) => {
205 | if (axios["__plugins__"]) {
206 | return;
207 | }
208 | const extension = new AxiosExtension(axios.defaults, axios.interceptors);
209 | const properties = Object.getOwnPropertyNames(axios).concat(["__shared__", "__plugins__"]).filter((prop) => extension[prop] && !IGNORE_COVERAGE.includes(prop)).reduce((properties2, prop) => {
210 | properties2[prop] = {
211 | get() {
212 | return extension[prop];
213 | },
214 | set(v) {
215 | extension[prop] = v;
216 | }
217 | };
218 | return properties2;
219 | }, {});
220 | Object.defineProperties(axios, properties);
221 | };
222 | const injectPlugin = (axios, plug) => {
223 | const soryByEnforce = (plugins) => {
224 | return plugins.sort((a, b) => {
225 | if (a.enforce === "pre" || b.enforce === "post") {
226 | return -1;
227 | } else if (a.enforce === "post" || b.enforce === "pre") {
228 | return 1;
229 | } else {
230 | return 0;
231 | }
232 | });
233 | };
234 | if (!plug.lifecycle)
235 | plug.lifecycle = {};
236 | if (axios.__plugins__) {
237 | axios.__plugins__.push(plug);
238 | axios.__plugins__ = soryByEnforce(axios.__plugins__);
239 | }
240 | };
241 | const useAxiosPlugin = (axios) => {
242 | injectPluginHooks(axios);
243 | return {
244 | /** 添加新插件 */
245 | plugin(plug) {
246 | plug.beforeRegister?.(axios);
247 | injectPlugin(axios, plug);
248 | return this;
249 | },
250 | /**
251 | * 包装 `axios({ ... })`
252 | *
253 | * @description 使 `axiox({ ... })` 具备插件能力
254 | */
255 | wrap() {
256 | return new Proxy(axios, {
257 | apply(_target, _thisArg, args) {
258 | return axios.request.call(axios, args[0]);
259 | }
260 | });
261 | }
262 | };
263 | };
264 |
265 | export { useAxiosPlugin };
266 |
--------------------------------------------------------------------------------
/dist/index.cjs:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | /**
3 | * axios-plugins@0.5.4
4 | *
5 | * Copyright (c) 2024 halo951
6 | * Released under MIT License
7 | *
8 | * @build Wed Aug 28 2024 19:53:48 GMT+0800 (中国标准时间)
9 | * @author halo951(https://github.com/halo951)
10 | * @license MIT
11 | */
12 | 'use strict';
13 |
14 | const core = require('./core.cjs');
15 | const plugins_debounce = require('./shared/axios-plugins.25412762.cjs');
16 | const plugins_throttle = require('./shared/axios-plugins.1d7c5336.cjs');
17 | const plugins_merge = require('./shared/axios-plugins.15d3e343.cjs');
18 | const plugins_retry = require('./shared/axios-plugins.dcd32757.cjs');
19 | const plugins_cancel = require('./shared/axios-plugins.df986c3c.cjs');
20 | const plugins_cache = require('./shared/axios-plugins.1b09ab51.cjs');
21 | const plugins_transform = require('./shared/axios-plugins.68a6ca3a.cjs');
22 | const plugins_auth = require('./shared/axios-plugins.9a53e249.cjs');
23 | const plugins_loading = require('./shared/axios-plugins.e3a6432b.cjs');
24 | const plugins_mock = require('./shared/axios-plugins.59164b27.cjs');
25 | const plugins_envs = require('./plugins/envs.cjs');
26 | const plugins_normalize = require('./shared/axios-plugins.56283e30.cjs');
27 | const plugins_pathParams = require('./shared/axios-plugins.6b74785e.cjs');
28 | const plugins_sign = require('./shared/axios-plugins.9456f591.cjs');
29 | const plugins_sentryCapture = require('./plugins/sentry-capture.cjs');
30 | const plugins_onlySend = require('./shared/axios-plugins.db6a038e.cjs');
31 | const plugins_mp = require('./shared/axios-plugins.e3a62982.cjs');
32 | require('axios');
33 | require('klona/json');
34 | require('./shared/axios-plugins.b029b3bb.cjs');
35 | require('crypto-js');
36 | require('./shared/axios-plugins.4b4ce731.cjs');
37 | require('./shared/axios-plugins.41879bd5.cjs');
38 | require('./shared/axios-plugins.ef8ba07f.cjs');
39 | require('./shared/axios-plugins.d42d23f0.cjs');
40 | require('qs');
41 |
42 |
43 |
44 | exports.useAxiosPlugin = core.useAxiosPlugin;
45 | exports.Debounce = plugins_debounce.debounce$1;
46 | exports.debounce = plugins_debounce.debounce;
47 | exports.Throttle = plugins_throttle.throttle$1;
48 | exports.throttle = plugins_throttle.throttle;
49 | exports.Merge = plugins_merge.merge$1;
50 | exports.merge = plugins_merge.merge;
51 | exports.Retry = plugins_retry.retry$1;
52 | exports.retry = plugins_retry.retry;
53 | exports.Cancel = plugins_cancel.cancel$1;
54 | exports.cancel = plugins_cancel.cancel;
55 | exports.cancelAll = plugins_cancel.cancelAll;
56 | exports.Cache = plugins_cache.cache$1;
57 | exports.cache = plugins_cache.cache;
58 | exports.Transform = plugins_transform.transform$1;
59 | exports.transform = plugins_transform.transform;
60 | exports.Auth = plugins_auth.auth$1;
61 | exports.auth = plugins_auth.auth;
62 | exports.Loading = plugins_loading.loading$1;
63 | exports.loading = plugins_loading.loading;
64 | exports.Mock = plugins_mock.mock$1;
65 | exports.mock = plugins_mock.mock;
66 | exports.envs = plugins_envs.envs;
67 | exports.Normalize = plugins_normalize.normalize$1;
68 | exports.normalize = plugins_normalize.normalize;
69 | exports.PathParams = plugins_pathParams.pathParams$1;
70 | exports.pathParams = plugins_pathParams.pathParams;
71 | exports.Sign = plugins_sign.sign$1;
72 | exports.sign = plugins_sign.sign;
73 | exports.sentryCapture = plugins_sentryCapture.sentryCapture;
74 | exports.OnlySend = plugins_onlySend.onlySend$1;
75 | exports.onlySend = plugins_onlySend.onlySend;
76 | exports.Mp = plugins_mp.mp$1;
77 | exports.mp = plugins_mp.mp;
78 |
--------------------------------------------------------------------------------
/dist/index.d.cts:
--------------------------------------------------------------------------------
1 | export { A as AxiosInstanceExtension, I as IPlugin } from './shared/axios-plugins.0db5f57e.cjs';
2 | export { useAxiosPlugin } from './core.cjs';
3 | export { a as Debounce, d as debounce } from './shared/axios-plugins.ca2df126.cjs';
4 | export { a as Throttle, t as throttle } from './shared/axios-plugins.1dd22b9f.cjs';
5 | export { a as Merge, m as merge } from './shared/axios-plugins.43065fa6.cjs';
6 | export { a as Retry, r as retry } from './shared/axios-plugins.2e59735b.cjs';
7 | export { b as Cancel, c as cancel, a as cancelAll } from './shared/axios-plugins.ce645f55.cjs';
8 | export { a as Cache, c as cache } from './shared/axios-plugins.ea3b0987.cjs';
9 | export { a as Transform, t as transform } from './shared/axios-plugins.1e62daf2.cjs';
10 | export { b as Auth, a as auth } from './shared/axios-plugins.ea5ef64b.cjs';
11 | export { a as Loading, l as loading } from './shared/axios-plugins.57833d1a.cjs';
12 | export { a as Mock, m as mock } from './shared/axios-plugins.34a69f60.cjs';
13 | export { envs } from './plugins/envs.cjs';
14 | export { a as Normalize, n as normalize } from './shared/axios-plugins.8e24a823.cjs';
15 | export { a as PathParams, p as pathParams } from './shared/axios-plugins.b86d2240.cjs';
16 | export { a as Sign, s as sign } from './shared/axios-plugins.032ebe61.cjs';
17 | export { sentryCapture } from './plugins/sentry-capture.cjs';
18 | export { a as OnlySend, o as onlySend } from './shared/axios-plugins.db4955fd.cjs';
19 | export { a as Mp, m as mp } from './shared/axios-plugins.907d6daa.cjs';
20 | import 'axios';
21 | import './shared/axios-plugins.955421f8.cjs';
22 |
--------------------------------------------------------------------------------
/dist/index.d.mts:
--------------------------------------------------------------------------------
1 | export { A as AxiosInstanceExtension, I as IPlugin } from './shared/axios-plugins.0db5f57e.mjs';
2 | export { useAxiosPlugin } from './core.mjs';
3 | export { a as Debounce, d as debounce } from './shared/axios-plugins.0f0d9f78.mjs';
4 | export { a as Throttle, t as throttle } from './shared/axios-plugins.edf1a015.mjs';
5 | export { a as Merge, m as merge } from './shared/axios-plugins.1855fd56.mjs';
6 | export { a as Retry, r as retry } from './shared/axios-plugins.ef74ad77.mjs';
7 | export { b as Cancel, c as cancel, a as cancelAll } from './shared/axios-plugins.bbe14abb.mjs';
8 | export { a as Cache, c as cache } from './shared/axios-plugins.9911be7b.mjs';
9 | export { a as Transform, t as transform } from './shared/axios-plugins.85a510d2.mjs';
10 | export { b as Auth, a as auth } from './shared/axios-plugins.9f9835fe.mjs';
11 | export { a as Loading, l as loading } from './shared/axios-plugins.6bc45b41.mjs';
12 | export { a as Mock, m as mock } from './shared/axios-plugins.1500b863.mjs';
13 | export { envs } from './plugins/envs.mjs';
14 | export { a as Normalize, n as normalize } from './shared/axios-plugins.c0c3f987.mjs';
15 | export { a as PathParams, p as pathParams } from './shared/axios-plugins.d82149fd.mjs';
16 | export { a as Sign, s as sign } from './shared/axios-plugins.8010248b.mjs';
17 | export { sentryCapture } from './plugins/sentry-capture.mjs';
18 | export { a as OnlySend, o as onlySend } from './shared/axios-plugins.1e3635cd.mjs';
19 | export { a as Mp, m as mp } from './shared/axios-plugins.26b1dc2e.mjs';
20 | import 'axios';
21 | import './shared/axios-plugins.955421f8.mjs';
22 |
--------------------------------------------------------------------------------
/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | export { A as AxiosInstanceExtension, I as IPlugin } from './shared/axios-plugins.0db5f57e.js';
2 | export { useAxiosPlugin } from './core.js';
3 | export { a as Debounce, d as debounce } from './shared/axios-plugins.3116b8da.js';
4 | export { a as Throttle, t as throttle } from './shared/axios-plugins.9ccbb099.js';
5 | export { a as Merge, m as merge } from './shared/axios-plugins.762da222.js';
6 | export { a as Retry, r as retry } from './shared/axios-plugins.8ddbe688.js';
7 | export { b as Cancel, c as cancel, a as cancelAll } from './shared/axios-plugins.6d144eb0.js';
8 | export { a as Cache, c as cache } from './shared/axios-plugins.62a3b671.js';
9 | export { a as Transform, t as transform } from './shared/axios-plugins.f3c1c0fa.js';
10 | export { b as Auth, a as auth } from './shared/axios-plugins.61bd7d9c.js';
11 | export { a as Loading, l as loading } from './shared/axios-plugins.c81f3258.js';
12 | export { a as Mock, m as mock } from './shared/axios-plugins.8102dc9e.js';
13 | export { envs } from './plugins/envs.js';
14 | export { a as Normalize, n as normalize } from './shared/axios-plugins.883b97e4.js';
15 | export { a as PathParams, p as pathParams } from './shared/axios-plugins.6feaf018.js';
16 | export { a as Sign, s as sign } from './shared/axios-plugins.450f599e.js';
17 | export { sentryCapture } from './plugins/sentry-capture.js';
18 | export { a as OnlySend, o as onlySend } from './shared/axios-plugins.0f86db12.js';
19 | export { a as Mp, m as mp } from './shared/axios-plugins.2b9e7a77.js';
20 | import 'axios';
21 | import './shared/axios-plugins.955421f8.js';
22 |
--------------------------------------------------------------------------------
/dist/index.mjs:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | /**
3 | * axios-plugins@0.5.4
4 | *
5 | * Copyright (c) 2024 halo951
6 | * Released under MIT License
7 | *
8 | * @build Wed Aug 28 2024 19:53:48 GMT+0800 (中国标准时间)
9 | * @author halo951(https://github.com/halo951)
10 | * @license MIT
11 | */
12 | export { useAxiosPlugin } from './core.mjs';
13 | export { a as Debounce, d as debounce } from './shared/axios-plugins.3ad9a204.mjs';
14 | export { a as Throttle, t as throttle } from './shared/axios-plugins.7b3591ed.mjs';
15 | export { a as Merge, m as merge } from './shared/axios-plugins.5a9a7c57.mjs';
16 | export { a as Retry, r as retry } from './shared/axios-plugins.1c3e2ab7.mjs';
17 | export { b as Cancel, c as cancel, a as cancelAll } from './shared/axios-plugins.c0ffa6d4.mjs';
18 | export { a as Cache, c as cache } from './shared/axios-plugins.8c07b51c.mjs';
19 | export { a as Transform, t as transform } from './shared/axios-plugins.99d2571b.mjs';
20 | export { b as Auth, a as auth } from './shared/axios-plugins.072fe16b.mjs';
21 | export { a as Loading, l as loading } from './shared/axios-plugins.8ce607da.mjs';
22 | export { a as Mock, m as mock } from './shared/axios-plugins.96031bd7.mjs';
23 | export { envs } from './plugins/envs.mjs';
24 | export { a as Normalize, n as normalize } from './shared/axios-plugins.ff78bd97.mjs';
25 | export { a as PathParams, p as pathParams } from './shared/axios-plugins.78f209d9.mjs';
26 | export { a as Sign, s as sign } from './shared/axios-plugins.e900b52d.mjs';
27 | export { sentryCapture } from './plugins/sentry-capture.mjs';
28 | export { a as OnlySend, o as onlySend } from './shared/axios-plugins.b4beee06.mjs';
29 | export { a as Mp, m as mp } from './shared/axios-plugins.54b8227d.mjs';
30 | import 'axios';
31 | import 'klona/json';
32 | import './shared/axios-plugins.9604892d.mjs';
33 | import 'crypto-js';
34 | import './shared/axios-plugins.26950955.mjs';
35 | import './shared/axios-plugins.0bcbd33e.mjs';
36 | import './shared/axios-plugins.f6dd94df.mjs';
37 | import './shared/axios-plugins.d0f44f40.mjs';
38 | import 'qs';
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "axios-plugins",
3 | "version": "0.6.1-dev.1",
4 | "description": "用最小的侵入性, 为 axios 扩展更多的插件能力 (防抖、节流 等等)",
5 | "keywords": [
6 | "axios",
7 | "plugins"
8 | ],
9 | "license": "MIT",
10 | "author": {
11 | "name": "halo951",
12 | "url": "https://github.com/halo951"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/halo951/axios-plugins.git"
17 | },
18 | "bugs": {
19 | "url": "https://github.com/halo951/axios-plugins/issues"
20 | },
21 | "sideEffects": false,
22 | "files": [
23 | "src",
24 | "dist"
25 | ],
26 | "main": "dist/index.cjs",
27 | "module": "dist/index.mjs",
28 | "typings": "dist/index.d.ts",
29 | "exports": {
30 | ".": {
31 | "types": "./dist/index.d.ts",
32 | "require": "./dist/index.cjs",
33 | "import": "./dist/index.mjs"
34 | },
35 | "./core": {
36 | "types": "./dist/core.d.ts",
37 | "require": "./dist/core.cjs",
38 | "import": "./dist/core.mjs"
39 | },
40 | "./plugins/*": {
41 | "types": "./dist/plugins/*.d.ts",
42 | "require": "./dist/plugins/*.cjs",
43 | "import": "./dist/plugins/*.mjs"
44 | }
45 | },
46 | "typesVersions": {
47 | "*": {
48 | "core": [
49 | "dist/typings/core.d.ts"
50 | ],
51 | "plugins/*": [
52 | "src/plugins/*.ts"
53 | ],
54 | "utils/*": [
55 | "src/utils/*.ts"
56 | ]
57 | }
58 | },
59 | "scripts": {
60 | "clean": "rimraf dist && rimraf typings",
61 | "type-check": "vue-tsc --noEmit -p tsconfig.json --composite false",
62 | "build": "unbuild",
63 | "format": "prettier -w **.ts **.md **.json"
64 | },
65 | "peerDependencies": {
66 | "axios": "*"
67 | },
68 | "devDependencies": {
69 | "@types/crypto-js": "^4.2.2",
70 | "@types/node": "^20.14.9",
71 | "@types/qs": "^6.9.15",
72 | "axios": "^1.7.2",
73 | "nock": "^13.5.4",
74 | "typescript": "^5.4.5"
75 | },
76 | "dependencies": {
77 | "crypto-js": "^4.2.0",
78 | "glob": "^10.4.2",
79 | "klona": "^2.0.6",
80 | "qs": "^6.12.1",
81 | "unbuild": "^2.0.0"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/prettier.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "printWidth": 120,
5 | "tabWidth": 4,
6 | "trailingComma": "none"
7 | }
8 |
--------------------------------------------------------------------------------
/src/core.ts:
--------------------------------------------------------------------------------
1 | export type { IPlugin, AxiosInstanceExtension } from './intf'
2 |
3 | export { useAxiosPlugin } from './use-plugin'
4 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export type { IPlugin, AxiosInstanceExtension } from './intf'
2 |
3 | export { useAxiosPlugin } from './use-plugin'
4 |
5 | /** 请求过程: 防抖 */
6 | export { debounce } from './plugins/debounce'
7 | /** 请求过程: 节流 */
8 | export { throttle } from './plugins/throttle'
9 | /** 请求过程: 重复请求合并 */
10 | export { merge } from './plugins/merge'
11 | /** 请求过程: 失败重试 */
12 | export { retry } from './plugins/retry'
13 | /** 请求过程: 取消(中断)请求 */
14 | export { cancel, cancelAll } from './plugins/cancel'
15 | /** 请求过程: 响应缓存 */
16 | export { cache } from './plugins/cache'
17 | /** 请求过程: 参数前置处理和响应后置处理 */
18 | export { transform } from './plugins/transform'
19 | /** 请求过程: 请求发送前登录态校验 */
20 | export { auth } from './plugins/auth'
21 |
22 | /** 工具: 全局 loading 控制 */
23 | export { loading } from './plugins/loading'
24 | /** 工具: mock */
25 | export { mock } from './plugins/mock'
26 | /** 工具: 多环境配置 */
27 | export { envs } from './plugins/envs'
28 | /** 工具: 参数规范化 */
29 | export { normalize } from './plugins/normalize'
30 | /** 工具: 路由参数处理 */
31 | export { pathParams } from './plugins/path-params'
32 | /** 工具: 请求签名 */
33 | export { sign } from './plugins/sign'
34 | /** 工具: sentry 请求错误日志上报 */
35 | export { sentryCapture } from './plugins/sentry-capture'
36 |
37 | /** 适配器: 仅发送 */
38 | export { onlySend } from './plugins/only-send'
39 | /** 适配器: 小程序、跨平台框架网络请求支持 */
40 | export { mp } from './plugins/mp'
41 |
42 | export * as Auth from './plugins/auth'
43 | export * as Cache from './plugins/cache'
44 | export * as Cancel from './plugins/cancel'
45 | export * as Debounce from './plugins/debounce'
46 | export * as Loading from './plugins/loading'
47 | export * as Merge from './plugins/merge'
48 | export * as Mock from './plugins/mock'
49 | export * as Mp from './plugins/mp'
50 | export * as Normalize from './plugins/normalize'
51 | export * as OnlySend from './plugins/only-send'
52 | export * as PathParams from './plugins/path-params'
53 | export * as Retry from './plugins/retry'
54 | export * as Sign from './plugins/sign'
55 | export * as Throttle from './plugins/throttle'
56 | export * as Transform from './plugins/transform'
57 |
--------------------------------------------------------------------------------
/src/intf/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type AxiosError,
3 | type AxiosInstance,
4 | type AxiosRequestConfig,
5 | type AxiosResponse,
6 | type InternalAxiosRequestConfig
7 | } from 'axios'
8 | import { type AbortChainController, type AbortError } from '../utils/create-abort-chain'
9 |
10 | export interface IUseAxiosPluginResult {
11 | plugin: (plug: IPlugin) => IUseAxiosPluginResult
12 | wrap: () => AxiosInstance
13 | }
14 |
15 | /** 实例内共享缓存 */
16 | export interface ISharedCache {
17 | [key: string]: any
18 | }
19 |
20 | /** axios 扩展属性 */
21 | export interface IAxiosPluginExtension {
22 | /** 已添加的插件集合 */
23 | __plugins__: Array
24 | /** 插件共享数据 */
25 | __shared__: ISharedCache
26 | }
27 |
28 | /** 扩展 axios 实例 */
29 | export type AxiosInstanceExtension = AxiosInstance & IAxiosPluginExtension
30 |
31 | /** 钩子共享参数 */
32 | export type IHooksShareOptions = {
33 | /** 原始请求参数 */
34 | readonly origin: AxiosRequestConfig
35 | /** 实例共享缓存 */
36 | readonly shared: ISharedCache
37 | /** axios 实例 */
38 | readonly axios: AxiosInstance
39 | }
40 |
41 | export enum ENext {
42 | next = 'next'
43 | }
44 |
45 | export type ILifecycleHookFunction = (
46 | value: V,
47 | options: IHooksShareOptions,
48 | controller: AbortChainController
49 | ) => V | Promise
50 |
51 | export type ILifecycleHookObject = {
52 | runWhen: (value: V, options: IHooksShareOptions) => boolean
53 | handler: ILifecycleHookFunction
54 | }
55 |
56 | export type ILifecycleHook = ILifecycleHookFunction | ILifecycleHookObject
57 |
58 | export interface IPluginLifecycle {
59 | /**
60 | * 在 `axios.request` 调用前触发钩子
61 | */
62 | preRequestTransform?: ILifecycleHook
63 |
64 | /**
65 | * `axios.interceptors.request` 钩子, 在拦截器内修改请求
66 | */
67 | transformRequest?: ILifecycleHook
68 | /**
69 | * 响应后触发钩子
70 | */
71 | postResponseTransform?: ILifecycleHook
72 | /**
73 | * 捕获异常钩子
74 | *
75 | * @description 这是一个特殊钩子, 将阻塞异常反馈, 并在钩子函数完成后, 返回正常结果. 如果需要抛出异常, 那么应通过 `throw Error` 方式, 抛出异常信息.
76 | */
77 | captureException?: ILifecycleHook
78 | /**
79 | * 请求中断钩子
80 | */
81 | aborted?: ILifecycleHook
82 | /**
83 | * 请求完成后置钩子
84 | */
85 | completed?:
86 | | ((options: IHooksShareOptions, controller: AbortChainController) => void | Promise)
87 | | {
88 | runWhen: (options: IHooksShareOptions) => boolean
89 | handler: (options: IHooksShareOptions, controller: AbortChainController) => void | Promise
90 | }
91 | }
92 |
93 | /** 插件接口 */
94 | export interface IPlugin {
95 | /** 插件名 */
96 | name: string
97 |
98 | /** 插件内部执行顺序 */
99 | enforce?: 'pre' | 'post'
100 |
101 | /**
102 | * 插件注册前置事件
103 | *
104 | * @description 可以在此检查 axios 实例是否可以支持当前插件的使用, 如果不能够支持, 应抛出异常.
105 | */
106 | beforeRegister?: (axios: AxiosInstanceExtension) => void
107 |
108 | /** 插件声明周期钩子函数
109 | *
110 | * @description 为了不对原有的axios实例产生影响,
111 | */
112 | lifecycle?: IPluginLifecycle
113 | }
114 |
--------------------------------------------------------------------------------
/src/plugins/auth.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError, InternalAxiosRequestConfig } from 'axios'
2 | import { IPlugin, ISharedCache } from '../intf'
3 | import { Filter, FilterPattern, createUrlFilter } from '../utils/create-filter'
4 | import { createOrGetCache } from '../utils/create-cache'
5 |
6 | /** 插件参数声明 */
7 | export interface IAuthOptions {
8 | /**
9 | * 指定哪些接口包含
10 | *
11 | * @description 未指定情况下, 所有接口均包含重复请求合并逻辑
12 | */
13 | includes?: FilterPattern
14 |
15 | /**
16 | * 指定哪些接口应忽略
17 | */
18 | excludes?: FilterPattern
19 |
20 | /** 是否只检查1次
21 | *
22 | * @description 如果 `login()` 返回 true 以后, 后续将跳过检查.
23 | */
24 | once?: boolean
25 | /**
26 | * 检查登录态
27 | *
28 | * @description
29 | * - 如果没有登录, 那么其他请求将被阻塞, 知道这个异步方法执行完成
30 | * - 如果已登录, 则校验通过后, 直接出发后续请求执行
31 | * - 如果设置了 timeout, 那么超时后, 将返回失败.
32 | *
33 | * @param {InternalAxiosRequestConfig} request 请求参数
34 | *
35 | * @returns 如果登录成功, 应返回 true, 如果登录失败或取消登录, 应返回 false 或抛出异常后, 自行处理后续逻辑.
36 | */
37 | login: (request: InternalAxiosRequestConfig) => Promise
38 | }
39 |
40 | interface SharedCache extends ISharedCache {
41 | auth: {
42 | /** 是否已经登录 () */
43 | isLogin?: boolean
44 | }
45 | }
46 |
47 | /**
48 | * 插件: 请求前, 登录态校验.
49 | *
50 | * @description 可以将每次请求前的登录检查、刷新token、登录操作抽象出来. 也可以用类似 `await Dialog.login()` 方式, 做前置的登录处理.
51 | */
52 | export const auth = (options: IAuthOptions): IPlugin => {
53 | const filter: Filter = createUrlFilter(options.includes, options.excludes)
54 |
55 | return {
56 | name: 'auth',
57 | lifecycle: {
58 | transformRequest: {
59 | runWhen: (config) => filter(config.url),
60 | handler: async (config, { shared }) => {
61 | // @ 从共享内存中创建或获取缓存对象
62 | const cache: SharedCache['auth'] = createOrGetCache(shared, 'auth')
63 | if (cache.isLogin && options.once) {
64 | return config
65 | }
66 | // ? 检查登录态
67 | const res: boolean = await options.login(config)
68 | if (!res) {
69 | throw new AxiosError('no auth', '401', config)
70 | }
71 | if (options.once) {
72 | cache.isLogin = true
73 | }
74 | return config
75 | }
76 | }
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/plugins/cache.ts:
--------------------------------------------------------------------------------
1 | import { AxiosInstance, AxiosRequestConfig } from 'axios'
2 |
3 | import { IHooksShareOptions, IPlugin } from '../intf'
4 |
5 | declare module 'axios' {
6 | interface CreateAxiosDefaults {
7 | /** 配置重复请求合并策略 */
8 | cache?: ICacheOptions
9 | }
10 |
11 | interface AxiosRequestConfig {
12 | /**
13 | * 配置是否触发重复请求合并策略
14 | *
15 | * @description 在一段时间内发起的重复请求, 仅请求一次, 并将请求结果分别返回给不同的发起者.
16 | *
17 | * - 需要注册 `merge()` 插件
18 | * - 不建议与 `debounce`, `throttle` 插件同时使用
19 | */
20 | cache?: boolean | (Pick & { key: string })
21 | }
22 | }
23 |
24 | /** 插件参数类型 */
25 | export interface ICacheOptions {
26 | /**
27 | * 缓存版本号
28 | *
29 | * @description 设置此参数可以避免因数据结构差异, 导致后续逻辑错误
30 | */
31 | version?: string
32 | /**
33 | * 过期时间
34 | *
35 | * @description 设置缓存有效期, 超过有效期将失效
36 | */
37 | expires?: number
38 |
39 | /**
40 | * 缓存key
41 | *
42 | * @description 缓存key遵循两个规则, 可以参考 `calcRequestHash` 自定义缓存键
43 | * @default ``` f(url, data, params) => hash ```
44 | */
45 | key?: (config: AxiosRequestConfig) => string
46 |
47 | /**
48 | * 响应缓存存储空间
49 | *
50 | * @default {sessionStorage}
51 | */
52 | storage?: Storage
53 |
54 | /**
55 | * storage 中, 缓存cache的字段名
56 | * @default ``` axios-plugins.cache ```
57 | */
58 | storageKey?: string
59 | }
60 |
61 | interface ICache {
62 | [key: string]: {
63 | /** 时效时间 */
64 | expires: number
65 | /** 响应内容 */
66 | res: any
67 | }
68 | }
69 |
70 | const mapping: Array<{ axios: AxiosInstance; patch: (patchCache: Partial) => void; clear: () => void }> = []
71 |
72 | /** 删除已有缓存 */
73 | export const removeCache = (axios: AxiosInstance, cacheKey: string): boolean => {
74 | for (const m of mapping) {
75 | if (m.axios === axios) {
76 | m.patch({ [cacheKey]: undefined })
77 | return true
78 | }
79 | }
80 | return false
81 | }
82 |
83 | /** 清除全部缓存 */
84 | export const clearAllCache = (axios: AxiosInstance): boolean => {
85 | for (const m of mapping) {
86 | if (m.axios === axios) {
87 | m.clear()
88 | return true
89 | }
90 | }
91 | return false
92 | }
93 |
94 | /**
95 | * 插件: 响应缓存
96 | *
97 | * @description 存储请求响应内容, 在下次请求时返回 (需要在缓存时效内)
98 | *
99 | * 注意: 考虑到缓存的复杂程度, 此插件仅允许对单个接口设置缓存, 且应在所有插件前注册
100 | */
101 | export const cache = (options: ICacheOptions = {}): IPlugin => {
102 | /** 触发检查 */
103 | const runWhen = (_: V, { origin }: IHooksShareOptions): boolean => {
104 | return !!origin['cache']
105 | }
106 |
107 | // @ 获取 storage
108 | const storage: Storage = options.storage ?? sessionStorage
109 | // @ 获取 storage 中, 存放缓存的字段名
110 | const storageKey: string = options.storageKey ?? 'axios-plugins.cache'
111 |
112 | const getCacheKey = (origin: AxiosRequestConfig, key: unknown): string | undefined => {
113 | if (typeof key === 'string') return key
114 | else if (typeof key === 'function') return key(origin)
115 | else if (typeof key === 'object') return getCacheKey(origin, (key as ICacheOptions).key)
116 | return undefined
117 | }
118 |
119 | const getCache = (): ICache => {
120 | let str: string | null = storage.getItem(storageKey)
121 | // ? 如果没有缓存, 跳过
122 | if (!str) {
123 | return {}
124 | }
125 | const { version, cache } = JSON.parse(str)
126 | if (version !== options.version) {
127 | return {}
128 | } else {
129 | return cache
130 | }
131 | }
132 |
133 | const patch = (patchCache: Partial): void => {
134 | const cache = getCache()
135 | Object.assign(cache, patchCache)
136 | for (const key of Object.keys(cache)) {
137 | if (!cache[key]?.expires || Date.now() > cache[key].expires) {
138 | delete cache[key]
139 | }
140 | }
141 | if (Object.keys(cache).length > 0) {
142 | storage.setItem(storageKey, JSON.stringify({ version: options.version, cache }))
143 | } else {
144 | storage.removeItem(storageKey)
145 | }
146 | }
147 | const clear = (): void => {
148 | storage.removeItem(storageKey)
149 | }
150 |
151 | return {
152 | name: 'cache',
153 | beforeRegister(axios) {
154 | // 参数合并
155 | Object.assign(options, axios.defaults['cache'])
156 | mapping.push({ axios, patch, clear })
157 | // 清理失效缓存
158 | patch({})
159 | },
160 | lifecycle: {
161 | preRequestTransform: {
162 | runWhen,
163 | /**
164 | * 请求前, 创建请求缓存, 遇到重复请求时, 将重复请求放入缓存等待最先触发的请求执行完成
165 | */
166 | handler: async (config, { origin }, { abort }) => {
167 | // @ 计算缓存的 key
168 | const key: string | undefined =
169 | getCacheKey(origin, origin.cache) ?? getCacheKey(origin, options.key)
170 | // 获取缓存
171 | const cache: ICache = getCache()
172 |
173 | if (key && cache[key]) {
174 | // ? 如果在有效期内, 中断请求并退出
175 | if (Date.now() < cache[key].expires) {
176 | abort(cache[key])
177 | } else {
178 | delete cache[key]
179 | }
180 | }
181 |
182 | return config
183 | }
184 | },
185 | postResponseTransform: {
186 | runWhen,
187 | handler: (response, { origin }) => {
188 | // @ 计算缓存的 key
189 | const key: string | undefined =
190 | getCacheKey(origin, origin.cache) ?? getCacheKey(origin, options.key)
191 | // patch to storage
192 | if (key) {
193 | patch({
194 | [key]: {
195 | expires: (origin.cache as any)?.expires ?? options.expires,
196 | res: response
197 | }
198 | })
199 | }
200 | return response
201 | }
202 | }
203 | }
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/src/plugins/cancel.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosInstance, CancelTokenSource, CanceledError } from 'axios'
2 | import { AxiosInstanceExtension, IPlugin, ISharedCache } from '../intf'
3 | import { createOrGetCache } from '../utils/create-cache'
4 |
5 | interface SharedCache extends ISharedCache {
6 | cancel: Array
7 | }
8 |
9 | /**
10 | * 插件: 取消请求
11 | *
12 | * @description 提供 `cancelAll()` 方法, 中止当前在进行的所有请求
13 | */
14 | export const cancel = (): IPlugin => {
15 | return {
16 | name: 'cancel',
17 | lifecycle: {
18 | preRequestTransform: {
19 | runWhen: (config) => !config.cancelToken,
20 | handler: (config, { origin, shared }) => {
21 | // @ 从共享内存中创建或获取缓存对象
22 | const cache: SharedCache['cancel'] = createOrGetCache(shared, 'cancel', [])
23 | const source: CancelTokenSource = axios.CancelToken.source()
24 | // > 复制给 config 用于请求过程执行
25 | config.cancelToken = source.token
26 | // > 复制给 origin, 用于在请求完成后作为清理内存标识
27 | origin.cancelToken = source.token
28 | // > 将终止请求的方法放到缓存中
29 | cache.push(source)
30 | return config
31 | }
32 | },
33 | captureException: {
34 | runWhen: (reason: any) => reason instanceof CanceledError,
35 | handler: (reason: any, {}, { abortError }) => {
36 | // ? 如果是 cancel 触发的请求, 那么终止执行, 并触发 `abortError`
37 | if (reason instanceof CanceledError) {
38 | abortError(reason)
39 | }
40 | return reason
41 | }
42 | },
43 | completed({ origin, shared }) {
44 | // @ 从共享内存中创建或获取缓存对象
45 | const cache: SharedCache['cancel'] = createOrGetCache(shared, 'cancel', [])
46 | const index: number = cache.findIndex((c) => c?.token === origin.cancelToken)
47 | // clear
48 | if (index !== -1) cache[index] = null
49 | }
50 | }
51 | }
52 | }
53 |
54 | /** 终止所有请求过程 */
55 | export const cancelAll = (axios: AxiosInstance, message?: string) => {
56 | const shared = (axios as AxiosInstanceExtension).__shared__ as SharedCache
57 | if (shared.cancel instanceof Array) {
58 | while (shared.cancel.length > 0) {
59 | const { cancel } = shared.cancel.pop()!
60 | cancel(message ?? '请求终止')
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/plugins/debounce.ts:
--------------------------------------------------------------------------------
1 | import { AxiosRequestConfig } from 'axios'
2 |
3 | import { IHooksShareOptions, IPlugin, ISharedCache } from '../intf'
4 | import { defaultCalcRequestHash as crh } from '../utils/calc-hash'
5 | import { createOrGetCache } from '../utils/create-cache'
6 | import { createUrlFilter, Filter, FilterPattern } from '../utils/create-filter'
7 | import { delay, getDelayTime } from '../utils/delay'
8 |
9 | declare module 'axios' {
10 | interface CreateAxiosDefaults {
11 | /** 配置防抖策略 */
12 | debounce?: IDebounceOptions
13 | }
14 |
15 | interface AxiosRequestConfig {
16 | /**
17 | * 配置是否触发防抖策略
18 | *
19 | * @description 在一段时间内发起的重复请求, 后执行的请求将等待上次请求完成后再执行
20 | *
21 | * - 需要注册 `debounce()` 插件
22 | * - 不建议与 `merge`, `throttle` 插件同时使用
23 | */
24 | debounce?: boolean | Pick
25 | }
26 | }
27 |
28 | /** 插件参数类型 */
29 | export interface IDebounceOptions {
30 | /**
31 | * 指定哪些接口包含
32 | */
33 | includes?: FilterPattern
34 | /**
35 | * 指定哪些接口应忽略
36 | */
37 | excludes?: FilterPattern
38 | /**
39 | * 延迟判定时间
40 | *
41 | * @description 当设置此值时, 在请求完成后 n 秒内发起的请求都属于重复请求
42 | * @default 0ms
43 | */
44 | delay?: number
45 | /** 自定义: 计算请求 hash 值
46 | *
47 | * @description 定制重复请求检查方法, 当请求hash值相同时, 判定两个请求为重复请求.
48 | * @default ``` f(url, data, params) => hash ```
49 | */
50 | calcRequstHash?: (config: AxiosRequestConfig) => string
51 | }
52 |
53 | interface SharedCache extends ISharedCache {
54 | debounce: {
55 | [hash: string]: Array<{ resolve: Function }>
56 | }
57 | }
58 |
59 | /**
60 | * 插件: 防抖
61 | *
62 | * @description 在一段时间内发起的重复请求, 后执行的请求将等待上次请求完成后再执行
63 | */
64 | export const debounce = (options: IDebounceOptions = {}): IPlugin => {
65 | /** 触发检查 */
66 | const runWhen = (_: V, { origin }: IHooksShareOptions): boolean => {
67 | if (origin['debounce']) {
68 | return !!origin['debounce']
69 | } else {
70 | const filter: Filter = createUrlFilter(options.includes, options.excludes)
71 | return filter(origin.url)
72 | }
73 | }
74 | return {
75 | name: 'debounce',
76 | beforeRegister(axios) {
77 | // 参数合并
78 | options = Object.assign({ calcRequstHash: crh }, options, axios.defaults['debounce'])
79 | },
80 | lifecycle: {
81 | preRequestTransform: {
82 | runWhen,
83 | handler: async (config, { origin, shared }) => {
84 | // @ 计算请求hash
85 | const hash: string = options.calcRequstHash!(origin)
86 | // @ 从共享内存中创建或获取缓存对象
87 | const cache: SharedCache['debounce'] = createOrGetCache(shared, 'debounce')
88 | // ? 判断是否重复请求
89 | if (cache[hash]) {
90 | // 创建延迟工具, 等待其他任务执行完成
91 | await new Promise((resolve) => {
92 | cache[hash].push({ resolve })
93 | })
94 | } else {
95 | // 创建重复请求缓存
96 | cache[hash] = []
97 | }
98 | return config
99 | }
100 | },
101 | completed: {
102 | runWhen: (options) => {
103 | return runWhen(undefined, options)
104 | },
105 | handler: async ({ origin, shared }) => {
106 | // @ 计算请求hash
107 | const hash: string = options.calcRequstHash!(origin)
108 | // @ 从共享内存中创建或获取缓存对象
109 | const cache: SharedCache['debounce'] = createOrGetCache(shared, 'debounce')
110 | // @ 获取延时时间
111 | const delayTime: number = getDelayTime(0, origin.debounce, options.delay)
112 | // ? 判断cache中, 是否包含阻塞的请求
113 | if (cache[hash]?.length) {
114 | const { resolve } = cache[hash].shift()!
115 | // > 在延时结束后, 触发缓存中被拦截下来的请求
116 | delay(delayTime).then(() => resolve())
117 | } else {
118 | // > 如果没有等待未执行的任务, 清理缓存
119 | delete cache[hash]
120 | }
121 | }
122 | }
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/plugins/envs.ts:
--------------------------------------------------------------------------------
1 | import { AxiosDefaults, AxiosHeaderValue, HeadersDefaults } from 'axios'
2 | import { IPlugin } from '../intf'
3 |
4 | /** 插件参数声明 */
5 | export type IEnvsOptions = Array<{
6 | rule: () => boolean
7 | config: Omit & {
8 | headers: HeadersDefaults & {
9 | [key: string]: AxiosHeaderValue
10 | }
11 | }
12 | }>
13 | /**
14 | * 插件: 多环境配置
15 | *
16 | * @description 规范化 axios 多环境配置工具
17 | */
18 | export const envs = (options: IEnvsOptions = []): IPlugin => {
19 | return {
20 | name: 'envs',
21 | beforeRegister(axios) {
22 | for (const { rule, config } of options) {
23 | if (rule()) {
24 | Object.assign(axios.defaults, config)
25 | break
26 | }
27 | }
28 | axios.defaults
29 | },
30 | lifecycle: {}
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/plugins/loading.ts:
--------------------------------------------------------------------------------
1 | import { IPlugin, ISharedCache } from '../intf'
2 | import { createOrGetCache } from '../utils/create-cache'
3 | import { Filter, FilterPattern, createUrlFilter } from '../utils/create-filter'
4 |
5 | declare module 'axios' {
6 | interface AxiosRequestConfig {
7 | /**
8 | * 设置当前请求是否触发 loading 切换判断
9 | *
10 | * @default {true}
11 | */
12 | loading?: boolean
13 | }
14 | }
15 |
16 | /** 插件参数类型 */
17 | export interface ILoadingOptions {
18 | /**
19 | * 指定哪些接口包含
20 | *
21 | * @description 未指定情况下, 所有接口均包含重复请求合并逻辑
22 | */
23 | includes?: FilterPattern
24 |
25 | /**
26 | * 指定哪些接口应忽略
27 | */
28 | excludes?: FilterPattern
29 |
30 | /**
31 | * 请求发起后, 延时多少毫秒显示loading
32 | *
33 | * @default 200ms
34 | */
35 | delay?: number
36 |
37 | /**
38 | * 是否延时关闭, 当所有请求完成后, 延迟多少毫秒关闭loading
39 | *
40 | * @default 200ms
41 | */
42 | delayClose?: number
43 |
44 | /**
45 | * 触发全局loading的切换事件
46 | *
47 | * @description 需要自行实现 loading 显示/隐藏的管理逻辑
48 | */
49 | onTrigger: (show: boolean) => void
50 | }
51 |
52 | interface SharedCache extends ISharedCache {
53 | /** loading 插件缓存 */
54 | loading: {
55 | /** 等待完成的请求计数 */
56 | pending: number
57 | /** loading 状态 */
58 | status: boolean
59 | /** 定时器 */
60 | timer?: any
61 | }
62 | }
63 |
64 | /**
65 | * 插件: 全局 loading 控制
66 | *
67 | * @description 提供全局 loading 统一控制能力, 减少每个加载方法都需要独立 loading 控制的工作
68 | *
69 | * - 如果插件链或`axios.interceptors`中存在耗时逻辑, 那么应将 loading 插件添加在插件链的最前面
70 | */
71 | export const loading = (options: ILoadingOptions): IPlugin => {
72 | const { delay, delayClose, onTrigger } = options
73 | let delayTimer: any
74 | let delayCloseTimer: any
75 | /** 触发检查 */
76 | const runWhen = (_: V, { origin }: any): boolean => {
77 | if (origin['loading']) {
78 | return !!origin['loading']
79 | } else {
80 | const filter: Filter = createUrlFilter(options.includes, options.excludes)
81 | return filter(origin.url)
82 | }
83 | }
84 | /** 打开loading */
85 | const open = (req: T, { shared }: { shared: ISharedCache }): T => {
86 | // @ 从共享内存中创建或获取缓存对象
87 | const cache: SharedCache['loading'] = createOrGetCache(shared, 'loading')
88 | cache.pending ? cache.pending++ : (cache.pending = 1)
89 | if (!cache.status && cache.pending > 0) {
90 | if (delayTimer) clearTimeout(delayTimer)
91 | delayTimer = setTimeout(() => {
92 | cache.status = true
93 | onTrigger(true)
94 | }, delay ?? 0)
95 | }
96 | if (delayCloseTimer) clearTimeout(delayCloseTimer)
97 | return req
98 | }
99 | /** 关闭loading */
100 | const close = (res: T, { shared }: { shared: ISharedCache }): T => {
101 | // @ 从共享内存中创建或获取缓存对象
102 | const cache: SharedCache['loading'] = createOrGetCache(shared, 'loading')
103 | cache.pending--
104 | if (cache.status && cache.pending <= 0) {
105 | if (delayCloseTimer) clearTimeout(delayCloseTimer)
106 | delayCloseTimer = setTimeout(() => {
107 | cache.status = false
108 | onTrigger(false)
109 | }, 200)
110 | }
111 | return res
112 | }
113 | /** 在捕获异常时关闭 */
114 | const closeOnError = (reason: T, { shared }: { shared: ISharedCache }) => {
115 | // @ 从共享内存中创建或获取缓存对象
116 | const cache: SharedCache['loading'] = createOrGetCache(shared, 'loading')
117 | cache.pending--
118 | if (cache.status && cache.pending <= 0) {
119 | if (delayCloseTimer) clearTimeout(delayCloseTimer)
120 | delayCloseTimer = setTimeout(() => {
121 | cache.status = false
122 | onTrigger(false)
123 | }, delayClose ?? 0)
124 | }
125 | return reason
126 | }
127 | return {
128 | name: 'loading',
129 | lifecycle: {
130 | preRequestTransform: { runWhen, handler: open },
131 | postResponseTransform: { runWhen, handler: close },
132 | captureException: { runWhen, handler: closeOnError },
133 | aborted: { runWhen, handler: closeOnError }
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/plugins/merge.ts:
--------------------------------------------------------------------------------
1 | import { AxiosRequestConfig } from 'axios'
2 |
3 | import { IHooksShareOptions, IPlugin, ISharedCache } from '../intf'
4 | import { defaultCalcRequestHash as crh } from '../utils/calc-hash'
5 | import { createOrGetCache } from '../utils/create-cache'
6 | import { createUrlFilter, Filter, FilterPattern } from '../utils/create-filter'
7 | import { delay, getDelayTime } from '../utils/delay'
8 |
9 | declare module 'axios' {
10 | interface CreateAxiosDefaults {
11 | /** 配置重复请求合并策略 */
12 | merge?: IMergeOptions
13 | }
14 |
15 | interface AxiosRequestConfig {
16 | /**
17 | * 配置是否触发重复请求合并策略
18 | *
19 | * @description 在一段时间内发起的重复请求, 仅请求一次, 并将请求结果分别返回给不同的发起者.
20 | *
21 | * - 需要注册 `merge()` 插件
22 | * - 不建议与 `debounce`, `throttle` 插件同时使用
23 | */
24 | merge?: boolean | Pick
25 | }
26 | }
27 |
28 | /** 插件参数类型 */
29 | export interface IMergeOptions {
30 | /**
31 | * 指定哪些接口包含
32 | *
33 | * @description 未指定情况下, 所有接口均包含重复请求合并逻辑
34 | */
35 | includes?: FilterPattern
36 |
37 | /**
38 | * 指定哪些接口应忽略
39 | */
40 | excludes?: FilterPattern
41 |
42 | /**
43 | * 延迟判定时间
44 | *
45 | * @description 当设置此值时, 在请求完成后 n 秒内发起的请求都属于重复请求
46 | * @default 200ms
47 | */
48 | delay?: number
49 |
50 | /** 自定义: 计算请求 hash 值
51 | *
52 | * @description 定制重复请求检查方法, 当请求hash值相同时, 判定两个请求为重复请求.
53 | * @default ``` f(url, data, params) => hash ```
54 | */
55 | calcRequstHash?: (config: AxiosRequestConfig) => string
56 | }
57 |
58 | interface SharedCache extends ISharedCache {
59 | merge: {
60 | [hash: string]: Array<(result: { status: boolean; response?: any; reason?: any }) => void>
61 | }
62 | }
63 |
64 | /**
65 | * 插件: 合并重复请求
66 | *
67 | * @description 在一段时间内发起的重复请求, 仅请求一次, 并将请求结果分别返回给不同的发起者.
68 | */
69 | export const merge = (options: IMergeOptions = {}): IPlugin => {
70 | /** 触发检查 */
71 | const runWhen = (_: V, { origin }: IHooksShareOptions): boolean => {
72 | if (origin['merge']) {
73 | return !!origin['merge']
74 | } else if (origin.url) {
75 | const filter: Filter = createUrlFilter(options.includes, options.excludes)
76 | return filter(origin.url)
77 | } else {
78 | return false
79 | }
80 | }
81 |
82 | /** 分发合并请求响应值 */
83 | const distributionMergeResponse = async (
84 | { origin, shared }: IHooksShareOptions,
85 | cb: (opt: SharedCache['merge'][any][0]) => void
86 | ) => {
87 | // @ 计算请求hash
88 | const hash: string = options.calcRequstHash!(origin)
89 | // @ 从共享内存中创建或获取缓存对象
90 | const cache: SharedCache['merge'] = createOrGetCache(shared, 'merge')
91 | // @ 获取延时时间
92 | const delayTime: number = getDelayTime(200, origin.debounce, options.delay)
93 | // > 将结果分发给缓存中的请求
94 | if (cache[hash]) {
95 | // > 分2次分发 (分别是响应结束、延时结束, 避免请求过度阻塞
96 | for (const callback of cache[hash] ?? []) cb(callback)
97 | delay(delayTime).then(() => {
98 | for (const callback of cache[hash] ?? []) cb(callback)
99 | delete cache[hash]
100 | })
101 | }
102 | }
103 |
104 | return {
105 | name: 'merge',
106 | beforeRegister(axios) {
107 | // 参数合并
108 | options = Object.assign({ calcRequstHash: crh }, options, axios.defaults['merge'])
109 | },
110 | lifecycle: {
111 | preRequestTransform: {
112 | runWhen,
113 | /**
114 | * 请求前, 创建请求缓存, 遇到重复请求时, 将重复请求放入缓存等待最先触发的请求执行完成
115 | */
116 | handler: async (config, { origin, shared }, { abort, abortError }) => {
117 | // @ 计算请求hash
118 | const hash: string = options.calcRequstHash!(origin)
119 | // @ 从共享内存中创建或获取缓存对象
120 | const cache: SharedCache['merge'] = createOrGetCache(shared, 'merge')
121 | // ? 当判断请求为重复请求时, 添加到缓存中, 等待最先发起的请求完成
122 | if (cache[hash]) {
123 | const { status, response, reason } = await new Promise(
124 | (resolve: SharedCache['merge'][''][0]) => {
125 | cache[hash].push(resolve)
126 | }
127 | )
128 | if (status) abort(response)
129 | else abortError(reason)
130 | return response
131 | } else {
132 | // 创建重复请求缓存
133 | cache[hash] = []
134 | return config
135 | }
136 | }
137 | },
138 | postResponseTransform: {
139 | runWhen,
140 | /**
141 | * 请求结束后, 向缓存中的请求分发结果 (分发成果结果)
142 | */
143 | handler: async (response, opt) => {
144 | distributionMergeResponse(opt, (resolve) => resolve({ status: true, response }))
145 | return response
146 | }
147 | },
148 | captureException: {
149 | runWhen,
150 | /**
151 | * 请求结束后, 向缓存中的请求分发结果 (分发失败结果)
152 | */
153 | handler: async (reason, opt) => {
154 | distributionMergeResponse(opt, (resolve) => resolve({ status: false, reason }))
155 | return reason
156 | }
157 | },
158 | aborted: {
159 | runWhen,
160 | /**
161 | * 如果请求被中断, 那么清理merge缓存
162 | */
163 | handler: (reason, opt) => {
164 | distributionMergeResponse(opt, () => {})
165 | throw reason
166 | }
167 | }
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/src/plugins/mock.ts:
--------------------------------------------------------------------------------
1 | import { IPlugin } from '../intf'
2 |
3 | import { isAbsoluteURL, combineURLs } from '../utils/url'
4 |
5 | declare module 'axios' {
6 | interface CreateAxiosDefaults {
7 | /** 配置mock请求策略 */
8 | mock?: boolean | Pick
9 | }
10 |
11 | interface AxiosRequestConfig {
12 | /**
13 | * 配置是否将请求映射到mock服务器
14 | *
15 | * @description 提供全局或单个接口请求 mock 能力
16 | *
17 | * - 需要注册 `mock()` 插件
18 | */
19 | mock?: boolean | Pick
20 | }
21 | }
22 |
23 | /** 插件参数声明: mock */
24 | export interface IMockOptions {
25 | /**
26 | * 是否启用插件
27 | *
28 | * @type {boolean} 是否启用插件,
29 | * - 在 `vitejs` 环境下, 建议配置为 `enable: !!import.meta.env.DEV`
30 | * - 在 `webpack` 环境下, 建议配置为 `enable: process.env.NODE_ENV === 'development'`
31 | * @default {false}
32 | */
33 | enable: boolean
34 |
35 | /**
36 | * 配置是否将请求映射到mock服务器
37 | *
38 | * @description 提供全局或单个接口请求 mock 能力
39 | *
40 | * - 需要注册 `mock()` 插件
41 | */
42 | mock?: boolean
43 |
44 | /**
45 | * mock 工具地址 | mock's baseUrl
46 | */
47 | mockUrl?: string
48 | }
49 |
50 | /**
51 | * 插件: mock 请求
52 | *
53 | * @description 提供全局或单个接口请求 mock 能力
54 | *
55 | * 注意: `mock` 修改的请求参数会受到 `axios.interceptors.
56 | */
57 | export const mock = (options: IMockOptions = { enable: false }): IPlugin => {
58 | return {
59 | name: 'mock',
60 | beforeRegister(axios) {
61 | // 参数合并
62 | Object.assign(options, axios.defaults['mock'])
63 | // ? 校验必要参数配置
64 | if (!options.mockUrl) {
65 | throw new Error(`headers 中似乎并没有配置 'mockURL'`)
66 | }
67 | },
68 | lifecycle: {
69 | preRequestTransform: {
70 | runWhen(_, { origin }) {
71 | if (!options.enable) {
72 | return false
73 | }
74 | // mock 启用条件
75 | // 条件1: `enable === true`
76 | // 条件2: `config.mock === true` or `options.mock === true`
77 | let mock = origin.mock ?? options.mock
78 | if (typeof mock === 'object') {
79 | return !!mock.mock
80 | } else {
81 | return !!mock
82 | }
83 | },
84 | handler: (config) => {
85 | const { mockUrl } = options
86 | const { url } = config
87 | if (!url) {
88 | // ? 如果未配置请求地址, 那么替换成 `baseUrl`
89 | config.baseURL = mockUrl
90 | } else {
91 | // ? 否则, 则填充(替换) url
92 | // TIP: 考虑到有些接口是配置的完整url, 此时比较合理的做法是替换url地址, 否则会因为存在完整url, 导致baseUrl选项失效.
93 | if (!isAbsoluteURL(url)) {
94 | config.url = combineURLs(mockUrl!, url)
95 | } else {
96 | // 转换为url对象
97 | const u = new URL(url)
98 | // 去除origin, 并附加 mockURL
99 | config.url = combineURLs(mockUrl!, url.replace(u.origin, ''))
100 | }
101 | }
102 | return config
103 | }
104 | }
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/plugins/mp.ts:
--------------------------------------------------------------------------------
1 | import { AxiosPromise, InternalAxiosRequestConfig } from 'axios'
2 | import { IPlugin } from '../intf'
3 | import { combineURLs, isAbsoluteURL } from '../utils/url'
4 | /** 插件参数类型 */
5 | export interface ILoadingOptions {
6 | env:
7 | | 'wx' // 微信
8 | | 'alipay' // 支付宝
9 | | 'baidu' // 百度
10 | | 'tt' // 头条
11 | | 'douyin' // 抖音
12 | | 'feishu' // 飞书小程序
13 | | 'dingTalk' // 钉钉小程序
14 | | 'qq' // qq小程序
15 | | 'uni' // uni-app
16 | | 'Taro' // Taro
17 | | string // 如果小程序平台不在上述预设, 那么可以使用自定义的预设名
18 |
19 | /**
20 | * 公共参数
21 | *
22 | * @description 由于不同平台差异, 通过 axios 转化的公共参数可能不够使用, 所以 这里预留了一个注入公共参数的接口.
23 | */
24 | config?: C
25 | }
26 |
27 | /** 小程序API前缀映射 */
28 | const mapping = {
29 | alipay: 'my',
30 | baidu: 'swan',
31 | douyin: 'tt',
32 | feishu: 'tt',
33 | dingTalk: 'dd'
34 | }
35 |
36 | /** 小程序请求错误 */
37 | export class MpRequestError extends Error {
38 | type = 'MpRequestError'
39 |
40 | /** 错误信息 */
41 | errMsg: string
42 | /** 需要基础库: `2.24.0`
43 | *
44 | * errno 错误码,错误码的详细说明参考 [Errno错误码](https://developers.weixin.qq.com/miniprogram/dev/framework/usability/PublicErrno.html) */
45 | errno: number
46 |
47 | constructor(err: { errMsg: string; errno: number }) {
48 | super(err.errMsg)
49 | this.errMsg = err.errMsg
50 | this.errno = err.errno
51 | }
52 | }
53 | /**
54 | * 适配器: 小程序请求
55 | *
56 | * @description 扩展对微信、头条、qq 等小程序请求的支持
57 | *
58 | * @support 微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝/快应用/uni-app/Taro
59 | */
60 | export const mp = (options: ILoadingOptions): IPlugin => {
61 | return {
62 | name: 'mp',
63 | enforce: 'post',
64 | lifecycle: {
65 | preRequestTransform(config) {
66 | if (typeof config.adapter === 'function') {
67 | throw new Error('适配器已经配置过了, 重复添加将产生冲突, 请检查!')
68 | }
69 | config.adapter = (config: InternalAxiosRequestConfig): AxiosPromise => {
70 | return new Promise((resolve, reject) => {
71 | let env: string = mapping[options.env as keyof typeof mapping] ?? options.env
72 | let sys: any = (globalThis as any)[env]
73 | if (!sys) {
74 | return reject(new Error(`插件不可用, 未找到 '${env}' 全局变量`))
75 | }
76 | if (!config.url) return reject(new Error("缺少必填参数 'url'"))
77 | // > 补全路径
78 | if (!isAbsoluteURL(config.url) && config.baseURL) {
79 | config.url = combineURLs(config.baseURL, config.url)
80 | }
81 | sys.request({
82 | method: config.method?.toUpperCase(),
83 | url: config.url,
84 | data: Object.assign({}, config.data, config.params),
85 | header: config.headers,
86 | timeout: config.timeout,
87 | // 合并公共参数
88 | ...options.config,
89 | success: (result: any): void => {
90 | resolve({
91 | data: result.data,
92 | status: result.statusCode,
93 | statusText: result.errMsg,
94 | headers: {
95 | 'set-cookie': result.cookies,
96 | ...result.header
97 | },
98 | config: config
99 | })
100 | },
101 | fail: (err: any) => {
102 | reject(new MpRequestError(err))
103 | }
104 | })
105 | })
106 | }
107 | return config
108 | }
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/plugins/normalize.ts:
--------------------------------------------------------------------------------
1 | import { IPlugin } from '../intf'
2 |
3 | type FilterRule =
4 | | {
5 | /** 过滤 null 值 */
6 | noNull?: boolean
7 | /** 过滤 undefined 值 */
8 | noUndefined?: boolean
9 | /** 过滤 nan */
10 | noNaN?: boolean
11 | /** 是否对对象进行递归 */
12 | deep?: boolean
13 | }
14 | | false
15 |
16 | /** 插件参数声明 */
17 | export interface INormalizeOptions {
18 | /** 过滤url */
19 | url?:
20 | | {
21 | noDuplicateSlash?: boolean
22 | }
23 | | boolean
24 | /**
25 | * 过滤 data
26 | *
27 | * @description 仅能够过滤 Object 类型参数
28 | */
29 | data?: FilterRule
30 | /**
31 | * 过滤 params
32 | *
33 | * @description 仅能够过滤 Object 类型参数
34 | */
35 | params?: FilterRule
36 | }
37 |
38 | /**
39 | * 插件: 规范化请求参数
40 | *
41 | * @description 过滤请求过程中产生的 undefined, null 等参数
42 | */
43 | export const normalize = (options: INormalizeOptions = {}): IPlugin => {
44 | return {
45 | name: 'normalize',
46 | lifecycle: {
47 | async transformRequest(config) {
48 | const def: INormalizeOptions['data'] = {
49 | noNull: false,
50 | noUndefined: true,
51 | noNaN: false,
52 | deep: false
53 | }
54 | let filterDataRule: INormalizeOptions['data'] = def
55 | const normal = (data: any): void => {
56 | if (!filterDataRule) return data
57 | if (typeof data === 'object') {
58 | if (data instanceof Array || data instanceof File) {
59 | return
60 | }
61 | for (const key in config.data) {
62 | let value = config.data[key]
63 | if (filterDataRule.noUndefined && value === undefined) {
64 | delete config.data[key]
65 | }
66 | if (filterDataRule.noNull && value === null) {
67 | delete config.data[key]
68 | }
69 | if (filterDataRule.noNaN && isNaN(value)) {
70 | delete config.data[key]
71 | }
72 | }
73 | }
74 | }
75 |
76 | if (config.url && (options.url === true || (options.url as any)?.noDuplicateSlash)) {
77 | let reg: RegExp = /^([a-z][a-z\d\+\-\.]*:)?\/\//i
78 | let matched: Array | null = config.url.match(reg)
79 | let schema: string = matched ? matched[0] : ''
80 | config.url = schema + config.url.replace(schema, '').replace(/[\/]{2,}/g, '/')
81 | }
82 |
83 | if (options.data !== false) {
84 | filterDataRule = typeof options.data === 'object' ? options.data : def
85 | normal(config.data)
86 | }
87 |
88 | if (options.params !== false) {
89 | filterDataRule = typeof options.params === 'object' ? options.params : def
90 | normal(config.params)
91 | }
92 | return config
93 | }
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/plugins/only-send.ts:
--------------------------------------------------------------------------------
1 | import { toFormData, AxiosPromise, InternalAxiosRequestConfig } from 'axios'
2 | import { IPlugin } from '../intf'
3 | import { combineURLs, isAbsoluteURL } from '../utils/url'
4 | /** 插件参数类型 */
5 | export interface IOnlySendOptions {
6 | /** 如果浏览器不支持 `navigator.sendBeacon` api, 那么应该如何操作
7 | *
8 | * @type {'lower'} (default) 降级, 使用 XHRRequest继续请求
9 | * @type {'error'} 抛出异常信息, 中断请求
10 | */
11 | noSupport?: 'lower' | 'error'
12 | }
13 |
14 | /** 仅发送插件相关异常 */
15 | export class OnlySendError extends Error {
16 | type = 'onlySend'
17 | }
18 |
19 | /**
20 | * 插件: 仅发送
21 | *
22 | * @description 提供 `navigator.sendBeacon` 方法封装, 实现页面离开时的埋点数据提交, 但这个需要后端支持
23 | */
24 | export const onlySend = (options: IOnlySendOptions = {}): IPlugin => {
25 | return {
26 | name: 'onlySend',
27 | enforce: 'post',
28 | lifecycle: {
29 | preRequestTransform(config) {
30 | if (typeof config.adapter === 'function') {
31 | throw new Error('适配器已经配置过了, 重复添加将产生冲突, 请检查!')
32 | }
33 |
34 | if (!navigator.sendBeacon) {
35 | let message: string = '当前浏览器不支持 `navigator.sendBeacon`'
36 | if (options.noSupport === 'error') {
37 | throw new OnlySendError(message)
38 | } else {
39 | console.error(message)
40 | }
41 | } else {
42 | config.adapter = async (config: InternalAxiosRequestConfig): AxiosPromise => {
43 | if (!config.url) throw new Error("缺少必填参数 'url'")
44 | // > 补全路径
45 | if (!isAbsoluteURL(config.url) && config.baseURL) {
46 | config.url = combineURLs(config.baseURL, config.url)
47 | }
48 | const form = new FormData()
49 | toFormData(Object.assign({}, config.data, config.params), new FormData())
50 | let success = navigator.sendBeacon(config.url, form)
51 | return {
52 | config,
53 | data: null,
54 | headers: {},
55 | status: success ? 200 : 500,
56 | statusText: 'success'
57 | }
58 | }
59 | }
60 |
61 | return config
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/plugins/path-params.ts:
--------------------------------------------------------------------------------
1 | import { IPlugin } from '../intf'
2 |
3 | /** 插件参数声明 */
4 | export interface IPathParamsOptions {
5 | /**
6 | * 从哪里获取 url 路径参数
7 | *
8 | * @default > 默认情况下, data, params 都会去检索.
9 | */
10 | form?: 'data' | 'params'
11 | }
12 |
13 | /**
14 | * 插件: 路由参数处理
15 | *
16 | * @description 扩展对 Restful API 规范的路由参数支持
17 | *
18 | * - url 格式需满足: `/api/${query}` 特征
19 | */
20 | export const pathParams = (options: IPathParamsOptions = {}): IPlugin => {
21 | return {
22 | name: 'pathParams',
23 | lifecycle: {
24 | async transformRequest(config) {
25 | const reg: RegExp = /[\$]{0,1}\{.+?\}/g
26 | const getKey = (part: string): string | undefined => {
27 | return part.match(/\$\{(.+?)\}/)?.[0]
28 | }
29 | const getValue = (key: string): any => {
30 | let ds = options.form ? config[options.form] : { ...config.data, ...config.params }
31 | return ds[key]
32 | }
33 | // 通过正则获取路径参数后, 将url中的路径参数进行替换
34 | const parts: Array = config.url?.match(reg) ?? []
35 | if (parts.length) {
36 | for (const part of parts) {
37 | const key: string | undefined = getKey(part)
38 | if (key) {
39 | const value: any = getValue(key)
40 | config.url = config.url!.replace(part, value)
41 | }
42 | }
43 | }
44 | return config
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/plugins/retry.ts:
--------------------------------------------------------------------------------
1 | import { AxiosResponse } from 'axios'
2 | import { IHooksShareOptions, IPlugin, ISharedCache } from '../intf'
3 | import { createOrGetCache } from '../utils/create-cache'
4 | import { createUrlFilter, Filter, FilterPattern } from '../utils/create-filter'
5 | import { defaultCalcRequestHash as crh } from '../utils/calc-hash'
6 |
7 | declare module 'axios' {
8 | interface CreateAxiosDefaults {
9 | /** 配置重试策略 */
10 | retry?: IRetryOptions
11 | }
12 | interface AxiosRequestConfig {
13 | /**
14 | * 接口请求失败重试规则
15 | *
16 | * @description
17 | * - 需要注册 `retry()` 插件, 指示接口请求失败后, 重试几次
18 | * - 设置为 0 时, 禁用重试功能
19 | */
20 | retry?: number | Pick
21 | }
22 | }
23 |
24 | /** 插件参数类型 */
25 | export interface IRetryOptions {
26 | /**
27 | * 指定哪些接口包含
28 | *
29 | * @description 建议使用 `axios.request({ retry: 3 })` 方式对单个请求设置重试规则
30 | */
31 | includes?: FilterPattern
32 | /**
33 | * 指定哪些接口应忽略
34 | */
35 | excludes?: FilterPattern
36 |
37 | /**
38 | * 最大重试次数
39 | *
40 | * @description 如果请求时, 指定了失败重试次数, 那么根据请求上标识, 确认失败后重试几次
41 | */
42 | max: number
43 |
44 | /**
45 | * 自定义异常请求检查方法
46 | *
47 | * @description 默认情况下, 仅在捕获到axios抛出异常时, 触发重试规则, 也可以通过此方法自定义重试检查
48 | */
49 | isExceptionRequest?: (response: AxiosResponse, options: IHooksShareOptions) => boolean
50 | }
51 |
52 | interface SharedCache extends ISharedCache {
53 | retry: {
54 | [hash: string]: number
55 | }
56 | }
57 | /** 重试异常 */
58 | class RetryError extends Error {
59 | type: string = 'retry'
60 | }
61 |
62 | /**
63 | * 插件: 失败重试
64 | *
65 | * @description 当请求失败(出错)后, 重试 n 次, 当全部失败时, 再抛出异常.
66 | *
67 | */
68 | export const retry = (options: IRetryOptions): IPlugin => {
69 | /** 触发检查 */
70 | const runWhen = (_: V, { origin }: IHooksShareOptions): boolean => {
71 | if (origin['retry']) {
72 | return !!origin['retry']
73 | } else {
74 | const filter: Filter = createUrlFilter(options.includes, options.excludes)
75 | return filter(origin.url)
76 | }
77 | }
78 | return {
79 | name: 'retry',
80 | beforeRegister(axios) {
81 | // 参数合并
82 | Object.assign(options, axios.defaults['retry'])
83 | },
84 | lifecycle: {
85 | postResponseTransform: {
86 | runWhen(_, opts) {
87 | if (!runWhen(_, opts)) return false
88 | if (typeof opts.origin.retry === 'object') {
89 | return !!opts.origin.retry.isExceptionRequest
90 | } else {
91 | return !!options.isExceptionRequest
92 | }
93 | },
94 | handler(response, opts) {
95 | let isExceptionRequest: IRetryOptions['isExceptionRequest']
96 | if (typeof opts.origin.retry === 'object') {
97 | isExceptionRequest = opts.origin.retry.isExceptionRequest
98 | } else {
99 | isExceptionRequest = options.isExceptionRequest
100 | }
101 | // > 通过自定义的异常判断方法, 判断请求是否需要重试
102 | if (isExceptionRequest?.(response, opts)) {
103 | throw new RetryError()
104 | }
105 | return response
106 | }
107 | },
108 | captureException: {
109 | runWhen,
110 | async handler(reason, { origin, shared, axios }, { abortError }) {
111 | // @ 计算请求hash
112 | const hash: string = crh(origin)
113 | // @ 从共享内存中创建或获取缓存对象
114 | const cache: SharedCache['retry'] = createOrGetCache(shared, 'retry')
115 | // @ 获取最大重试次数
116 | let max: number
117 | if (typeof origin.retry === 'object') {
118 | max = origin.retry.max
119 | } else if (typeof origin.retry === 'number') {
120 | max = origin.retry
121 | } else {
122 | max = options.max
123 | }
124 | max = max ?? 0
125 | // ? 判断请求已达到最大重试次数, 达到时中断请求过程, 并抛出异常.
126 | if (cache[hash] && cache[hash] >= max) {
127 | // 删除重试记录
128 | delete cache[hash]
129 | abortError(reason)
130 | } else {
131 | // 添加重试失败次数
132 | if (!cache[hash]) {
133 | cache[hash] = 1
134 | } else {
135 | cache[hash]++
136 | }
137 | // > 发起重试
138 | return await axios.request(origin)
139 | }
140 | }
141 | }
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/plugins/sentry-capture.ts:
--------------------------------------------------------------------------------
1 | import { IPlugin } from '../intf'
2 |
3 | export interface ISentryOptions {
4 | /** sentry 实例 */
5 | sentry: {
6 | captureException(exception: any): ReturnType
7 | }
8 | }
9 |
10 | /**
11 | * 插件: sentry 错误请求日志上报
12 | *
13 | * @description 提供 sentry 捕获请求异常并上报的简单实现.
14 | */
15 | export const sentryCapture = (options: ISentryOptions): IPlugin => {
16 | return {
17 | name: 'sentry-capture',
18 | lifecycle: {
19 | captureException: (reason) => {
20 | if (options?.sentry?.captureException) options.sentry.captureException(reason)
21 | return reason
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/plugins/sign.ts:
--------------------------------------------------------------------------------
1 | import { IPlugin } from '../intf'
2 | import { klona } from 'klona/json'
3 | import { stringify } from 'qs'
4 | import { MD5 } from 'crypto-js'
5 |
6 | export interface IData {
7 | [key: string]: any
8 | }
9 |
10 | /** 插件参数声明 */
11 | export interface ISignOptions {
12 | /** 签名字段
13 | *
14 | * @default 'sign'
15 | */
16 | key?: 'sign' | 'signature' | string
17 |
18 | /**
19 | * 签名算法
20 | *
21 | * @default 'md5'
22 | */
23 | algorithm?: 'md5' | ((str: string) => string)
24 |
25 | /**
26 | * 自定义参数排序规则
27 | *
28 | * @default {true}
29 | */
30 | sort?: boolean | ((key1: string, key2: string) => number)
31 |
32 | /**
33 | * 过滤空值
34 | *
35 | * @default {true}
36 | */
37 | filter?: boolean | ((key: string, value: any) => boolean)
38 |
39 | /** 加盐
40 | *
41 | * @description 加盐操作, 在参数排序后进行, 默认附加在参数最后一位. 如果无法满足需求, 可以在 getData 中, 自行实现加盐操作.
42 |
43 | * @type {string} 序列化后加盐, 格式: `params1=xxx¶ms2=xxx${salt}`
44 | * @type {{ [key: string]: any }} 排序后, 在 data 中, 添加盐值字段
45 | */
46 | salt?: string | { [key: string]: any }
47 |
48 | /**
49 | * 参数序列化
50 | * @default 默认使用 `qs.stringify` 实现参数序列化
51 | */
52 | serialize?: (data: { [key: string]: any }) => string
53 | }
54 |
55 | /**
56 | * 插件: 请求签名
57 | *
58 | * @description 提供请求防篡改能力, 这个功能需要搭配后端逻辑实现
59 | *
60 | * 注:
61 | * - 需要手工添加到所有插件的末尾, 避免后续其他修改导致签名不一致.
62 | * - 这个插件实现为初稿, 如果无法满足需要, 可以给我提 Issue
63 | */
64 | export const sign = (options: ISignOptions = {}): IPlugin => {
65 | return {
66 | name: 'sign',
67 | enforce: 'post',
68 | lifecycle: {
69 | async transformRequest(config) {
70 | // 序列化后的请求参数
71 | let serializedStr: string
72 | // 拷贝待结算的参数对象
73 | const data = klona(config.data)
74 |
75 | let entries = Object.entries(data)
76 | // 排序
77 | if (options.sort !== false) {
78 | entries = entries.sort(([a], [b]) => {
79 | if (options.sort) return (options.sort as Function)(a, b)
80 | else return a.localeCompare(b)
81 | })
82 | }
83 | // 过滤空值
84 | if (options.filter !== false) {
85 | entries = entries.filter(([key, value]) => {
86 | if (typeof options.filter === 'function') {
87 | return options.filter(key, value)
88 | } else {
89 | return value !== null && value !== undefined && `${value}`.trim() === ''
90 | }
91 | })
92 | }
93 | // 重新转化为 Object
94 | let obj = entries.reduce((o: { [key: string]: any }, [key, value]) => {
95 | o[key] = value
96 | return o
97 | }, {})
98 |
99 | // 加盐 & 序列化
100 | if (typeof options.salt === 'object') {
101 | Object.assign(obj, options.salt)
102 | serializedStr = stringify(data, { arrayFormat: 'brackets' })
103 | } else if (typeof options.salt === 'string') {
104 | serializedStr = stringify(data, { arrayFormat: 'brackets' }) + options.salt
105 | } else {
106 | serializedStr = stringify(data, { arrayFormat: 'brackets' })
107 | }
108 |
109 | // 计算签名
110 | const sign: string =
111 | typeof options.algorithm === 'function'
112 | ? options.algorithm(serializedStr)
113 | : MD5(serializedStr).toString()
114 |
115 | // 添加到 data
116 | config.data[options.key ?? 'sign'] = sign
117 |
118 | return config
119 | }
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/plugins/throttle.ts:
--------------------------------------------------------------------------------
1 | import { AxiosRequestConfig } from 'axios'
2 |
3 | import { IHooksShareOptions, IPlugin, ISharedCache } from '../intf'
4 | import { defaultCalcRequestHash as crh } from '../utils/calc-hash'
5 | import { createOrGetCache } from '../utils/create-cache'
6 | import { createUrlFilter, Filter, FilterPattern } from '../utils/create-filter'
7 | import { delay } from '../utils/delay'
8 |
9 | declare module 'axios' {
10 | interface CreateAxiosDefaults {
11 | /** 配置节流策略 */
12 | throttle?: IThrottleOptions
13 | }
14 |
15 | interface AxiosRequestConfig {
16 | /**
17 | * 配置是否触发防抖策略
18 | *
19 | * @description 在一段时间内发起的重复请求, 后执行的请求将等待上次请求完成后再执行
20 | *
21 | * - 需要注册 `debounce()` 插件
22 | * - 不建议与 `merge`, `throttle` 插件同时使用
23 | */
24 | throttle?: boolean | Pick
25 | }
26 | }
27 |
28 | /** 节流异常类型 */
29 | export class ThrottleError extends Error {
30 | type: string = 'throttle'
31 | }
32 |
33 | /** 节流请求的放弃规则 */
34 | export enum GiveUpRule {
35 | /** 抛出异常 */
36 | throw = 'throw',
37 | /** 中断请求, 并返回空值结果 */
38 | cancel = 'cancel',
39 | /** 静默, 既不返回成功、也不抛出异常 */
40 | silent = 'silent'
41 | }
42 |
43 | /** 插件参数类型 */
44 | export interface IThrottleOptions {
45 | /**
46 | * 指定哪些接口包含
47 | *
48 | * @description 未指定情况下, 所有接口均包含节流逻辑
49 | */
50 | includes?: FilterPattern
51 |
52 | /**
53 | * 指定哪些接口应忽略
54 | */
55 | excludes?: FilterPattern
56 |
57 | /**
58 | * 延迟判定时间
59 | *
60 | * @description 当设置此值时, 在请求完成后 n 秒内发起的请求都属于重复请求
61 | * @default 0ms
62 | */
63 | delay?: number
64 |
65 | /** 自定义: 计算请求 hash 值
66 | *
67 | * @description 定制重复请求检查方法, 当请求hash值相同时, 判定两个请求为重复请求.
68 | * @default ``` f(url, data, params) => hash ```
69 | */
70 | calcRequstHash?: (config: AxiosRequestConfig) => string
71 |
72 | /**
73 | * 遇到重复请求的抛弃逻辑
74 | *
75 | * @default `throw` 抛出异常
76 | */
77 | giveUp?: GiveUpRule
78 |
79 | /** 自定义触发节流异常的错误消息 */
80 | throttleErrorMessage?: string | ((config: AxiosRequestConfig) => string)
81 | }
82 |
83 | interface ISharedThrottleCache extends ISharedCache {
84 | throttle: {
85 | [hash: string]: boolean
86 | }
87 | }
88 |
89 | /**
90 | * 插件: 节流
91 | *
92 | * @description 在一段时间内发起的重复请求, 后执行的请求将被抛弃.
93 | */
94 | export const throttle = (options: IThrottleOptions = {}): IPlugin => {
95 | /** 触发检查 */
96 | const runWhen = (_: V, { origin }: IHooksShareOptions): boolean => {
97 | if (origin['throttle']) {
98 | return !!origin['throttle']
99 | } else {
100 | const filter: Filter = createUrlFilter(options.includes, options.excludes)
101 | return filter(origin.url)
102 | }
103 | }
104 | return {
105 | name: 'throttle',
106 | beforeRegister(axios) {
107 | // 参数合并
108 | options = Object.assign({ calcRequstHash: crh }, options, axios.defaults['throttle'])
109 | },
110 | lifecycle: {
111 | preRequestTransform: {
112 | runWhen,
113 | handler: async (config, { origin, shared }, { abort, abortError, slient }) => {
114 | // @ 计算请求hash
115 | const hash: string = options.calcRequstHash!(origin)
116 | // @ 从共享内存中创建或获取缓存对象
117 | const cache: ISharedThrottleCache['throttle'] = createOrGetCache(shared, 'throttle')
118 | // ? 判断是否重复请求
119 | if (cache[hash]) {
120 | let message!: string
121 | let giveUp: GiveUpRule | undefined =
122 | (config.throttle as IThrottleOptions)?.giveUp ?? options.giveUp
123 | let throttleErrorMessage: IThrottleOptions['throttleErrorMessage'] =
124 | (config.throttle as IThrottleOptions)?.throttleErrorMessage ?? options.throttleErrorMessage
125 | // ! 触发 `abort`, `abortError`, `slient` 时, 将抛出相应异常, 中止请求
126 | switch (giveUp) {
127 | case GiveUpRule.silent:
128 | slient()
129 | break
130 | case GiveUpRule.cancel:
131 | abort()
132 | break
133 | case GiveUpRule.throw:
134 | default:
135 | // > 默认情况下, 抛出节流异常
136 | if (throttleErrorMessage) {
137 | if (typeof throttleErrorMessage === 'function') {
138 | message = throttleErrorMessage(config)
139 | } else {
140 | message = throttleErrorMessage
141 | }
142 | } else {
143 | message = `'${config.url}' 触发了节流规则, 请求被中止`
144 | }
145 | abortError(new ThrottleError(message))
146 | break
147 | }
148 | } else {
149 | // 创建重复请求缓存
150 | cache[hash] = true
151 | }
152 | return config
153 | }
154 | },
155 | completed: {
156 | runWhen: (options) => {
157 | return runWhen(undefined, options)
158 | },
159 | handler: async ({ origin, shared }) => {
160 | // @ 计算请求hash
161 | const hash: string = options.calcRequstHash!(origin)
162 | // @ 从共享内存中创建或获取缓存对象
163 | const cache: ISharedThrottleCache['throttle'] = createOrGetCache(shared, 'throttle')
164 | // ? 如果配置了延时函数, 那么执行延时等待
165 | if (options.delay && options.delay > 0) {
166 | await delay(options.delay)
167 | }
168 | delete cache[hash]
169 | return void 0
170 | }
171 | }
172 | }
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/src/plugins/transform.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
2 |
3 | import { ILifecycleHook, IPlugin } from '../intf'
4 |
5 | /** 插件参数类型 */
6 | export interface ITransformOptions {
7 | request?: ILifecycleHook
8 | response?: ILifecycleHook
9 | capture?: ILifecycleHook
10 | }
11 |
12 | /**
13 | * 插件: 转换请求/响应/异常处理
14 | *
15 | * @description 替代`axios.interceptors`的使用, 用于统一管理 axios 请求过程
16 | */
17 | export const transform = (options: ITransformOptions = {}): IPlugin => {
18 | return {
19 | name: 'transform',
20 | lifecycle: {
21 | transformRequest: options.request,
22 | postResponseTransform: options.response,
23 | captureException: options.capture
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/use-plugin.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type AxiosInstance,
3 | type AxiosResponse,
4 | type AxiosRequestConfig,
5 | type AxiosInterceptorManager,
6 | type InternalAxiosRequestConfig,
7 | Axios,
8 | AxiosDefaults
9 | } from 'axios'
10 | import type {
11 | AxiosInstanceExtension,
12 | IHooksShareOptions,
13 | ILifecycleHookObject,
14 | IPlugin,
15 | IPluginLifecycle,
16 | ISharedCache,
17 | IUseAxiosPluginResult
18 | } from './intf'
19 | import { klona } from 'klona/json'
20 | import { AbortChainController, AbortError, SlientError, createAbortChain } from './utils/create-abort-chain'
21 |
22 | /**
23 | * Axios 实例扩展
24 | *
25 | * @description
26 | * 由于 axios 实例由 `function wrap()` 包裹, 无法直接修改对象属性。
27 | * 所有这里想了一个hacker的方法, 通过继承的方式, 扩展 Axios 类, 然后通过 `Object.defineProperties` 映射当前axios实例到插件扩展的实例上,
28 | * 从而实现扩展 axios 属性的效果
29 | */
30 | class AxiosExtension extends Axios {
31 | /** 添加的插件集合 */
32 | __plugins__: Array = []
33 | /** 插件共享内存空间 */
34 | __shared__: ISharedCache = {}
35 |
36 | constructor(
37 | config: AxiosDefaults,
38 | interceptors: {
39 | request: AxiosInterceptorManager
40 | response: AxiosInterceptorManager
41 | }
42 | ) {
43 | super(config as AxiosRequestConfig)
44 | // 继承原实例的适配器
45 | this.interceptors = interceptors
46 | // 覆盖(扩展) `request` 成员方法
47 | const originRequest = this.request
48 | const vm = this
49 |
50 | /** 获取钩子函数 */
51 | const getHook = (hookName: K): Array> => {
52 | return this.__plugins__
53 | .map((plug) => {
54 | const hook: IPluginLifecycle[K] | undefined = plug.lifecycle?.[hookName]
55 | if (typeof hook === 'function') {
56 | return {
57 | runWhen: () => true,
58 | handler: hook
59 | } as ILifecycleHookObject
60 | } else if (hook) {
61 | return hook as ILifecycleHookObject
62 | }
63 | })
64 | .filter((hook) => !!hook) as Array>
65 | }
66 |
67 | /** 是否存在钩子 */
68 | const hasHook = (hookName: K): boolean => {
69 | return getHook(hookName).length > 0
70 | }
71 |
72 | /**
73 | * 触发钩子函数
74 | * @description 遵循先进先出原则触发插件钩子
75 | */
76 | const runHook = async (
77 | hookName: K,
78 | reverse: boolean,
79 | arg1: T,
80 | arg2: unknown,
81 | arg3: AbortChainController
82 | ): Promise => {
83 | let hooks = reverse ? getHook(hookName).reverse() : getHook(hookName)
84 | for (const hook of hooks) {
85 | if (hook.runWhen.call(hook.runWhen, arg1, arg2 as IHooksShareOptions)) {
86 | arg1 = await hook.handler.call(
87 | hook,
88 | // @ts-ignore
89 | ...((arg2 as IHooksShareOptions) ? [arg1, arg2, arg3] : [arg1, arg3])
90 | )
91 | }
92 | }
93 | return arg1
94 | }
95 |
96 | // 包装 request
97 | this.request = async function , D = any>(config: AxiosRequestConfig) {
98 | const origin: AxiosRequestConfig = klona(config)
99 | const share: IHooksShareOptions = { origin, shared: this.__shared__, axios: vm as unknown as AxiosInstance }
100 | return await createAbortChain(config)
101 | .next((config, controller) => runHook('preRequestTransform', false, config, share, controller))
102 | .next(async (config) => await (>originRequest.call(vm, config)))
103 | .next((response, controller) => runHook('postResponseTransform', true, response, share, controller))
104 | .capture(async (e, controller) => {
105 | // ? 如果添加了捕获异常钩子, 那么当钩子函数 `return void` 时, 将返回用户原始响应信息
106 | // 否则应通过 `throw error` 直接抛出异常或 `return error` 触发下一个 `captureException` 钩子
107 | if (hasHook('captureException')) {
108 | // # 添加捕获异常钩子 (运行后直接抛出异常)
109 | return await runHook('captureException', true, e, share, controller)
110 | } else {
111 | throw e
112 | }
113 | })
114 | .completed(
115 | (controller) => runHook('completed', true, share, undefined, controller) as unknown as Promise
116 | )
117 | .abort((reason) => runHook('aborted', true, reason, share, undefined as any))
118 | .done()
119 | }
120 |
121 | // > 添加请求拦截器
122 | this.interceptors.request.use((config) => {
123 | return runHook('transformRequest', false, config, this.__shared__, {
124 | abort(res: any) {
125 | throw new AbortError({ success: true, res })
126 | },
127 | abortError(reason: any) {
128 | throw new AbortError({ success: false, res: reason })
129 | },
130 | slient() {
131 | throw new SlientError()
132 | }
133 | })
134 | })
135 | }
136 | }
137 |
138 | /** 定义忽略映射的类属性 */
139 | export const IGNORE_COVERAGE: ReadonlyArray = ['prototype']
140 |
141 | /** 向 axios 实例注入插件生命周期钩子 */
142 | const injectPluginHooks = (axios: AxiosInstanceExtension): void => {
143 | // ? 如果 axios 实例已经调用了 `useAxiosPlugin()`, 那么不需要重复注入
144 | if (axios['__plugins__']) {
145 | return
146 | }
147 | // @ 实例化扩展类
148 | const extension: AxiosExtension = new AxiosExtension(axios.defaults, axios.interceptors)
149 | // > 通过 `defineProperties` 将当前实例的请求映射到扩展类的方法上, 从而实现扩展的效果
150 | const properties = Object.getOwnPropertyNames(axios)
151 | .concat(['__shared__', '__plugins__'])
152 | .filter((prop: string) => extension[prop as unknown as keyof AxiosExtension] && !IGNORE_COVERAGE.includes(prop))
153 | .reduce((properties: PropertyDescriptorMap, prop: string) => {
154 | properties[prop] = {
155 | get() {
156 | return extension[prop as unknown as keyof AxiosExtension]
157 | },
158 | set(v) {
159 | extension[prop as unknown as keyof AxiosExtension] = v
160 | }
161 | }
162 | return properties
163 | }, {})
164 | // 映射
165 | Object.defineProperties(axios, properties)
166 | }
167 |
168 | /** 插件能力注入 */
169 | const injectPlugin = (axios: AxiosInstanceExtension, plug: IPlugin): void => {
170 | // @ 插件排序
171 | const soryByEnforce = (plugins: Array): Array => {
172 | return plugins.sort((a, b): number => {
173 | if (a.enforce === 'pre' || b.enforce === 'post') {
174 | return -1
175 | } else if (a.enforce === 'post' || b.enforce === 'pre') {
176 | return 1
177 | } else {
178 | return 0
179 | }
180 | })
181 | }
182 | // ? 补全插件必要属性
183 | if (!plug.lifecycle) plug.lifecycle = {}
184 | // > 挂载插件
185 | if (axios.__plugins__) {
186 | // > 添加插件
187 | axios.__plugins__.push(plug)
188 | // > 排序
189 | axios.__plugins__ = soryByEnforce(axios.__plugins__)
190 | }
191 | }
192 |
193 | /**
194 | * 使用 axios 扩展插件
195 | *
196 | * @description 通过链式调用方式, 为 `axios` 扩展插件支持.
197 | */
198 | export const useAxiosPlugin = (axios: AxiosInstance): IUseAxiosPluginResult => {
199 | // > 注入插件钩子
200 | injectPluginHooks(axios as AxiosInstanceExtension)
201 |
202 | // > 返回函数链
203 | return {
204 | /** 添加新插件 */
205 | plugin(plug: IPlugin) {
206 | // > 注册插件前检查 (如果需要)
207 | plug.beforeRegister?.(axios as AxiosInstanceExtension)
208 | // > 挂载插件
209 | injectPlugin(axios as AxiosInstanceExtension, plug)
210 | return this
211 | },
212 | /**
213 | * 包装 `axios({ ... })`
214 | *
215 | * @description 使 `axiox({ ... })` 具备插件能力
216 | */
217 | wrap() {
218 | return new Proxy(axios, {
219 | apply(_target, _thisArg, args: Array) {
220 | return axios.request.call(axios, args[0])
221 | }
222 | })
223 | }
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/src/utils/calc-hash.ts:
--------------------------------------------------------------------------------
1 | import { AxiosRequestConfig } from 'axios'
2 | import { MD5 } from 'crypto-js'
3 | /**
4 | * 计算对象hash值
5 | *
6 | * @description 借助浏览器内置的 `crypto` 库实现, 如果存在兼容性问题, 那么这里可能需要添加 polyfill
7 | */
8 | export const calcHash = (obj: Object): string => {
9 | const data = JSON.stringify(obj)
10 | return MD5(data).toString()
11 | }
12 |
13 | export type TCalcRequestHsah = (config: AxiosRequestConfig) => string
14 |
15 | /** 计算请求hash值方法 */
16 | export const defaultCalcRequestHash: TCalcRequestHsah = (config) => {
17 | const { url, data, params } = config
18 | return calcHash({ url, data, params })
19 | }
20 |
--------------------------------------------------------------------------------
/src/utils/create-abort-chain.ts:
--------------------------------------------------------------------------------
1 | export interface AbortChainController {
2 | /** 中断 */
3 | abort(res?: any): void
4 | /** 中断并抛出异常 */
5 | abortError(reason: any): void
6 | /** 静默 */
7 | slient: () => Promise
8 | }
9 |
10 | interface Chain {
11 | /**
12 | * 下一步执行什么
13 | */
14 | next: (
15 | onNext: (value: T, controller: AbortChainController) => TResult | PromiseLike
16 | ) => Chain
17 | /** 任务完成后执行 */
18 | completed: (onCompleted: (controller: AbortChainController) => void | PromiseLike) => Chain
19 | /** 任务中断后执行 */
20 | abort: (onAbort: (reason: AbortError) => AbortError | PromiseLike) => Chain
21 | /** 捕获异常 */
22 | capture: (
23 | onCapture: (reason: any, controller: AbortChainController) => TResult | PromiseLike
24 | ) => Chain
25 | /** 标记链任务添加完成, 并开始执行 */
26 | done(): Promise
27 | }
28 |
29 | export class AbortError extends Error {
30 | type: string = 'abort'
31 | abort!: { success: boolean; res: any }
32 | constructor(abort: { success: boolean; res: any }) {
33 | super()
34 | this.abort = abort
35 | }
36 | }
37 | export class SlientError extends Error {
38 | type: string = 'slient'
39 | }
40 |
41 | /** 创建可中止的链式调用 */
42 | export const createAbortChain = (initial?: T): Chain => {
43 | type TResult = any
44 | /** 任务链 */
45 | const chain: Array<(value: T, controller: AbortChainController) => TResult | PromiseLike> = []
46 |
47 | /** 链控制器
48 | *
49 | * @description 借助 throw error 传递数据
50 | */
51 | const controller: AbortChainController = {
52 | abort(res: any) {
53 | throw new AbortError({ success: true, res })
54 | },
55 | abortError(reason: any) {
56 | throw new AbortError({ success: false, res: reason })
57 | },
58 | slient() {
59 | throw new SlientError()
60 | }
61 | }
62 |
63 | let onCapture: ((reason: any, controller: AbortChainController) => TResult | PromiseLike) | undefined
64 | let onCompleted: ((controller: AbortChainController) => void | PromiseLike) | undefined
65 | let onAbort: ((reason: AbortError) => TResult | PromiseLike) | undefined
66 | let res: T = initial as unknown as T
67 |
68 | return {
69 | /** 下一任务 */
70 | next(event) {
71 | chain.push(event)
72 | return this as Chain
73 | },
74 | /** 捕获异常后触发 */
75 | capture(event) {
76 | if (onCapture) {
77 | throw new Error('`onCapture` is registered')
78 | }
79 | onCapture = event
80 | return this as Chain
81 | },
82 | /** 执行完成后触发 */
83 | completed(event) {
84 | if (onCompleted) {
85 | throw new Error('`onCompleted` is registered')
86 | }
87 | onCompleted = event
88 | return this as Chain
89 | },
90 | /**
91 | * 执行中断后触发
92 | *
93 | * @description 增加 `abort` 以解决触发 `Abort` 后造成的后续请求阻塞. (主要体现在: merge 插件)
94 | */
95 | abort(event) {
96 | if (onAbort) {
97 | throw new Error('`onAbort` is registered')
98 | }
99 | onAbort = event
100 | return this as Chain
101 | },
102 | /** 停止添加并执行 */
103 | async done(): Promise {
104 | /** 包装任务执行过程 */
105 | const run = async (): Promise => {
106 | let abortRes: AbortError | undefined
107 | try {
108 | // > loop run next task
109 | for (const task of chain) {
110 | res = await task(res, controller)
111 | }
112 | return res
113 | } catch (reason) {
114 | if (reason instanceof AbortError) {
115 | abortRes = reason
116 | }
117 | if (onCapture && !(reason instanceof AbortError || reason instanceof SlientError)) {
118 | // emit `capture` hook
119 | return await onCapture(reason, controller)
120 | } else {
121 | throw reason
122 | }
123 | } finally {
124 | // emit `completed` hook
125 | if (onCompleted) await onCompleted(controller)
126 | if (!!abortRes && onAbort) await onAbort(abortRes)
127 | }
128 | }
129 | try {
130 | return await run()
131 | } catch (reason) {
132 | // ? 处理 Abort、AbortError
133 | if (reason instanceof AbortError) {
134 | if (reason.abort.success) {
135 | return reason.abort.res
136 | } else {
137 | throw reason.abort.res
138 | }
139 | } else if (reason instanceof SlientError) {
140 | return new Promise(() => {})
141 | } else {
142 | throw reason
143 | }
144 | }
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/utils/create-cache.ts:
--------------------------------------------------------------------------------
1 | import { ISharedCache } from '../intf'
2 |
3 | /** 从共享内存中, 创建或获取缓存 */
4 | export const createOrGetCache = (
5 | shared: T,
6 | key: K,
7 | initial?: T[K]
8 | ): R => {
9 | if (!shared[key]) {
10 | shared[key] = initial ?? ({} as T[K])
11 | }
12 | return shared[key]
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/create-filter.ts:
--------------------------------------------------------------------------------
1 | type Matcher = { test: (url: string) => boolean }
2 |
3 | export type Filter = (url?: string) => boolean
4 |
5 | export type FilterPatternType = string | boolean | RegExp | ((url: string) => boolean) | null | undefined
6 | export type FilterPattern = ReadonlyArray | FilterPatternType
7 |
8 | const getMatchers = (fp: FilterPattern): Array => {
9 | fp = fp instanceof Array ? fp : [fp]
10 | return fp
11 | .filter((rule) => rule !== undefined && rule !== null)
12 | .map((rule) => {
13 | if (rule instanceof RegExp) {
14 | return rule
15 | } else if (typeof rule === 'function') {
16 | return { test: rule }
17 | } else if (typeof rule === 'string') {
18 | // match id in url
19 | return { test: (url: string) => url.includes(rule) }
20 | } else if (typeof rule === 'boolean') {
21 | return { test: () => rule }
22 | } else {
23 | throw new TypeError('请检查 `includes`, `excludes` 配置')
24 | }
25 | })
26 | }
27 |
28 | /** 创建简易的url过滤器 */
29 | export const createUrlFilter = (include?: FilterPattern, exclude?: FilterPattern): Filter => {
30 | if (include === undefined && exclude === undefined) include = true
31 | const includeMatchers = getMatchers(include)
32 | const excludeMatchers = getMatchers(exclude)
33 |
34 | return (url?: string): boolean => {
35 | if (!url) return false
36 | // ? 判断是否排除
37 | for (const matcher of excludeMatchers) {
38 | if (matcher.test(url)) return false
39 | }
40 | // ? 判断是否包含
41 | for (const matcher of includeMatchers) {
42 | if (matcher.test(url)) {
43 | return true
44 | }
45 | }
46 | return false
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/utils/delay.ts:
--------------------------------------------------------------------------------
1 | /** 延时函数 */
2 | export const delay = (time: number = 0): Promise => {
3 | return new Promise((resolve) => {
4 | setTimeout(() => resolve(), time)
5 | })
6 | }
7 |
8 | /** 获取延时时间 */
9 | export const getDelayTime = (def: number, ...args: Array): number => {
10 | for (const o of args) {
11 | if (typeof o === 'number') return o
12 | else if (typeof o === 'object' && typeof o.delay === 'number') return o.delay
13 | }
14 | return def
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/url.ts:
--------------------------------------------------------------------------------
1 | export const isAbsoluteURL = (url: string): boolean => {
2 | // A URL is considered absolute if it begins with "://" or "//" (protocol-relative URL).
3 | // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed
4 | // by any combination of letters, digits, plus, period, or hyphen.
5 | return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url)
6 | }
7 | export const combineURLs = (baseURL: string, relativeURL: string): string => {
8 | return relativeURL ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '') : baseURL
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "Bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 |
22 | "declaration": true,
23 | "declarationDir": "dist/typings/"
24 | },
25 | "include": ["src/**/*.ts"]
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 | "types": ["node", "jest"],
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "Bundler",
12 | "allowImportingTsExtensions": true,
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true,
22 |
23 | "declaration": true,
24 | "declarationDir": "dist/typings/"
25 | },
26 | "include": ["jest.config.ts", "__tests__/**/*.ts"]
27 | }
28 |
--------------------------------------------------------------------------------