├── .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 | [![npm version](https://badge.fury.io/js/axios-plugins.svg)](https://badge.fury.io/js/axios-plugins) 2 | [![NPM downloads](https://img.shields.io/npm/dm/axios-plugins.svg?style=flat)](https://npmjs.org/package/axios-plugins) 3 | ![license](https://badgen.net/static/license/MIT/blue) 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 | --------------------------------------------------------------------------------