feat: pre-set serialization methods for request parameters (#5814)

添加快捷设置请求参数序列化方法的配置
pull/62/head
Netfan 2025-03-29 19:21:21 +08:00 committed by YunaiV
parent a08d73b034
commit 98b2d2a972
10 changed files with 181 additions and 38 deletions

View File

@ -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<T = any> = {
/**
* 参数序列化方式。预置了几种针对数组的序列化类型
* - 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<T>['paramsSerializer'];
/**
* 响应数据的返回方式。
* - raw: 原始的AxiosResponse包括headers、status等不做是否成功请求的检查。
* - body: 返回响应数据的BODY部分只会根据status检查请求是否成功忽略对code的判断这种情况下应由调用方检查请求是否成功
* - data: 解构响应的BODY数据只返回其中的data节点数据会检查status和code是否为成功状态
*/
responseReturn?: 'body' | 'data' | 'raw';
};
```
### 请求示例
#### GET 请求

View File

@ -26,7 +26,7 @@
"qs": "catalog:"
},
"devDependencies": {
"axios-mock-adapter": "catalog:",
"@types/qs": "catalog:"
"@types/qs": "catalog:",
"axios-mock-adapter": "catalog:"
}
}

View File

@ -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<T> = await this.instance({
url,
...config,
...(config.paramsSerializer
? { paramsSerializer: getParamsSerializer(config.paramsSerializer) }
: {}),
});
return response as T;
} catch (error: any) {

View File

@ -5,15 +5,29 @@ import type {
InternalAxiosRequestConfig,
} from 'axios';
type ExtendOptions = {
/**
* raw: AxiosResponseheadersstatus
* body: BODYstatuscode
* data: BODYdatastatuscode
type ExtendOptions<T = any> = {
/**
*
* - 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<T>['paramsSerializer'];
/**
*
* - raw: AxiosResponseheadersstatus
* - body: BODYstatuscode
* - data: BODYdatastatuscode
*/
responseReturn?: 'body' | 'data' | 'raw';
};
type RequestClientConfig<T = any> = AxiosRequestConfig<T> & ExtendOptions;
type RequestClientConfig<T = any> = AxiosRequestConfig<T> & ExtendOptions<T>;
type RequestResponse<T = any> = AxiosResponse<T> & {
config: RequestClientConfig<T>;

View File

@ -0,0 +1,19 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
/**
*
*/
async function getParamsData(
params: Recordable<any>,
type: 'brackets' | 'comma' | 'indices' | 'repeat',
) {
return requestClient.get('/status', {
params,
paramsSerializer: type,
responseReturn: 'raw',
});
}
export { getParamsData };

View File

@ -50,7 +50,8 @@
"clipboard": "剪贴板",
"menuWithQuery": "带参菜单",
"openInNewWindow": "新窗口打开",
"fileDownload": "文件下载"
"fileDownload": "文件下载",
"requestParamsSerializer": "参数序列化"
},
"breadcrumb": {
"navigation": "面包屑导航",

View File

@ -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'),
},
},
],
},
// 面包屑导航

View File

@ -0,0 +1,61 @@
<script lang="ts" setup>
import { computed, ref, watchEffect } from 'vue';
import { Page } from '@vben/common-ui';
import { Card, Radio, RadioGroup } from 'ant-design-vue';
import { getParamsData } from '#/api/examples/params';
const params = { ids: [2512, 3241, 4255] };
const paramsSerializer = ref<'brackets' | 'comma' | 'indices' | 'repeat'>(
'brackets',
);
const response = ref('');
const paramsStr = computed(() => {
// URL
const url = response.value;
return new URL(url).searchParams.toString();
});
watchEffect(() => {
getParamsData(params, paramsSerializer.value).then((res) => {
response.value = res.request.responseURL;
});
});
</script>
<template>
<Page
title="请求参数序列化"
description="不同的后台接口可能对数组类型的GET参数的解析方式不同我们预置了几种数组序列化方式通过配置 paramsSerializer 来实现不同的序列化方式"
>
<Card>
<RadioGroup v-model:value="paramsSerializer" name="paramsSerializer">
<Radio value="brackets">brackets</Radio>
<Radio value="comma">comma</Radio>
<Radio value="indices">indices</Radio>
<Radio value="repeat">repeat</Radio>
</RadioGroup>
<div class="mt-4 flex flex-col gap-4">
<div>
<h3>需要提交的参数</h3>
<div>{{ JSON.stringify(params, null, 2) }}</div>
</div>
<template v-if="response">
<div>
<h3>访问地址</h3>
<pre>{{ response }}</pre>
</div>
<div>
<h3>参数字符串</h3>
<pre>{{ paramsStr }}</pre>
</div>
<div>
<h3>参数解码</h3>
<pre>{{ decodeURIComponent(paramsStr) }}</pre>
</div>
</template>
</div>
</Card>
</Page>
</template>

View File

@ -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:

View File

@ -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