From 96d2bc52e9820990e3f3e3b20de741429931a9a7 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 | 33 +++++++++- .../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 | 25 ++++++++ pnpm-workspace.yaml | 2 + 10 files changed, 206 insertions(+), 9 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 f35edd52..18c36e22 100644 --- a/packages/effects/request/package.json +++ b/packages/effects/request/package.json @@ -22,9 +22,11 @@ "dependencies": { "@vben/locales": "workspace:*", "@vben/utils": "workspace:*", - "axios": "catalog:" + "axios": "catalog:", + "qs": "catalog:" }, "devDependencies": { + "@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 56412dd8..e5811673 100644 --- a/packages/effects/request/src/request-client/request-client.ts +++ b/packages/effects/request/src/request-client/request-client.ts @@ -2,14 +2,39 @@ 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'; 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']; @@ -39,6 +64,9 @@ class RequestClient { }; const { ...axiosConfig } = options; const requestConfig = merge(axiosConfig, defaultConfig); + requestConfig.paramsSerializer = getParamsSerializer( + requestConfig.paramsSerializer, + ); this.instance = axios.create(requestConfig); bindMethods(this); @@ -108,6 +136,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 1abfd981..494741dc 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 ec1fb451..28668782 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,6 +111,9 @@ catalogs: '@types/qrcode': specifier: ^1.5.5 version: 1.5.5 + '@types/qs': + specifier: ^6.9.18 + version: 6.9.18 '@types/sortablejs': specifier: ^1.15.8 version: 1.15.8 @@ -369,6 +372,9 @@ catalogs: qrcode: specifier: ^1.5.4 version: 1.5.4 + qs: + specifier: ^6.14.0 + version: 6.14.0 radix-vue: specifier: ^1.9.17 version: 1.9.17 @@ -1714,7 +1720,13 @@ importers: axios: specifier: 'catalog:' version: 1.8.2 + qs: + specifier: 'catalog:' + version: 6.14.0 devDependencies: + '@types/qs': + specifier: 'catalog:' + version: 6.9.18 axios-mock-adapter: specifier: 'catalog:' version: 2.1.0(axios@1.8.2) @@ -4331,6 +4343,9 @@ packages: '@types/qrcode@1.5.5': resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==} + '@types/qs@6.9.18': + resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} + '@types/readdir-glob@1.1.5': resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} @@ -8650,6 +8665,10 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + quansync@0.2.8: resolution: {integrity: sha512-4+saucphJMazjt7iOM27mbFCk+D9dd/zmgMDCzRZ8MEoBfYp7lAvoN38et/phRQF6wOPMy/OROBGgoWeSKyluA==} @@ -13162,6 +13181,8 @@ snapshots: dependencies: '@types/node': 22.13.10 + '@types/qs@6.9.18': {} + '@types/readdir-glob@1.1.5': dependencies: '@types/node': 22.13.10 @@ -17951,6 +17972,10 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + quansync@0.2.8: {} queue-microtask@1.2.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 06081dac..f6d84ac2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -50,6 +50,7 @@ catalog: '@types/nprogress': ^0.2.3 '@types/postcss-import': ^14.0.3 '@types/qrcode': ^1.5.5 + '@types/qs': ^6.9.18 '@types/sortablejs': ^1.15.8 '@typescript-eslint/eslint-plugin': ^8.26.0 '@typescript-eslint/parser': ^8.26.0 @@ -139,6 +140,7 @@ catalog: prettier-plugin-tailwindcss: ^0.6.11 publint: ^0.2.12 qrcode: ^1.5.4 + qs: ^6.14.0 radix-vue: ^1.9.17 resolve.exports: ^2.0.3 rimraf: ^6.0.1