From 98b2d2a9722afdc950424282ba8a4a05f49fd479 Mon Sep 17 00:00:00 2001 From: Netfan Date: Sat, 29 Mar 2025 19:21:21 +0800 Subject: [PATCH] feat: pre-set serialization methods for request parameters (#5814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加快捷设置请求参数序列化方法的配置 --- docs/src/guide/essentials/server.md | 30 +++++++++ packages/effects/request/package.json | 4 +- .../src/request-client/request-client.ts | 32 +++++++++- .../request/src/request-client/types.ts | 26 ++++++-- playground/src/api/examples/params.ts | 19 ++++++ playground/src/locales/langs/zh-CN/demos.json | 3 +- playground/src/router/routes/modules/demos.ts | 12 ++++ .../request-params-serializer/index.vue | 61 +++++++++++++++++++ pnpm-lock.yaml | 26 +------- pnpm-workspace.yaml | 6 +- 10 files changed, 181 insertions(+), 38 deletions(-) create mode 100644 playground/src/api/examples/params.ts create mode 100644 playground/src/views/demos/features/request-params-serializer/index.vue diff --git a/docs/src/guide/essentials/server.md b/docs/src/guide/essentials/server.md index fedfbae2..9a494967 100644 --- a/docs/src/guide/essentials/server.md +++ b/docs/src/guide/essentials/server.md @@ -110,6 +110,36 @@ VITE_GLOB_API_URL=https://mock-napi.vben.pro/api 项目中默认自带了基于 `axios` 封装的基础的请求配置,核心由 `@vben/request` 包提供。项目没有过多的封装,只是简单的封装了一些常用的配置,如有其他需求,可以自行增加或者调整配置。针对不同的app,可能是用到了不同的组件库以及`store`,所以在应用目录下的`src/api/request.ts`文件夹下,有对应的请求配置文件,如`web-antd`项目下的`src/api/request.ts`文件,可以根据自己的需求进行配置。 +### 扩展的配置 + +除了基础的Axios配置外,扩展了部分配置。 + +```ts +type ExtendOptions = { + /** + * 参数序列化方式。预置了几种针对数组的序列化类型 + * - brackets: ids[]=1&ids[]=2&ids[]=3 + * - comma: ids=1,2,3 + * - indices: ids[0]=1&ids[1]=2&ids[2]=3 + * - repeat: ids=1&ids=2&ids=3 + * @default 'brackets' + */ + paramsSerializer?: + | 'brackets' + | 'comma' + | 'indices' + | 'repeat' + | AxiosRequestConfig['paramsSerializer']; + /** + * 响应数据的返回方式。 + * - raw: 原始的AxiosResponse,包括headers、status等,不做是否成功请求的检查。 + * - body: 返回响应数据的BODY部分(只会根据status检查请求是否成功,忽略对code的判断,这种情况下应由调用方检查请求是否成功)。 + * - data: 解构响应的BODY数据,只返回其中的data节点数据(会检查status和code是否为成功状态)。 + */ + responseReturn?: 'body' | 'data' | 'raw'; +}; +``` + ### 请求示例 #### GET 请求 diff --git a/packages/effects/request/package.json b/packages/effects/request/package.json index c5b4c6b8..18c36e22 100644 --- a/packages/effects/request/package.json +++ b/packages/effects/request/package.json @@ -26,7 +26,7 @@ "qs": "catalog:" }, "devDependencies": { - "axios-mock-adapter": "catalog:", - "@types/qs": "catalog:" + "@types/qs": "catalog:", + "axios-mock-adapter": "catalog:" } } diff --git a/packages/effects/request/src/request-client/request-client.ts b/packages/effects/request/src/request-client/request-client.ts index dc32a460..cc7aebc7 100644 --- a/packages/effects/request/src/request-client/request-client.ts +++ b/packages/effects/request/src/request-client/request-client.ts @@ -2,7 +2,7 @@ import type { AxiosInstance, AxiosResponse } from 'axios'; import type { RequestClientConfig, RequestClientOptions } from './types'; -import { bindMethods, merge } from '@vben/utils'; +import { bindMethods, isString, merge } from '@vben/utils'; import axios from 'axios'; import qs from 'qs'; @@ -11,6 +11,30 @@ import { FileDownloader } from './modules/downloader'; import { InterceptorManager } from './modules/interceptor'; import { FileUploader } from './modules/uploader'; +function getParamsSerializer( + paramsSerializer: RequestClientOptions['paramsSerializer'], +) { + if (isString(paramsSerializer)) { + switch (paramsSerializer) { + case 'brackets': { + return (params: any) => + qs.stringify(params, { arrayFormat: 'brackets' }); + } + case 'comma': { + return (params: any) => qs.stringify(params, { arrayFormat: 'comma' }); + } + case 'indices': { + return (params: any) => + qs.stringify(params, { arrayFormat: 'indices' }); + } + case 'repeat': { + return (params: any) => qs.stringify(params, { arrayFormat: 'repeat' }); + } + } + } + return paramsSerializer; +} + class RequestClient { public addRequestInterceptor: InterceptorManager['addRequestInterceptor']; @@ -44,6 +68,9 @@ class RequestClient { }; const { ...axiosConfig } = options; const requestConfig = merge(axiosConfig, defaultConfig); + requestConfig.paramsSerializer = getParamsSerializer( + requestConfig.paramsSerializer, + ); this.instance = axios.create(requestConfig); bindMethods(this); @@ -113,6 +140,9 @@ class RequestClient { const response: AxiosResponse = await this.instance({ url, ...config, + ...(config.paramsSerializer + ? { paramsSerializer: getParamsSerializer(config.paramsSerializer) } + : {}), }); return response as T; } catch (error: any) { diff --git a/packages/effects/request/src/request-client/types.ts b/packages/effects/request/src/request-client/types.ts index d6560062..ba684607 100644 --- a/packages/effects/request/src/request-client/types.ts +++ b/packages/effects/request/src/request-client/types.ts @@ -5,15 +5,29 @@ import type { InternalAxiosRequestConfig, } from 'axios'; -type ExtendOptions = { - /** 响应数据的返回方式。 - * raw: 原始的AxiosResponse,包括headers、status等,不做是否成功请求的检查。 - * body: 返回响应数据的BODY部分(只会根据status检查请求是否成功,忽略对code的判断,这种情况下应由调用方检查请求是否成功)。 - * data: 解构响应的BODY数据,只返回其中的data节点数据(会检查status和code是否为成功状态)。 +type ExtendOptions = { + /** + * 参数序列化方式。预置的有 + * - brackets: ids[]=1&ids[]=2&ids[]=3 + * - comma: ids=1,2,3 + * - indices: ids[0]=1&ids[1]=2&ids[2]=3 + * - repeat: ids=1&ids=2&ids=3 + */ + paramsSerializer?: + | 'brackets' + | 'comma' + | 'indices' + | 'repeat' + | AxiosRequestConfig['paramsSerializer']; + /** + * 响应数据的返回方式。 + * - raw: 原始的AxiosResponse,包括headers、status等,不做是否成功请求的检查。 + * - body: 返回响应数据的BODY部分(只会根据status检查请求是否成功,忽略对code的判断,这种情况下应由调用方检查请求是否成功)。 + * - data: 解构响应的BODY数据,只返回其中的data节点数据(会检查status和code是否为成功状态)。 */ responseReturn?: 'body' | 'data' | 'raw'; }; -type RequestClientConfig = AxiosRequestConfig & ExtendOptions; +type RequestClientConfig = AxiosRequestConfig & ExtendOptions; type RequestResponse = AxiosResponse & { config: RequestClientConfig; diff --git a/playground/src/api/examples/params.ts b/playground/src/api/examples/params.ts new file mode 100644 index 00000000..6568ec64 --- /dev/null +++ b/playground/src/api/examples/params.ts @@ -0,0 +1,19 @@ +import type { Recordable } from '@vben/types'; + +import { requestClient } from '#/api/request'; + +/** + * 发起数组请求 + */ +async function getParamsData( + params: Recordable, + type: 'brackets' | 'comma' | 'indices' | 'repeat', +) { + return requestClient.get('/status', { + params, + paramsSerializer: type, + responseReturn: 'raw', + }); +} + +export { getParamsData }; diff --git a/playground/src/locales/langs/zh-CN/demos.json b/playground/src/locales/langs/zh-CN/demos.json index 254e072b..5cd87ce5 100644 --- a/playground/src/locales/langs/zh-CN/demos.json +++ b/playground/src/locales/langs/zh-CN/demos.json @@ -50,7 +50,8 @@ "clipboard": "剪贴板", "menuWithQuery": "带参菜单", "openInNewWindow": "新窗口打开", - "fileDownload": "文件下载" + "fileDownload": "文件下载", + "requestParamsSerializer": "参数序列化" }, "breadcrumb": { "navigation": "面包屑导航", diff --git a/playground/src/router/routes/modules/demos.ts b/playground/src/router/routes/modules/demos.ts index 955bfe92..7b927373 100644 --- a/playground/src/router/routes/modules/demos.ts +++ b/playground/src/router/routes/modules/demos.ts @@ -243,6 +243,18 @@ const routes: RouteRecordRaw[] = [ title: 'Tanstack Query', }, }, + { + name: 'RequestParamsSerializerDemo', + path: '/demos/features/request-params-serializer', + component: () => + import( + '#/views/demos/features/request-params-serializer/index.vue' + ), + meta: { + icon: 'lucide:git-pull-request-arrow', + title: $t('demos.features.requestParamsSerializer'), + }, + }, ], }, // 面包屑导航 diff --git a/playground/src/views/demos/features/request-params-serializer/index.vue b/playground/src/views/demos/features/request-params-serializer/index.vue new file mode 100644 index 00000000..4ed4d08e --- /dev/null +++ b/playground/src/views/demos/features/request-params-serializer/index.vue @@ -0,0 +1,61 @@ + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45d791c5..28668782 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,9 +78,6 @@ catalogs: '@types/archiver': specifier: ^6.0.3 version: 6.0.3 - '@types/crypto-js': - specifier: ^4.2.2 - version: 4.2.2 '@types/eslint': specifier: ^9.6.1 version: 9.6.1 @@ -115,7 +112,7 @@ catalogs: specifier: ^1.5.5 version: 1.5.5 '@types/qs': - specifier: ^6.9.17 + specifier: ^6.9.18 version: 6.9.18 '@types/sortablejs': specifier: ^1.15.8 @@ -192,9 +189,6 @@ catalogs: cross-env: specifier: ^7.0.3 version: 7.0.3 - crypto-js: - specifier: ^4.2.0 - version: 4.2.0 cspell: specifier: ^8.17.5 version: 8.17.5 @@ -379,7 +373,7 @@ catalogs: specifier: ^1.5.4 version: 1.5.4 qs: - specifier: ^6.13.1 + specifier: ^6.14.0 version: 6.14.0 radix-vue: specifier: ^1.9.17 @@ -1554,9 +1548,6 @@ importers: '@vueuse/integrations': specifier: 'catalog:' version: 12.8.2(async-validator@4.2.5)(axios@1.8.2)(change-case@5.4.4)(focus-trap@7.6.4)(nprogress@0.2.0)(qrcode@1.5.4)(sortablejs@1.15.6)(typescript@5.8.2) - crypto-js: - specifier: 'catalog:' - version: 4.2.0 qrcode: specifier: 'catalog:' version: 1.5.4 @@ -1576,9 +1567,6 @@ importers: specifier: 'catalog:' version: 6.6.0(vue@3.5.13(typescript@5.8.2)) devDependencies: - '@types/crypto-js': - specifier: 'catalog:' - version: 4.2.2 '@types/qrcode': specifier: 'catalog:' version: 1.5.5 @@ -4268,9 +4256,6 @@ packages: '@types/conventional-commits-parser@5.0.1': resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==} - '@types/crypto-js@4.2.2': - resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} - '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} @@ -5412,9 +5397,6 @@ packages: crossws@0.3.4: resolution: {integrity: sha512-uj0O1ETYX1Bh6uSgktfPvwDiPYGQ3aI4qVsaC/LWpkIzGj1nUYm5FK3K+t11oOlpN01lGbprFCH4wBlKdJjVgw==} - crypto-js@4.2.0: - resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} - crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} @@ -13110,8 +13092,6 @@ snapshots: dependencies: '@types/node': 22.13.10 - '@types/crypto-js@4.2.2': {} - '@types/doctrine@0.0.9': {} '@types/eslint@9.6.1': @@ -14473,8 +14453,6 @@ snapshots: dependencies: uncrypto: 0.1.3 - crypto-js@4.2.0: {} - crypto-random-string@2.0.0: {} cspell-config-lib@8.17.5: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b8d844aa..f6d84ac2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -50,9 +50,8 @@ catalog: '@types/nprogress': ^0.2.3 '@types/postcss-import': ^14.0.3 '@types/qrcode': ^1.5.5 - '@types/qs': ^6.9.17 + '@types/qs': ^6.9.18 '@types/sortablejs': ^1.15.8 - '@types/crypto-js': ^4.2.2 '@typescript-eslint/eslint-plugin': ^8.26.0 '@typescript-eslint/parser': ^8.26.0 '@vee-validate/zod': ^4.15.0 @@ -79,7 +78,6 @@ catalog: commitlint-plugin-function-rules: ^4.0.1 consola: ^3.4.0 cross-env: ^7.0.3 - crypto-js: ^4.2.0 cspell: ^8.17.5 cssnano: ^7.0.6 cz-git: ^1.11.1 @@ -142,7 +140,7 @@ catalog: prettier-plugin-tailwindcss: ^0.6.11 publint: ^0.2.12 qrcode: ^1.5.4 - qs: ^6.13.1 + qs: ^6.14.0 radix-vue: ^1.9.17 resolve.exports: ^2.0.3 rimraf: ^6.0.1