165. 精读《数据搭建引擎 bi-designer API-组件》

2022-03-14 17:45:05 浏览数 (1)

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

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

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

组件加载

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

代码语言:javascript复制
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  element: () => <div />,
};

异步加载

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

代码语言:javascript复制
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  // 懒加载
  element: React.lazy(async () => import("./real-component")),
};

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

组件异步、错误处理

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

异步加载

代码语言:javascript复制
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 来处理异步中的状态。

错误处理

代码语言:javascript复制
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 即可:

代码语言:javascript复制
export const yourComponentMeta: Interfaces.ComponentMeta = {
  componentName: "yourComponent",
  element: YourComponent,
  isContainer: true,
};

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

代码语言:javascript复制
const YourComponent = ({ children }) => {
  return <div>{children}</div>;
};

多插槽容器组件

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

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

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

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

代码语言:javascript复制
export const cardComponentMeta: Interfaces.ComponentMeta = {
  componentName: "card",
  element: CardComponent,
};

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

代码语言:javascript复制
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 容器,可以是这么实现的:

代码语言:javascript复制
const Div: Interfaces.ComponentElement = ({ children, style }) => {
  return (
    <div style={{ width: "100%", height: "100%", ...style }}>{children}</div>
  );
};

Tabs 容器组件

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

代码语言:javascript复制
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% 的宽高:

代码语言:javascript复制
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 中定义:

代码语言:javascript复制
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  props: [
    {
      name: "title",
      defaultValue: "标题",
    },
  ],
};

Props 描述了组件入参信息,包括:

代码语言:javascript复制
interface MetaProps {
  /**
   * 属性名
   */
  name: string;
  /**
   * 属性类型
   */
  type?: string;
  /**
   * 属性描述
   */
  description?: string;
  /**
   * 默认值
   */
  defaultValue?: any;
}

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

组件配置表单

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

代码语言:javascript复制
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 中定义:

代码语言:javascript复制
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  onPropsChange: ({ prevProps, currentProps, componentMeta }) => {
    return {
      ...currentProps,
      color: "red",
    };
  },
};
  • prevProps :上一次组件配置。
  • currentProps :当前组件配置。
  • componentMeta :组件元信息。
  • Return :新的组件配置。

跨组件关联配置更新

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

代码语言:javascript复制
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 设置:

代码语言:javascript复制
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  hide: ({ componentInstance, mode }) => true,
};
  • componentInstance :组件实例信息。
  • mode :当前模式,比如组件仅编辑模式隐藏,可以判断 ({ mode }) => mode === 'edit' 。

属性值类型 - JSSlot

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

代码语言:javascript复制
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 参数设置为自定义函数。

代码语言:javascript复制
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 参数设置为自定义表达式。

代码语言:javascript复制
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 函数将状态持久化到配置中:

代码语言:javascript复制
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 实现:

代码语言:javascript复制
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,可选。

动态组件嵌套

动态组件可以任意嵌套:

代码语言:javascript复制
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 时不渲染

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

代码语言:javascript复制
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 抛出异常即可暂停取数:

代码语言:javascript复制
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 访问到,组件和容器层都可以拦截:

代码语言:javascript复制
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 中定义

代码语言:javascript复制
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 拿到数据了:

代码语言:javascript复制
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 可以主动取数:

代码语言:javascript复制
const NameList: Interfaces.ComponentElement = ({ fetchData }) => {
  const { fetchData } = useDesigner();
  return <button onClick={fetchData}>主动取数</button>;
};
  • fetchData :主动取数函数,调用后可以立即重新取数。

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

自定义取数参数

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

代码语言:javascript复制
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 由组件自行处理。

代码语言:javascript复制
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 可以主动取消取数:

代码语言:javascript复制
const NameList: Interfaces.ComponentElement = ({ cancelFetch }) => {
  const { cancelFetch } = useDesigner();
  return <button onClick={cancelFetch}>取消取数</button>;
};
  • cancelFetch :取消取数函数,调用后立即生效。取数完成后再调用则无作用。

优化取数性能

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

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

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

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

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

组件取数事件钩子

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

取数完成后

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

代码语言:javascript复制
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 实现定时自动取数功能:

代码语言:javascript复制
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  autoFetchInterval: () => 1000,
};
  • autoFetchInterval :自动重新取数间隔,单位 ms,不设置则无此功能。

组件强制取数

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

代码语言:javascript复制
import { useDesigner } from "@alife/bi-designer";
export default () => {
  const { forceFetch } = useDesigner();

  // 指定某个组件重新取数
  // forceFetch('jtw4x8ns')
};
  • forceFetch :强制取数函数,传参为组件 ID。

组件筛选

触发筛选行为

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

代码语言:javascript复制
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,只需要如下定义:

代码语言:javascript复制
{
  props: {
    onChange: {
      type: "JSExpression",
      value: "this.onFilterChange"
    },
    value: {
      type: "JSExpression",
      value: "this.filterValue"
    }
  }
}

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

设置筛选作用的组件

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

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

代码语言:javascript复制
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 节点:

代码语言:javascript复制
{
  eventConfigs: componentInstances?.filter(
    // 不选中 root 节点
    (each) =>
      each.componentName !== "root" &&
      // 不选中自己
      each.componentId === componentInstance.id
  );
  // ...
}

传递额外筛选信息

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

代码语言:javascript复制
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 定义:

代码语言:javascript复制
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 定义:

代码语言:javascript复制
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 定义:

代码语言:javascript复制
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 模式

代码语言:javascript复制
import { themeSelector, useDesigner } from "@alife/bi-designer";
const Component: Interfaces.ComponentElement = () => {
  const { theme } = useDesigner(themeSelector());

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

CSS 模式

代码语言:javascript复制
import "./index.scss";
const Component: Interfaces.ComponentElement = () => {
  return <div className="custom-text">文本</div>;
};
代码语言:javascript复制
.custom-text {
  color: var(--primaryColor);
}

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

组件国际化

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

代码语言:javascript复制
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 中定义:

代码语言:javascript复制
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 定义:

代码语言:javascript复制
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  fixTop: ({ componentInstance }) => true,
};
  • 配置 fixTop 后即可吸顶,不需要组件做额外支持。
  • 如果置顶的组件具有筛选功能,吸顶后仍具有筛选功能。

组件内吸顶

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

  • 平滑取消滚动:设置 ComponentMeta.smoothlyFadeOut 可以实现该效果。
  • 直接让组件回到原位置:不需要任何配置。
代码语言:javascript复制
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  fixTop: () => true,
  fixTopInsideParent: () => true,
  smoothlyFadeOut: () => true,
};

设置吸顶组件自定义样式

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

代码语言:javascript复制
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 渲染完毕不是时机加载完毕时机时,可以选择主动上报:

代码语言:javascript复制
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 控制组件取数时机:

代码语言:javascript复制
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 :

代码语言:javascript复制
const componentMeta = {
  componentName: "line-chart",
  fetchonlyActive: () => true,
};

当组件开启此功能后:

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

组件回调事件

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

代码语言:javascript复制
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
  eventConfigs: ({ componentInstance, pageSchema }) => [
    {
      type: "callback",
      callbackName: "onClick",
    },
  ],
};
  • callbackName :回调函数名。

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

事件 - 更新组件 Props

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

代码语言:javascript复制
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 如下调用:

代码语言:javascript复制
props.onClick("jack", 19);
代码语言:javascript复制
setProps: (props, eventArgs) => {
  return {
    ...props,
    name: eventArgs[0],
    age: eventArgs[1],
  };
};

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

事件 - 更新取数参数

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

代码语言:javascript复制
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 :

代码语言:javascript复制
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 特色功能的搭建引擎对组件功能的支持

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)

0 人点赞