Browse Source

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

添加快捷设置请求参数序列化方法的配置
Netfan 1 month ago
parent
commit
96d2bc52e9

+ 30 - 0
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<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 请求

+ 3 - 1
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:"
   }
 }

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

+ 20 - 6
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<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: 原始的AxiosResponse,包括headers、status等,不做是否成功请求的检查。
+   * - body: 返回响应数据的BODY部分(只会根据status检查请求是否成功,忽略对code的判断,这种情况下应由调用方检查请求是否成功)。
+   * - data: 解构响应的BODY数据,只返回其中的data节点数据(会检查status和code是否为成功状态)。
    */
   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>;

+ 19 - 0
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<any>,
+  type: 'brackets' | 'comma' | 'indices' | 'repeat',
+) {
+  return requestClient.get('/status', {
+    params,
+    paramsSerializer: type,
+    responseReturn: 'raw',
+  });
+}
+
+export { getParamsData };

+ 2 - 1
playground/src/locales/langs/zh-CN/demos.json

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

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

+ 61 - 0
playground/src/views/demos/features/request-params-serializer/index.vue

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

+ 25 - 0
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: {}

+ 2 - 0
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