ProTable 如何实现筛选条件跟 url 参数双向同步?

958 阅读9分钟

业务场景

某些时候,我们需要将筛选条件(也就是筛选表单的表单值)同步记录在 url 上。可能你目前没有遇到,但是我相信你将来一定会遇到。什么时候有这种需求呢?常见的有下面的两种业务场景需要这种特性:

  • 从列表页跳转到详情页,返回之后,我还是想看到之前的筛选结果;
  • 把查询结果分享给第三方,被分享者一访问页面就能看到相应的筛选结果;

从列表页跳转到详情页,返回之后,我还是想看到之前的筛选结果

也许你会说,我详情页不做成路由不就行了吗?确实可以。但是你想想,假如你的详情页的内容很多,这个时候你还是用 modal 或者 drawer 去展示的话,那么,我相信产品经理或者 UI 设计师是不会同意的。因而,详情页用一个单独的路由去承载是一件避免不了的事。

那么问题就来了。假如我的筛选条件很多,我辛辛苦苦填完这个筛选条件后,点击查询按钮,查到了我想看的条目后,点击某个条目的详情。看完该条目的详情,我本意是想接着看其他的条目的详情的。结果发现,从详情页返回列表页后,我的筛选条件不见了。这个时候,产品经理或者 UI 设计师肯定会眉头紧皱,叹口气,摇摇头。最后,他们会找到你说,「从详情页返回到列表页后,能不能记住上一次的查询结果啊」?

把查询结果分享给第三方,被分享者一访问页面就能看到相应的筛选结果

某些业务场景下,可能需要前端去实现「查询结果可分享」的这种诉求的。那么这种情况下,查询条件必须要带到 url 上了。显而易见,这是没有什么选择的余地。

特性诉求

以上举例的业务场景对 <ProTable> 组件的特性诉求就是:「如何实现筛选条件跟 url 参数双向同步?

特性实现

乍地一看,实现这个特性诉求似乎很常见。于是乎,我们到 ProTable 的官方文档上面一查,结果发现没有直接的介绍(其实,它是放在 ProForm那一节介绍了)。可能最终,我们通过不断地地毯式的搜索或者直接问 AI 大模型,最终找到了相应的配置字段:

<ProTable
  ...
  form={{
      syncToUrl: true,
  }}
/>

你以为这样就完啦?不,当你执行下面的操作步骤后,你会发现问题:

  1. 录入筛选条件后,点击「查询」按钮;
  2. 查询成功后,<ProTable> 组件会把筛选条件以 url query 参数的形式同步到 url 上;
  3. 然后,你刷新页面,模拟分享出去的页面被访问的场景
  4. 最后,你再点击「重置」按钮。

这个时候,就会发现了问题了:“重置功能失效了”。也就是说,筛选表单并没有被清空。

以上结论是基于这么一个基本事实:“99% 的用户对「重置功能」的预期肯定是「清空表单」,而不是将筛选表单重置到之前的某个状态”。

这种表现基本上等同于 bug 级别了。之前我使用了syncToUrl: true 这个配置后,被测试反馈了这个问题,第一直觉是这是 <ProTable> 一个bug。因为时间紧迫,当时的解决方案是弃用这个特性。

后面,随着对 pro-component 源码的深入,我发现,其实他们是提供了这个配置项去满足这个需求的。那就是 syncToInitialValues: false。源码里面对这个配置属性的注释是:

同步结果到 initialValues,默认为true。如果为false,reset的时将会忽略从url上获取的数据

这个字段的中文注释真的让人看不太懂。啥叫「同步xxx到initialValues」啊?按理说,initialValues的设置只有一次,为啥还有「同步」的说法。经过探索,我发现这个字段的用途应该是这么描述:

是否允许 url 上的查询参数参与到 initialValues 的计算(resolve)逻辑里面去。

那么 syncToInitialValues: false 的意思就是说「不允许 url 上的查询参数参与到 initialValues 的计算逻辑里面去」。那么,如果你在别的地方(表单层面或者单个字段层面)没有对 initialValues 进行设置,那么筛选表单的 initialValues 就是为空。在这种情况下,如果用户再点击「重置」按钮,那么就实现了「表单清空」的效果。

有细心的读者可能会发现,上面的阐述中,我们作了前置假设:

如果你在别的地方(表单层面或者单个字段层面)没有对 initialValues 进行设置」。

为什么呢?那是因为 <ProTable> 内置的对「重置」功能的实现所决定的。「重置」按钮的 click handler 面核心的方法是 - form.resetFields()

// /packages/form/src/components/Submitter/index.tsx
import { proTheme, useIntl } from '@ant-design/pro-provider';
import type { ButtonProps } from 'antd';
import { Button, Form } from 'antd';
import omit from 'omit.js';
import React from 'react';

...

const Submitter: React.FC<SubmitterProps> = (props) => {

...

  const reset = () => {
    form.resetFields();
    onReset?.();
  };

...

  if (resetButtonProps !== false) {
    dom.push(
      <Button
        {...omit(resetButtonProps, ['preventDefault'] as any)}
        key="rest"
        onClick={(e) => {
          if (!resetButtonProps?.preventDefault) reset();
          resetButtonProps?.onClick?.(
            e as React.MouseEvent<HTMLButtonElement, MouseEvent>,
          );
        }}
      >
        {resetText}
      </Button>,
    );
  }

...
};

export default Submitter;

如你所见,内置的处理是,先调用 antd form 组件的 form.resetFields()antd form 组件的文档明确指出,这个方法只是将 form 重置到它的 initialValues 状态

image.png

所以,对 antd 生态里面这个 form 的 「重置功能」的正确理解是:「它是重置表单值回到它的 initialValues 而已,并不是实现我们所期待的“清空表单”」

好了,到目前为止,我们得到了实现我们特性的配置组合,它就是:

<ProTable
  ...
  form={{
      syncToUrl: true,
      syncToInitialValues: false
  }}
/>

这个完事了吗?不,还没有!

还有一个细节我们要补上。这个细节是特别针对时间区间类型的字段的。举例说明,假设我们有这样的列表页:

image.png 这个列表页的筛选表单有一个时间区间类的字段:「创建时间」。因为时间区间类的字段最终是用antd 的时间类的原子组件来呈现的,这些组件原始值是一个具有两个元素的数组。而官方文档明确指出,url 的 query 参数值只能是字符串:

image.png

因此,这里就要涉及到 url 跟筛选表单双向同步的时候做转换处理:

  • 表单 -> url - 从筛选表单同步到 url 的时候,要拆分为两个字段;
  • url -> 表单 - 从 url 回显到筛选表单的时候,要把之前拆分的两个字段组合成一个数组。

从筛选表单同步到 url 的时候,要拆分为两个字段

要实现这个,我们在配置 column 的时候,<ProTable> 提供了一个 search 字段给我们去声明该怎么拆分:

const columns: ProColumns<GithubIssueItem>[] = [
  {
    title: "创建时间",
    dataIndex: "created_at",
    valueType: "dateRange",
    hideInTable: true,
    search: {
      transform: (value) => {
        return {
          startTime: value?.[0],
          endTime: value?.[1],
        };
      },
    }
  },
]

这里,我们声明了,我们要把 created_at 字段的值拆分为两个字段:startTimeendTimestartTime的值取数组的第一个元素值,endTimeendTime的值取数组的第二个元素值。

从 url 回显到筛选表单的时候,要把之前拆分的两个字段组合成一个数组

这样做的目的是应底层原子组件对 value 的格式要求 - antd 时间区间类型的原子组件的值是一个具有两个元素的数组。我们要把它拼成人家所要求的格式,然后才能喂给它。这里还是利用回上面我提到的那个配置项:syncToUrl。只不过,要实现数据的重组,syncToUrl的值是一个函数了,而不是 boolean 值:

<ProTable
  ...
  form={{
    syncToUrl: (values, type) => {
        if (type === "get") {
          return {
            ...values,
            created_at: [values.startTime, values.endTime],
          };
        }
        return values;
      }
  }}
/>

在这里,values 指代的是解析 url 参数所得到的结果。而 type === "get" 对应的就是「从 url 解析得到表单值回显到筛选表单」的场景。

如果只是考虑「表单到url」场景下的字段拆分,其实我们可以不把字段拆分的声明写在 column 字段的配置那里,而是一并写在了在 syncToUrl 函数这里。因为,在 syncToUrl 函数还会被应用到「把筛选表单同步到 url」的场景,也就是 type === "set" 的分支情况。

所以,如果单单考虑「表单 -> url」场景下的字段拆分,那么我们还可以这么写:

<ProTable
  ...
  form={{
    syncToUrl: (values, type) => {
        // 对应的是「url -> 表单」的同步
        if (type === "get") {
          return {
            ...values,
            created_at: [values.startTime, values.endTime],
          };
        }else { // 对应的是「表单 -> url」的同步
          if (Array.isArray(values.created_at)) {
            const [startTime, endTime] = values.created_at;
            delete values.created_at;
            
            return {
              ...values,
              startTime,
              endTime,
            };
          }
          return values;
        }
        
      }
  }}
/>

但是这种写法有一种弊端,在内部实现中,针对这种写法,<ProTable> 并不会帮我们同步到 request 请求所用到的 params 里面去,比如,你在 request 里面做个打印,你会看到上面的这种分解声明并不会同步到request 的请求参数里面去:

<ProTable
  ...
 request={async (params, sort, filter) => {
      // params 里面,created_at并没有被拆解为 startTime 和 endTime 两个字段
      console.log("🚀 ~ request={ ~ params:", params);
      ...
  }}
/>

不过,通过这个探索,我们也知道了 syncToUrl 函数只会被应用到 url 跟筛选表单双向同步的场景。

总结

综上所述,要想实现「筛选条件跟 url 参数双向同步」特性以及需要连带处理的两个细节:

  • 将「重置」按钮实现为「表单清空」
  • 对于时间区间类型的字段进行拆分和重组

那么,我们就需要三个配置字段:

  • syncToUrl - 如果筛选表单没有需要拆分的字段,那么直接设置为 true 即可;否则的话,那就是配置为一个函数,然后在 type === "get" 分支情况下去声明如何将拆分过的字段重组起来,喂给底层的原子组件;
  • search.transform - 在特定的字段的 column 配置中,声明一个 transform 函数,用于声明这个字段会被拆分为哪几个字段;
  • syncToInitialValues - 把这个字段配置为 false,用于解决 antd 对 「重置」功能的默认实现(重置到 form 的 initialValues)所带来的 bug 幻觉。在特定的前置条件下(没有在表单或者字段层面去设置初始值),实现人类直觉的「表单清空」功能。

最后,附上一个 playground 项目,大家可以上去看看,玩一玩~