筛选条件是 BI 搭建的核心概念，我们大部分所说的探索式分析、图表联动也都属于筛选条件的范畴，**其本质就是一个组件对另一个组件的数据查询起到筛选作用**。

## 筛选组件是如何作用的

我们最常见的筛选条件就是表单场景的查询控件，如下图所示：

<img width=300 src="https://img.alicdn.com/tfs/TB107njjRFR4u4jSZFPXXanzFXa-724-302.png">

若干 “具有输出能力” 的组件作为筛选组件，点击查询按钮时触发其作用组件重新取数。

注意这里 “具有输出能力” 的组件不仅是输入框等具有输入性质的组件，其实所有具备交互能力的组件都可以，甚至可以由普通组件承担筛选触发的能力：

<img width=300 src="https://img.alicdn.com/tfs/TB1V.sVgSslXu8jSZFuXXXg7FXa-858-196.png">

一个表格的表头点击也可以触发筛选行为，或者柱状图的一个柱子被点击都可以，只要进行到这层抽象，**组件间联动本质也属于筛选行为**。

同样重要的，筛选作用的组件也可以是具备输入能力的组件：

<img width=450 src="https://img.alicdn.com/tfs/TB1qqrxUpT7gK0jSZFpXXaTkpXa-1280-198.png">

当目标组件是具备筛选能力组件时，这就是筛选联动场景了，所以 **筛选联动也属于普通筛选行为**。至于目标组件触发取数后，是否立即修改其筛选值，进而触发后续的筛选联动，就完全由业务特性决定了。

一个组件也可以自己联动自己筛选，比如折线图点击下钻的场景，就是自己触发了筛选，作用到自己的例子。

## 什么是筛选组件

**任何组件都可以是筛选组件**。

可能最容易理解的是输入框、下拉框、日期选择器等具备输入特征的组件，这些组件只能说天然适合作为筛选组件，但不代表系统设计要为这些组件特殊处理。

扩大想一想，其实普通的按钮、表格、折线图等等 **具有展示属性的组件也具有输入特性的一面**，比如按钮被点击时触发查询、单元格被点击时想查询当前城市的数据趋势、折线图某条线被点击时希望自身从年下钻到月等等。

所以 **不存在筛选组件这概念，而是任何组件都具有筛选的能力**，因此筛选是一种任何组件都具有的能力，而不局限在某几个组件上，一旦这么设计，可以做到以下几点：

1. 实现输入类组件到展示类组件的筛选，符合基本筛选诉求。
2. 实现展示类组件到展示类组件的筛选，属于图表联动图表的高级功能。
3. 实现输入类组件到输入类组件的筛选，属于筛选联动功能。
4. 实现组件自身到自身的筛选，实现下钻功能。

下面介绍 bi-designer 的筛选条件设计。

## 筛选条件设计

基于上述分析，bi-designer 在组件元信息中没有增加所谓的筛选组件类型，而是将其设定为一种筛选能力，任何组件都能触发。

### 如何触发筛选

组件调用 `onFilterChange` 即可完成筛选动作：

```jsx
import { useDesigner } from "@alife/bi-designer";

const InputFilter = () => {
  const { onFilterChange } = useDesigner();

  return (
    <input onChange={(event) => () => onFilterChange(event.target.value)} />
  );
};
```

但这种开发方式违背了 **低侵入** 的设计理念，我们可以采用组件与引擎解构的方式，让输入框变更的时候直接调用 `props.onChange` ，这个组件保持了最大的独立性：

```jsx
const InputFilter = ({ onChange }) => {
  return <input onChange={(event) => () => onChange(event.target.value)} />;
};
```

那渲染引擎怎么将 `onFilterChange` 映射到 `props.onChange` 呢？如下配置 DSL 即可：

```json
{
  "props": {
    "onChange": {
      "type": "JSExpression",
      "value": "this.onFilterChange"
    }
  }
}
```

### 筛选影响哪些组件

一般筛选组件会选择作用于的目标组件，类似下图：

<img width=300 src="https://img.alicdn.com/tfs/TB1RJHPUxD1gK0jSZFsXXbldVXa-768-486.png">

这些信息会存储在筛选组件的组件配置中，即 `componentInstance.props`，筛选目标组件在 `componentMeta.eventConfigs` 组件元信息的事件中配置：

```jsx
import { Interfaces } from "@alife/bi-designer";

const componentMeta: Interfaces.ComponentMeta = {
  eventConfigs: ({ componentInstance }) =>
    componentInstance.props.targets?.map((target) => ({
      // 筛选取数
      type: "filterFetch",
      // 触发组件
      source: componentInstance.id,
      // 作用组件
      target: target.id,
    })),
};
```

如上所示，假设作用于组件存储在 `props.targets` 字段中，我们将其 `map` 一下都设置为 `filterFetch` 类型，表示筛选作用，`source` 触发源是自己，`target` 目标组件是存储的 `target.id`。

这样当 `source` 组件调用了 `onFilterChange`，`target` 组件就会触发取数，并在取数参数中拿到作用于其的筛选组件信息与筛选值。

### 组件如何感知筛选条件

组件取数是结合了筛选条件一起的，只要如上设置了 `filterFetch`，渲染引擎会自动在计算取数参数的回调函数 `getFetchParam` 中添加 `filters` 代表筛选组件信息，组件可以结合自身 `componentInstance` 与 `filters` 推导出最终取数参数：

<img width=300 src="https://img.alicdn.com/tfs/TB1tDDTUuH2gK0jSZJnXXaT1FXa-870-434.png">

最终，组件元信息只要写一个 `getFetchParam` 回调函数即可，**可以自动拿到作用于它的筛选组件，而不用关心是哪些配置导致了关联，只要响应式的去处理筛选作用即可**。

```jsx
import { Interfaces } from "@alife/bi-designer";

const componentMeta: Interfaces.ComponentMeta = {
  // 组装取数参数
  getFetchParam: ({ componentInstance, filters }) => {
    // 结合 componentInstance 与 filters.map... 返回取数参数
  },
};
```

## 筛选组件间联动带来的频繁取数问题

对于筛选联动的复杂场景，会遇到频繁取数的问题。

假设国家、省、市三级联动筛选条件同时 `filterFetch` 作用于一个表格，这个表格取数的筛选条件需要同时包含国家、省、市三个参数，但我们又设置了 国家、省、市 这三个筛选组件之间的 `filterFetch` 作为筛选联动，那么国家切换后、省改变、联动市改变，这个过程筛选值会变化三次，但我们只想表格组件取数函数仅执行最后的一次，怎么办呢？

<img width=350 src="https://img.alicdn.com/tfs/TB1CFD9UBr0gK0jSZFnXXbRRXXa-984-656.png">

如上图所示，其实每个筛选条件在渲染引擎数据流中还存储了一个 `ready` 状态，表示筛选条件是否就绪，**一个组件关联的筛选条件只要有一个 `ready` 不为 `true`，组件就不会触发取数**。

因此我们需要在筛选变化的过程中，总是保证一个筛选组件的 `ready` 为 `false`，等筛选间联动完毕了，所有筛选器的 `ready` 为 `true`，组件才会取数，我们可以使用 `filterReady` 筛选依赖配置：

```jsx
import { Interfaces, createComponentInstancesArray } from "@alife/bi-designer";

const componentMeta: Interfaces.ComponentMeta = {
  eventConfigs: ({ componentInstance }) =>
    componentInstance.props.targets?.map((target) => ({
      // 筛选就绪依赖
      type: "filterReady",
      // 触发组件
      source: componentInstance.id,
      // 作用组件
      target: target.id,
    })),
};
```

这样配置后，当 `source` 组件触发 `onFilterChange` 后，`target` 组件的筛选 `ready` 会立即设置为 `false`，只有 `target` 组件取完数后主动触发 `onFilterChange` 才会将自己的 `ready` 重新置为 `true`。**That'a all，其他流程没有任何感知**。

## 若干筛选组件聚合成一个查询控件

除了联动外，也会存在防止频繁查询的诉求，希望将多个筛选条件绑定成一个大筛选组件，在点击 “查询” 按钮时再取数：

<img width=400 src="https://img.alicdn.com/tfs/TB1nmHVUuH2gK0jSZJnXXaT1FXa-972-174.png">

可以利用 **筛选作用域** 轻松实现此功能，只需要两步：

### 筛选组件设置独立筛选作用域

```jsx
import { Interfaces } from "@alife/bi-designer";

const componentMeta: Interfaces.ComponentMeta = {
  // 通过 componentInstance 判断，如果是全局筛选器内部，则设置 filterScope
  filterScope: ({ componentInstance }) => ["my-custom-scope-name"],
};
```

这样，这批筛选组件就与其作用的组件属于不同的 **筛选作用域** 了，所以筛选不会对其立即生效，功能实现了一半。

### 确认按钮点击时调用 `submitFilterScope`

```jsx
import { useDesigner } from '@alife/bi-designer'

const componentMeta: Interfaces.ComponentMeta = {
  const { submitFilterScope } = useDesigner()
  // 点击确认按钮时，调用 submitFilterScope('my-custom-scope-name')
};
```

你可以在点击查询按钮后调用 `submitFilterScope` 并传入对应作用域名称，这样作用域内筛选组件就会立即对其 `target` 组件生效了。

至于确认按钮、UI 上的聚合，这些你可以写一个自定义组件去做，利用 `ComponentLoader` 把筛选组件聚合到一起加载，总之功能与 UI 是解耦的。

如果你对原理感兴趣，可以再多看一下这张图：

<img width=400 src="https://img.alicdn.com/tfs/TB1eZPJhAcx_u4jSZFlXXXnUFXa-1082-645.png">

### 突破筛选作用域

然而实际场景中，可能存在更复杂的组合，见下面的例子：

<img width=350 src="https://img.alicdn.com/tfs/TB1cfGNiIVl614jSZKPXXaGjpXa-966-600.png">

筛选器 1 同时对 筛选器 2、表格 产生筛选作用 `filterFetch`，但对 表格 的作用希望通过查询按钮拦截住，而对 筛选器 2 的作用希望能立即生效，对于这个例子有两种方式解决：

最简单的方式就是将 筛选器 1、筛选器 2 设置为相同作用域 `group1`，这样就通过作用域分割自然实现了效果，**而且这本质上是两个筛选器 UI 不在一起，但筛选作用域相同的例子**：

<img width=350 src="https://img.alicdn.com/tfs/TB1_kn1UpT7gK0jSZFpXXaTkpXa-1056-660.png">

但是再变化一下，如果筛选器 2 也对表格产生筛选作用，那我们将 筛选器 1、筛选器 2 放入同一个 `group1` 等于对表格的查询都会受到 “查询” 按钮的控制，但 **我们又希望筛选器 2 可以立即作用于表格**：

<img width=350 src="https://img.alicdn.com/tfs/TB1ftP3Urr1gK0jSZFDXXb9yVXa-968-602.png">

如图所示，我们只能将 筛选器 1 的筛选作用域设置为 `group1`，这样 筛选器 2 与 表格 属于同一个筛选作用域，他们之间筛选会立即生效，我们只要解决 筛选器 1 不能立即作用于 筛选器 2 的问题即可，可以通过 `ignoreFilterScope` 方式突破筛选作用域：

```jsx
import { Interfaces } from "@alife/bi-designer";

const componentMeta: Interfaces.ComponentMeta = {
  eventConfigs: ({ componentInstance }) =>
    componentInstance.props.targets?.map((target) => ({
      // 筛选取数
      type: "filterFetch",
      // 触发组件
      source: componentInstance.id,
      // 作用组件
      target: target.id,
      // 突破筛选作用域
      ignoreFilterFetch: true,
    })),
};
```

我们只要在 `source: 筛选器1` `target: 筛选器2` 的 `filterFetch` 配置中，将 `ignoreFilterFetch` 设置为 `true`，这个 `filterFetch` 就会忽略筛选作用域，实现立即 筛选器 1 立即作用到 筛选器 2 的效果。

## 总结

你还有哪些特殊的筛选诉求？可以用这套筛选设计解决吗？

> 讨论地址是：[精读《BI 搭建 - 筛选条件》· Issue #270 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/270)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
