bi-designer 是阿里数据中台团队自研的前端搭建引擎，基于它开发了阿里内部最大的数据分析平台，以及阿里云上的 QuickBI。

> bi-designer 目前没有开源，因此文中使用的私有 npm 源 `@alife/bi-designer` 是无法在公网访问的。

本文介绍 bi-designer 组件的使用 API。

## 组件加载

组件实例定义在元信息 - element 中：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  element: () => <div />,
};
```

### 异步加载

使用 React.lazy 即可实现异步加载组件：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  // 懒加载
  element: React.lazy(async () => import("./real-component")),
};
```

懒加载的组件会自动完成加载，如需自定义加载 Loading 效果，可以阅读 组件异步、错误处理 文档。

## 组件异步、错误处理

- 组件源码异步加载或者进行 Suspense 取数时，会调用 ComponentMeta.suspenseFallback 渲染。
- 组件渲染出错时，会调用 ComponentMeta.errorFallback 渲染。

### 异步加载

```jsx
import { Interfaces } from "@alife/bi-designer";
const SuspenseFallback: Interfaces.InnerComponentElement = ({
  componentInstance,
  componentmeta,
}) => {
  return <span>Loading</span>;
};
const componentMeta = {
  componentName: "suspense-custom-fallback",
  element: React.lazy(async () => {
    await sleep(2000);
    return Promise.resolve({ default: () => null });
  }),
  suspenseFallback,
};
```

上面例子中，对异步加载的组件定义了 suspenseFallback 来处理异步中的状态。

### 错误处理

```jsx
import { Interfaces } from "@alife/bi-designer";
const errorFallback: Interfaces.ErrorFallbackElement = ({
  componentInstance,
  componentmeta,
  error,
}) => {
  return <span>错误：{error.toString()}</span>;
};
const componentMeta = {
  componentName: "error-custom-fallback",
  element: () => {
    throw new Error("error!");
  },
  errorFallback,
};
```

上面例子中， errorFallback 处理了组件抛出的任何错误。

- error ：当前组件报错信息。

## 容器组件

容器元素可以被拖入子元素，只要将 isContainer 设置为 true 即可：

```jsx
export const yourComponentMeta: Interfaces.ComponentMeta = {
  componentName: "yourComponent",
  element: YourComponent,
  isContainer: true,
};
```

之后可以从 props.children 访问到子元素：

```jsx
const YourComponent = ({ children }) => {
  return <div>{children}</div>;
};
```

## 多插槽容器组件

多插槽容器即一个容器内部有多个位置可响应拖拽。

实现多插槽容器组件注意两点即可：

1. 这个大容器组件本身不为容器类型，因为我们要拖入到子元素，不需要拖入到它自己本身。
2. 内部通过 ComponentLoader 添加容器类组件作为子元素。

比如我们要利用 Antd Card 实现一个多插槽容器，首先把 Card 申明为普通组件：

```jsx
export const cardComponentMeta: Interfaces.ComponentMeta = {
  componentName: "card",
  element: CardComponent,
};
```

在实现 Card 功能时，我们在两处内部可拖拽区域调用 ComponentLoader 加载一个事先定义好的容器组件 div :

```jsx
import { ComponentLoader, useDesigner } from '@alife/bi-designer'
const CardComponent: Interfaces.ComponentElement = () => {
  const { useKeepComponentLoaders } = useDesigner()

  useKeepComponentLoaders(['1'])

  return (
    <Card
      actions={[...]}
    >
      <ComponentLoader
        id="1"
        componentName="div"
        props={{style: { minHeight: 30 }}}
      />
    </Card>
  );
};
```

总结一下，我们可以利用 ComponentLoader 在组件内部加载任意组件，如果加载的是容器组件，就相当于增加了一块内部插槽。
这种插槽可以插入理论上无数种容器组件，根据业务需求而定，比如上面这种最简单的 div 容器，可以是这么实现的：

```jsx
const Div: Interfaces.ComponentElement = ({ children, style }) => {
  return (
    <div style={{ width: "100%", height: "100%", ...style }}>{children}</div>
  );
};
```

## Tabs 容器组件

Tabs 容器可以看作动态数量的多插槽容器：

```jsx
import { ComponentLoader, useDesigner } from "@alife/bi-designer";
const TabsComponent: Interfaces.ComponentElement = ({ tabs }) => {
  const { useKeepComponentLoaders } = useDesigner();

  useKeepComponentLoaders(tabs?.map((each) => each.key));

  return (
    <div>
      <Tabs>
        {tabs?.map((each) => (
          <Tabs.TabPane tab={`Tab${each.title}`} key={each.key}>
            /* 举个例子，拿 div 这个组件作为 TabPane 的容器 */
            <ComponentLoader id={each.key} componentName="div" />
          </Tabs.TabPane>
        ))}
      </Tabs>
    </div>
  );
};
```

Tabs 根据配置动态渲染 TabPane ，为每个 TabPane 塞入一个容器即可。

注意， useKeepComponentLoaders 函数可以让数据变化后某个子 Tab 消失时，及时做画布脏数据清除。另外即便数据不是动态的，也要及时更新这个函数，比如某次更新， ComponentLoader id 为 3 的值从代码移除了，也要把 3 这个 id 从 useKeepComponentLoaders 中移除。

## 组件宽高

对于能自适应高度的组件，最佳方案是设置 100% 的宽高：

```jsx
import { Interfaces } from "@alife/bi-designer";
const CustomComponent: Interfaces.ComponentElement = () => {
  return <div style={{ width: "100%", height: "100%", minHeight: 50 }} />;
};
```

流式布局下 height: '100%' 高度会坍塌，因此可以设置个最小高度固定值兜底，或者通过 props 让用户配置。

如果组件不支持自适应宽高，比如渲染 canvas、svg 等图表时，需要自己监听宽高，或者利用 容器拓展组件 props 功能，在容器算好宽高具体值，再传入组件。

当然也可以直接设置一个默认高度，或者根据内容动态撑开组件，在流式布局、磁贴布局下可以自动撑开容器（磁贴布局编辑模式下拖拽的高度允许被运行时自动撑大），在自由布局下无法撑开，会出现内滚动条。

## 组件配置默认值

组件配置表单的默认值在 ComponentMeta.props 中定义：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  props: [
    {
      name: "title",
      defaultValue: "标题",
    },
  ],
};
```

Props 描述了组件入参信息，包括：

```typescript
interface MetaProps {
  /**
   * 属性名
   */
  name: string;
  /**
   * 属性类型
   */
  type?: string;
  /**
   * 属性描述
   */
  description?: string;
  /**
   * 默认值
   */
  defaultValue?: any;
}
```

如果只设置默认值，只需要关心 name 和 defaultValue 。

## 组件配置表单

组件配置表单在 ComponentMeta.propsSchema 中定义：

```jsx
import { Interfaces } from '@alife/bi-designer'
const componentMeta: Interfaces.ComponentMeta = {
  platform: 'fbi', // 平台名称
  propsSchema: {
    style: {
      color: {
        title: 'Color',
        type: 'color',
        redirect: 'color',
      }，
    },
  },
}
```

- platform ：项目类型。不同项目类型的 propsSchema 结构可能不同，其他取数逻辑可能也不同。
- propsSchema ：表单配置结构，符合 UISchema 规范。对于特殊表单可能使用自己的规范。

## 组件配置修改回调

组件配置修改回调在每次组件实例信息被修改时触发，在 ComponentMeta.onPropsChange 中定义：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  onPropsChange: ({ prevProps, currentProps, componentMeta }) => {
    return {
      ...currentProps,
      color: "red",
    };
  },
};
```

- prevProps ：上一次组件配置。
- currentProps ：当前组件配置。
- componentMeta ：组件元信息。
- Return ：新的组件配置。

## 跨组件关联配置更新

当画布任何组件变化时，组件都可以在 ComponentMeta.onPageChange 监听到，并修改自己的组件配置：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  onPageChange: ({ props, pageSchema }) => {
    // 当关联的组件找不到时清空
    if (
      !pageSchema?.componentInstances?.some((each) => each.id === props.value)
    ) {
      return {
        ...props,
        // 清空 props.value
        value: "",
      };
    }
    // 返回值会更新当前组件配置
    return props;
  },
};
```

- props ：当前组件配置。
- pageSchema ：页面信息。
- Return ：新的组件配置。

假设组件配置中用到了其他组件 id 等数据，可以在 onPageChange 回调时做判断，如果目标组件不存在，对当前组件的部分配置内容做更新。

## 组件隐藏

组件隐藏可以通过 hide 设置：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  hide: ({ componentInstance, mode }) => true,
};
```

- componentInstance ：组件实例信息。
- mode ：当前模式，比如组件仅编辑模式隐藏，可以判断 ({ mode }) => mode === 'edit' 。

## 属性值类型 - JSSlot

JSSlot 是一种配置类型，可以将组件某个 props 参数设置为另一个组件实例，运行时作为 React Node 传参。

```jsx
import { Interfaces } from "@alife/bi-designer";
// 组件直接使用 props.header 作为 JSX
const ComponentWithJSSlot: Interfaces.ComponentElement = ({ header }) => {
  return (
    <div>
      header 元素：
      {header}
    </div>
  );
};
// DSL 中增加 Slot 描述
const defaultPageSchema: Interfaces.PageSchema = {
  componentInstances: {
    tg43g42f: {
      id: "tg43g42f",
      componentName: "js-slot-component",
      index: 0,
      props: {
        header: {
          type: "JSSlot",
          value: ["child1", "child2"],
        },
      },
    },
    child1: {
      id: "child1",
      componentName: "input",
      parentId: "tg43g42f",
      index: 0,
      isSlot: true,
    },
    child2: {
      id: "child2",
      componentName: "input",
      parentId: "tg43g42f",
      index: 1,
      isSlot: true,
    },
  },
};
```

- isSlot ：标识节点是 JSSlot 类型。
- type: 'JSSlot' ：标记属性为 JSSlot 类型， value 数组存储 Slot 组件 id。

## 属性值类型 - JSFunction

JSFunction 是一种配置类型，可以将组件某个 props 参数设置为自定义函数。

```jsx
import { Interfaces } from "@alife/bi-designer";
// 组件直接使用 props.onClick 作为函数调用
const FunctionComponent: Interfaces.ComponentElement = ({ onClick }) => {
  return <div onClick={onClick} />;
};
// DSL 中增加 Function 描述
const defaultPageSchema: Interfaces.PageSchema = {
  componentInstances: {
    test: {
      id: "tg43g42f",
      componentName: "functionComponent",
      index: 0,
      props: {
        onClick: {
          type: "JSFunction",
          value: 'function onClick() { console.log("123") }',
        },
      },
    },
  },
};
```

- type: 'JSFunction' ：标记属性为 JSFunction 类型， value 用字符串存储函数体。
  函数中可以使用 上下文数据对象 与 工具类拓展。

## 属性值类型 - JSExpression

JSExpression 是一种配置类型，可以将组件某个 props 参数设置为自定义表达式。

```jsx
import { Interfaces } from "@alife/bi-designer";
// 组件直接使用 props.variable 作为变量直接渲染
const ExpressionComponent: Interfaces.ComponentElement = ({ variable }) => {
  return <div>JSExpression：{variable}</div>;
};
// DSL 中增加 Expression 描述
const defaultPageSchema: Interfaces.PageSchema = {
  componentInstances: {
    test: {
      id: "tg43g42f",
      componentName: "expressionComponent",
      props: {
        variable: {
          type: "JSExpression",
          value: '"1" + "2"',
        },
      },
    },
  },
};
```

- type: 'JSExpression' ：标记属性为 JSExpression 类型， value 用字符串存储表达式。
  表达式可以使用 上下文数据对象、与 工具类拓展。

## 组件状态持久化

组件自身在运行时可以通过 updateComponentById 函数将状态持久化到配置中：

```jsx
import { Interfaces, useDesigner } from "@alife/bi-designer";
import * as fp from "lodash/fp";
const componentMeta: Interfaces.ComponentMeta = {
  element: Component,
};
const Component: Interfaces.ComponentElement = ({ id, count }) => {
  const { updateComponentById } = useDesigner();

  const handleIncCount = React.useCallback(() => {
    updateComponentById(id, (each) =>
      fp.set("props.count", each?.props?.count + 1, each)
    );
  }, [id, updateComponentById]);

  return <div onClick={handleIncCount}>{count}</div>;
};
```

注意：由于 updateComponentById 修改的是画布 DSL，因此在非编辑模式下，此 DSL 无法持久化。
对于此模式下产生的脏数据清理问题，同 组件配置订正。

## 动态创建组件

组件内可以动态创建任何其他组件，通过 props.ComponentLoader 实现：

```jsx
import { Interfaces, useDesigner, ComponentLoader } from "@alife/bi-designer";
const Card: Interfaces.ComponentElement = () => {
  const { useKeepComponentLoaders } = useDesigner();

  useKeepComponentLoaders(["1"]);

  return (
    <ComponentLoader id="1" componentName="button" props={{ color: "red" }} />
  );
};
```

- useKeepComponentLoaders ：与下面动态创建的组件 id 保持同步，以便引擎管理动态组件。
  ComponentLoader 参数说明：
- id ：动态组件的唯一 id，在同一个组件内，动态组件的 id 需要保持唯一。
- componentName ：组件名。
- props ：组件 Props，可选。

### 动态组件嵌套

动态组件可以任意嵌套：

```jsx
import { Interfaces, useDesigner, ComponentLoader } from "@alife/bi-designer";
const Card: Interfaces.ComponentElement = ({
  ComponentLoader,
  useKeepComponentLoaders,
}) => {
  const { useKeepComponentLoaders } = useDesigner();

  useKeepComponentLoaders(["1", "2"]);

  return (
    <ComponentLoader id="1" componentName="div">
      这是子元素：
      <ComponentLoader id="2" componentName="button" />
    </ComponentLoader>
  );
};
```

## 组件配置未 Ready 时不渲染

可以在组件容器或通用容器层对组件渲染做拦截，比如判断某些配置不满足，展示一个兜底图而不是直接渲染组件：

```jsx
import { Interfaces, useDesigner } from "@alife/bi-designer";
import * as fp from "lodash/fp";
const componentMeta: Interfaces.ComponentMeta = {
  element: Component,
  container: Container,
};
const Container: Interfaces.InnerComponentElement = ({
  componentInstance,
  children,
}) => {
  if (!componentInstance?.props?.count) {
    // 不满足渲染条件
    return <div>count 配置未 ready，不渲染组件</div>;
  }

  // 渲染 children，children 即组件本身
  return children;
};
```

### 配置未 Ready 时不取数

只要 getFetchParam 抛出异常即可暂停取数：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  getFetchParam: ({ componentInstance, componentMeta, filters, context }) => {
    if (componentInstance.props?.count !== "5") {
      // count 不为 '5' 则不取数
      throw Error("Not Ready");
    }

    return "123";
  },
};
```

这个错误可以通过 props.fetchError 访问到，组件和容器层都可以拦截：

```jsx
import { Interfaces } from "@alife/bi-designer";
class PropsNotReadyError extends Error {}
const componentMeta: Interfaces.ComponentMeta = {
  getFetchParam: ({ componentInstance, componentMeta, filters, context }) => {
    if (componentInstance.props?.count !== "5") {
      throw PropsNotReadyError("Not Ready");
    }

    return "123";
  },
  container: Wrapper,
};
const Wrapper: Interfaces.InnerComponentElement = ({ componentInstance }) => {
  if (componentInstance.props.fetchError instanceof PropsNotReadyError) {
    return <div>不满足取数条件</div>;
  }
};
```

## 组件取数

组件是否初始化取数在 ComponentMeta.initFetch 中定义；生成取数参数在 ComponentMeta.getFetchParam 中定义；组件取数函数在 ComponentMeta.fetcher 中定义

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  // 组件是否开启自动取数，当取数参数变化时(getFetchParam 控制)会触发自动取数
  autoFetch: ({ componentInstance, componentMeta }) => true,
  // 组件是否默认取数，仅 autoFetch 为 true 时生效
  initFetch: ({ componentInstance, componentMeta }) => true,
  // 组装取数参数
  getFetchParam: ({ componentInstance, componentMeta, filters, context }) => {
    return { name: componentInstance?.props?.name };
  },
  // 取数函数
  fetcher: async ({ componentMeta, param, context }) => {
    // 根据当前组件信息 componentInstance 与筛选条件组件&值 filters 进行取数
    return await customFetcher(param.name);
  },
};
```

- componentInstance ：当前组件实例信息。
- getFetchParam ：取数开始的回调，用来组装取数参数。返回 null 或 undefined 不会触发取数。
- filters ：作用于此组件的筛选信息，在 组件筛选 文档有进一步阐述。包含的 key 有：
- componentInstance ：筛选条件组件实例信息。
- filterValue ：筛选条件的当前筛选值。
- payload ：自定义传参，由组件筛选的 eventConfigs 定义，具体见文档 组件筛选 - 传递额外筛选信息 。
- context ：上下文，可以访问 useDesigner 一切内容。

做了取数配置后，组件就可以通过 props 拿到数据了：

```jsx
import { useDesigner } from "@alife/bi-designer";
const NameList: Interfaces.ComponentElement = () => {
  const { data, isFetching, isFilterReady } = useDesigner();

  if (!isFilterReady) {
    return <Spin>筛选条件未 Ready</Spin>;
  }
  if (isFetching) {
    return <Spin>取数中</Spin>;
  }
  return (
    <div>
      {data.map((each: any, index: number) => (
        <div key={index}>{each}</div>
      ))}
    </div>
  );
};
```

- data 取数结果。
- isFetching 是否在取数中。
- isFilterReady 筛选条件是否 Ready，在组件筛选一节会详细说明，此处理解为一种特殊取数 Hold 状态。
- fetchError 取数错误。

还可以 在引擎层配置全局组件取数配置，组件级配置的优先级高于引擎层的。

### 组件主动取数

通过 fetchData 可以主动取数：

```jsx
const NameList: Interfaces.ComponentElement = ({ fetchData }) => {
  const { fetchData } = useDesigner();
  return <button onClick={fetchData}>主动取数</button>;
};
```

- fetchData ：主动取数函数，调用后可以立即重新取数。

主动取数调用后，取数结果依然通过 props.data 返回。

### 自定义取数参数

fetchData 可以传入参数 getFetchParam 自定义取数参数：

```jsx
const NameList: Interfaces.ComponentElement = ({ fetchData }) => {
  const { fetchData } = useDesigner();
  const handleFetchData = React.useCallback(() => {
    fetchData({
      getFetchParam: ({ param, context }) => ({
        ...param,
        top: 1,
      }),
    });
  }, [fetchData]);

  return <button onClick={handleFetchData}>主动取数</button>;
};
```

要注意，非独立取数模式下即便修改了取数参数，下一次由外部触发的取数会重置取数参数。

### 独立取数

独立取数可以通过 standalone 参数申明，此时触发取数不会导致组件 Rerender 并拿到新 data ，而是返回一个 Promise 由组件自行处理。

```jsx
const NameList: Interfaces.ComponentElement = ({ fetchData }) => {
  const { fetchData } = useDesigner();

  const handleFetchData = React.useCallback(async () => {
    const data = await fetchData({
      standalone: true,
    });

    // 组件自己处理取数结果 data
  }, [fetchData]);

  return <button onClick={handleFetchData}>主动取数</button>;
};
```

这种独立取数场景可以适应下钻等组件自由取数的场景。

独立取数模式下当然也可以结合 getFetchParam 一起使用。

### 主动取消取数

通过 cancelFetch 可以主动取消取数：

```jsx
const NameList: Interfaces.ComponentElement = ({ cancelFetch }) => {
  const { cancelFetch } = useDesigner();
  return <button onClick={cancelFetch}>取消取数</button>;
};
```

- cancelFetch ：取消取数函数，调用后立即生效。取数完成后再调用则无作用。

### 优化取数性能

是否重新取数由 getFetchParam 返回值是否有变化决定，默认写法会进行 deepEqual 判断：

```jsx
import { Interfaces } from "@alife/bi-design";
const componentMeta: Interfaces.ComponentMeta = {
  getFetchParam: ({ componentInstance }) => {
    // 引擎会对返回值进行深对比
    return { name: componentInstance?.props?.name };
  },
};
```

但是下面两种情况可能会产生性能问题：

1. 返回值数据结构非常大，导致频繁 deepEqual 开销明显增大。
2. 生成取数参数的逻辑本身就耗时，导致频繁执行 getFetchParam 函数本身的开销明显增大。

我们对这种情况提供了一种优化方案，利用 shouldFetch 主动阻止不必要的取数，具体参考 组件阻止自动取数。

## 组件取数事件钩子

如果想在取数后做一些更新，但不想触发额外的重渲染，可以在“组件取数事件钩子”里做。

### 取数完成后

afterFetch 钩子在取数完成后执行：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  afterFetch: ({ data, context, componentInstance }) => {
    context.updateComponentById(componentInstance.id, (each) =>
      fp.set("props.value", "newValue", each)
    );
  },
};
```

- data ：取数结果，即 fetcher 的返回值。
- context ：上下文。
- componentInstance ：组件实例信息。
- componentMeta ：组件元信息。

在取数钩子触发的数据流变更事件（比如 updateComponentById ）不会触发额外重渲染，其渲染时机与取数结束后时机合并。

## 组件定时取数

对于需要定时刷新重新取数的实时数据，可以配置 autoFetchInterval 实现定时自动取数功能：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  autoFetchInterval: () => 1000,
};
```

- autoFetchInterval ：自动重新取数间隔，单位 ms，不设置则无此功能。

## 组件强制取数

正常情况取数参数变化才会重新取数，但如有强制取数的诉求，可执行 forceFetch ：

```jsx
import { useDesigner } from "@alife/bi-designer";
export default () => {
  const { forceFetch } = useDesigner();

  // 指定某个组件重新取数
  // forceFetch('jtw4x8ns')
};
```

- forceFetch ：强制取数函数，传参为组件 ID。

## 组件筛选

### 触发筛选行为

任何组件都可以作为筛选条件，只要实现 onFilterChange 接口就具备了筛选能力，通过 filterValue 可以拿到当前组件筛选值，下面创建一个具有筛选功能的组件：

```jsx
import { useDesigner } from "@alife/bi-designer";
const SelectFilter = () => {
  const { filterValue, onFilterChange } = useDesigner();
  return (
    <span>
      <Select value={filterValue} onChange={onFilterChange}>
        // ...
      </Select>
    </span>
  );
};
```

当组件触发 onFilterChange 时则视为触发筛选，其作用的组件会触发 组件取数。

### 通过表达式设置任意 key

注意， onFilterChange 与 filterValue 可以映射到组件任意 key，只需要如下定义：

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

组件的 props.onChange 与 props.value 就拥有了 onFilterChange 与 filterValue 的能力。

### 设置筛选作用的组件

那么如何定义被作用的组件呢？由于筛选关联属于运行时能力，我们需要用到 组件运行时配置 功能。

运行时能力中，筛选关联功能属于 ComponentMeta.eventConfigs 中 filterFetch 部分能力 ，即筛选条件的作用范围，在列表中的组件会在当前组件触发 onFilterChange 时触发取数：

```jsx
import { Interfaces, createComponentInstancesArray } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  eventConfigs: ({ componentInstance, pageSchema }) =>
    createComponentInstancesArray(pageSchema?.componentInstances)
      // 找到画布中所有 name-list 组件
      ?.filter((each) => each.componentName === "name-list")
      ?.map((each) => ({
        // 事件类型是筛选触发取数
        type: "filterFetch",
        // 条件由当前组件触发
        source: componentInstance.id,
        // 作用于找到的 name-list 组件
        target: each.id,
      })),
};
```

上面的例子，我们通过 eventConfigs 将所有组件名为 name-list 都做了绑定，当然你也可以根据 componentInstance.props 根据组件当前配置来绑定，自由使用。

同理，还可以实现条件反向绑定，只要设置 source 和 target 即可，source 是触发 onFilterChange 的组件，target 是被作用取数的组件。

注意： componentInstances 包含所有组件，包括自身及 root 根节点，如果要绑定所有组件，一般情况下需要排除掉自身和 root 节点：

```jsx
{
  eventConfigs: componentInstances?.filter(
    // 不选中 root 节点
    (each) =>
      each.componentName !== "root" &&
      // 不选中自己
      each.componentId === componentInstance.id
  );
  // ...
}
```

### 传递额外筛选信息

考虑到筛选条件正向、反向绑定，或者同一个筛选条件组件针对同一个组件有多个不同筛选功能，bi-designer 支持 source 与 target 重复的多对多，比如：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  eventConfigs: ({ componentInstance, pageSchema }) => [
    {
      type: "filterFetch",
      source: componentInstance.id,
      target: 1,
      payload: "作用于取数参数",
    },
    {
      type: "filterFetch",
      source: componentInstance.id,
      target: 1,
      payload: "作用于字段筛选",
    },
  ],
};
```

在上面的例子中，我们可以将当前组件连续绑定多个同一个目标（ target ），为了区分作用，我们可以申明 payload ，这个 payload 最终会传递到 target 组件的 getFetchParam.filters 参数中，可以通过 eachFilter.payload 访问，具体见文档 组件取数 。

对于同一个组件连续绑定多个相同目标组件场景较少，但对于 A 组件配置绑定 B，B 组件配置被 A 绑定的场景还是很多的。

### 筛选依赖

筛选条件间存在的依赖关系称为筛选依赖。

#### 筛选 Ready 依赖

筛选 Ready 依赖由 filterReady 定义：

```jsx
import { Interfaces, createComponentInstancesArray } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  eventConfigs: ({ componentInstance, pageSchema }) =>
    createComponentInstancesArray(pageSchema?.componentInstances)
      // 找到画布中所有 input 组件
      ?.filter((each) => each.componentName === "input")
      ?.map((each) => ({
        type: "filterReady",
        source: each.id,
        target: componentInstance.id,
      })),
};
```

target 依赖 source ，当筛选条件 source 变化时， target 组件的筛选就会失效并且被置空。

- source ：一旦触发 onFilterChange 。
- target ：组件筛选 Ready 就置为 false，且 filterValue 置为 null。

#### 筛选 Value 依赖

筛选 Value 依赖由 filterValue 定义：

```jsx
import { Interfaces, createComponentInstancesArray } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  eventConfigs: ({ componentInstance, pageSchema }) =>
    createComponentInstancesArray(pageSchema?.componentInstances)
      // 找到画布中所有 input 组件
      ?.filter((each) => each.componentName === "input")
      ?.map((each) => ({
        type: "filterValue",
        source: each.id,
        target: componentInstance.id,
      })),
};
```

target 依赖 source ，当筛选条件 source 变化时， target 组件的 filterValue 将被赋值为 from 的 filterValue 。

- source ：一旦触发 onFilterChange 。
- target ：组件 filterValue 就会被置为 source 组件 filterValue 的值。

## 组件筛选默认值

默认情况下，组件筛选器的默认值为 undefined ，并且后续筛选条件变更由组件 onFilterChange 行为控制（具体可以看 组件筛选 文档）。

但如果配置了筛选默认值，或者默认从 URL 参数等，让组件筛选拥有默认值，这个需求也是非常合理的，可以通过 defaultFilterValue 定义：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  // 组件筛选默认值
  defaultFilterValue: ({ componentInstance }) =>
    componentInstance.props.defaultFilterValue,
};
```

注意此为筛选条件默认值，后续筛选条件变化不会再受此参数控制。

## 组件主题风格

组件可以通过两种方式读取主题风格配置：

1. JS：通过例如 props.theme.primaryColor 读取。
2. CSS：通过例如 var(--primaryColor) 读取。

### JS 模式

```jsx
import { themeSelector, useDesigner } from "@alife/bi-designer";
const Component: Interfaces.ComponentElement = () => {
  const { theme } = useDesigner(themeSelector());

  return <div style={{ color: theme?.primaryColor }}>文本</div>;
};
```

### CSS 模式

```jsx
import "./index.scss";
const Component: Interfaces.ComponentElement = () => {
  return <div className="custom-text">文本</div>;
};
```

```css
.custom-text {
  color: var(--primaryColor);
}
```

CSS 模式的 Key 与 JS 变量的 Key 完全相同。

## 组件国际化

组件配置通过 JSExpression 方式使用国际化：

```jsx
const defaultPageSchema: Interfaces.PageSchema = {
  componentInstances: {
    test: {
      id: "tg43g42f",
      componentName: "expressionComponent",
      props: {
        variable: {
          type: "JSExpression",
          value: 'this.i18n["中国"]',
        },
      },
    },
  },
};
```

通过 this.i18n 即可根据 key 访问国际化内容。

- 国际化内容配置 - 配置国际化。
- JSExpression 说明 - JSExpression。

## 组件配置订正

当组件实例版本低于最新版本号时，说明产生了回滚，也会按照顺序依次订正。

> 注：需要考虑数据回滚的组件，在发布前要把 undo 逻辑写好并测试后提前上线，之后再进行项目正式上线，以保证回滚后可以正确执行 undo 。
>
> 组件配置订正在 ComponentMeta.revises 中定义：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  revises: [
    {
      version: 1,
      redo: async (prevProps) => {
        return prevProps;
      },
      undo: async (prevProps) => {
        return prevProps;
      },
    },
  ],
};
```

- version ：订正的版本号。
- redo ：升级到这个版本订正逻辑。
- undo ：回退到这个版本订正逻辑。
- Return ：新的组件 props 。

## 组件吸顶

### 全局吸顶

组件吸顶通过 ComponentMeta.fixTop 定义：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  fixTop: ({ componentInstance }) => true,
};
```

- 配置 fixTop 后即可吸顶，不需要组件做额外支持。
- 如果置顶的组件具有筛选功能，吸顶后仍具有筛选功能。

### 组件内吸顶

通过 ComponentMeta.fixTopInsideParent 来设置组件在父容器内吸顶。

- 平滑取消滚动： 设置 ComponentMeta.smoothlyFadeOut 可以实现该效果。
- 直接让组件回到原位置： 不需要任何配置。

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  fixTop: () => true,
  fixTopInsideParent: () => true,
  smoothlyFadeOut: () => true,
};
```

### 设置吸顶组件自定义样式

设置 ComponentMeta.getFixTopStyle 来自定义组件吸顶后的样式，一般拿来设置 zIndex 。

```typescript
type getFixTopStyle = (componentInfo: {
  componentInstance: ComponentInstance;
  componentMeta: ComponentMeta;
  dom: HTMLElement;
  context: any;
}) => React.CSSProperties;
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  getFixTopStyle: () => ({
    zIndex: 1000000,
  }),
};
```

## 组件渲染完成标识

**默认组件渲染完毕不需要主动上报**，下面是自动上报机制：

- 组件 initFetch 为 false 时，组件 DOM Ready 作为渲染完成时机。
- 组件 initFetch 为 true 时，组件取数完毕后且 DOM Ready 作为渲染完成时机。

### 主动上报渲染完成标识

对于特殊组件，比如 DOM 渲染完毕不是时机加载完毕时机时，可以选择主动上报：

```jsx
import { Interfaces, useDesigner } from "@alife/bi-designer";
const customOnRendered: Interfaces.ComponentElement = () => {
  const { onRendered } = useDesigner();

  return <div onClick={onRendered}>点我后这个组件才算渲染完成</div>;
};

const customOnRenderedMeta: Interfaces.ComponentMeta = {
  manualOnRendered: true,
};
```

- manualOnRendered ：设置为 true 时禁用自动上报。
- onRendered ：主动上报组件渲染完毕，仅第一次生效。

## 组件阻止自动取数

对于需要精细化控制取数时机的场景，可以使用 shouldFetch 控制组件取数时机：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  shouldFetch: ({
    prevComponentInstance,
    nextComponentInstance,
    prevFilters,
    nextFilters,
    componentMeta,
    context,
  }) => true,
};
```

shouldFetch 返回 false 则阻止自动取数逻辑，不会执行到 getFetchParam 与 fetcher 。

- prevComponentInstance ：上一次组件实例信息。
- nextComponentInstance ：下一次组件实例信息。
- prevFilters ：上一次筛选条件信息。
- nextFilters ：下一次筛选条件信息。
- componentMeta ：组件元信息。
- context ：上下文。

对于取数参数没变化时仍要重新取数，参考 组件强制取数。

- shouldFetch 不会阻塞 组件强制取数、组件定时自动取数、组件主动取数。
- shouldFetch 会阻塞 initFetch=true 初始化取数。

## 组件按需取数

默认 bi-designer 取数是全量并发的，也就是无论组件是否出现在可视区域内，都会第一时间取数，但取数结果不会造成非可视区域组件的刷新。

如果考虑到浏览器请求并发限制，需要优先发起可视区域内组件的取数，可以将 fetchOnlyActive 设置为 true ：

```jsx
const componentMeta = {
  componentName: "line-chart",
  fetchonlyActive: () => true,
};
```

当组件开启此功能后：

- 在可视区域内组件才会发起自动取数。
- 当组件从非可视区域出现在可视区域时，如果需要则会自动发起取数。

## 组件回调事件

组件回调可以触发事件，通过运行时配置 ComponentMeta.eventConfigs 中 callback 定义：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  eventConfigs: ({ componentInstance, pageSchema }) => [
    {
      type: "callback",
      callbackName: "onClick",
    },
  ],
};
```

- callbackName ：回调函数名。

定义了回调时机后，我们可以触发一些 action 实现自定义效果，在后面的 更新组件 Props、更新组件配置、更新取数参数 了解详细内容。

## 事件 - 更新组件 Props

更新组件配置属于 Action 之 setProps ：

```jsx
import { Interfaces } from '@alife/bi-designer'
const componentMeta: Interfaces.ComponentMeta = {
  eventConfigs: ({ componentInstance, pageSchema }) => [{
    type: 'callback',
    callbackName: 'onClick',
    source: componentInstance.id,
    target: componentInstance.id
    action: {
      type: 'setProps',
      setProps: (props, eventArgs) => {
        return {
          ...props,
          color: 'red'
        }
      }
    }
  }]
}
```

如上配置，效果是将 props.color 设置为 red 。

eventArgs 是事件参数，比如 onClick 如下调用：

```jsx
props.onClick("jack", 19);
```

```jsx
setProps: (props, eventArgs) => {
  return {
    ...props,
    name: eventArgs[0],
    age: eventArgs[1],
  };
};
```

如果有多个事件同时作用于同一个组件的 setProps ，则 setProps 函数会依次触发多次。

## 事件 - 更新取数参数

更新组件取数参数属于 Action 之 setFetchParam ：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  eventConfigs: ({ componentInstance, pageSchema }) => [
    {
      type: "callback",
      callbackName: "onClick",
      action: {
        type: "setFetchParam",
        setFetchParam: (param, eventArgs) => {
          return {
            ...param,
            count: true,
          };
        },
      },
    },
  ],
};
```

如上配置，效果是在取数参数中增加一项 count：true 。

## 事件 - 更新筛选条件

更新筛选条件属于 Action 之 setFilterValue ：

```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  eventConfigs: ({ componentInstance, pageSchema }) => [
    {
      type: "callback",
      callbackName: "onClick",
      action: {
        type: "setFilterValue",
        setFilterValue: (filterValue, eventArgs) => {
          return "uv";
        },
      },
    },
  ],
};
```

如上配置，效果是将目标组件的筛选条件值改为 uv 。

## 总结

以上就是结合了通用搭建与 BI 特色功能的搭建引擎对组件功能的支持，如果你对功能、或者 API 有任何问题或建议，欢迎联系我。

> 讨论地址是：[精读《数据搭建引擎 bi-designer API-组件》· Issue #269 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/269)

**如果你想参与讨论，请 [点击这里](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)）
