2

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

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

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

组件加载

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


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

const componentMeta: Interfaces.ComponentMeta = {

element: () => <div />,

};

异步加载

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


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

const componentMeta: Interfaces.ComponentMeta = {

// 懒加载

element: React.lazy(async () => import("./real-component")),

};

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

组件异步、错误处理

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

异步加载


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 来处理异步中的状态。

错误处理


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 即可:


export const yourComponentMeta: Interfaces.ComponentMeta = {

componentName: "yourComponent",

element: YourComponent,

isContainer: true,

};

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


const YourComponent = ({ children }) => {

return <div>{children}</div>;

};

多插槽容器组件

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

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

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

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


export const cardComponentMeta: Interfaces.ComponentMeta = {

componentName: "card",

element: CardComponent,

};

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


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 容器,可以是这么实现的:


const Div: Interfaces.ComponentElement = ({ children, style }) => {

return (

<div style={{ width: "100%", height: "100%", ...style }}>{children}</div>

);

};

Tabs 容器组件

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


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% 的宽高:


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 中定义:


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

const componentMeta: Interfaces.ComponentMeta = {

props: [

{

name: "title",

defaultValue: "标题",

},

],

};

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


interface MetaProps {

/**

* 属性名

*/

name: string;

/**

* 属性类型

*/

type?: string;

/**

* 属性描述

*/

description?: string;

/**

* 默认值

*/

defaultValue?: any;

}

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

组件配置表单

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


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 中定义:


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

const componentMeta: Interfaces.ComponentMeta = {

onPropsChange: ({ prevProps, currentProps, componentMeta }) => {

return {

...currentProps,

color: "red",

};

},

};
  • prevProps :上一次组件配置。
  • currentProps :当前组件配置。
  • componentMeta :组件元信息。
  • Return :新的组件配置。

跨组件关联配置更新

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


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 设置:


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

const componentMeta: Interfaces.ComponentMeta = {

hide: ({ componentInstance, mode }) => true,

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

属性值类型 - JSSlot

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


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 参数设置为自定义函数。


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 参数设置为自定义表达式。


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 函数将状态持久化到配置中:


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 实现:


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,可选。

动态组件嵌套

动态组件可以任意嵌套:


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 时不渲染

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


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 抛出异常即可暂停取数:


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 访问到,组件和容器层都可以拦截:


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 中定义


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 拿到数据了:


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 可以主动取数:


const NameList: Interfaces.ComponentElement = ({ fetchData }) => {

const { fetchData } = useDesigner();

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

};
  • fetchData :主动取数函数,调用后可以立即重新取数。

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

自定义取数参数

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


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 由组件自行处理。


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 可以主动取消取数:


const NameList: Interfaces.ComponentElement = ({ cancelFetch }) => {

const { cancelFetch } = useDesigner();

return <button onClick={cancelFetch}>取消取数</button>;

};
  • cancelFetch :取消取数函数,调用后立即生效。取数完成后再调用则无作用。

优化取数性能

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


import { Interfaces } from "@alife/bi-design";

const componentMeta: Interfaces.ComponentMeta = {

getFetchParam: ({ componentInstance }) => {

// 引擎会对返回值进行深对比

return { name: componentInstance?.props?.name };

},

};

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

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

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

组件取数事件钩子

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

取数完成后

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


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 实现定时自动取数功能:


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

const componentMeta: Interfaces.ComponentMeta = {

autoFetchInterval: () => 1000,

};
  • autoFetchInterval :自动重新取数间隔,单位 ms,不设置则无此功能。

组件强制取数

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


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

export default () => {

const { forceFetch } = useDesigner();

  

// 指定某个组件重新取数

// forceFetch('jtw4x8ns')

};
  • forceFetch :强制取数函数,传参为组件 ID。

组件筛选

触发筛选行为

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


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,只需要如下定义:


{

props: {

onChange: {

type: "JSExpression",

value: "this.onFilterChange"

},

value: {

type: "JSExpression",

value: "this.filterValue"

}

}

}

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

设置筛选作用的组件

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

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


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 节点:


{

eventConfigs: componentInstances?.filter(

// 不选中 root 节点

(each) =>

each.componentName !== "root" &&

// 不选中自己

each.componentId === componentInstance.id

);

// ...

}

传递额外筛选信息

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


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 定义:


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 定义:


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 定义:


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 模式


import { themeSelector, useDesigner } from "@alife/bi-designer";

const Component: Interfaces.ComponentElement = () => {

const { theme } = useDesigner(themeSelector());

  

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

};

CSS 模式


import "./index.scss";

const Component: Interfaces.ComponentElement = () => {

return <div className="custom-text">文本</div>;

};

.custom-text {

color: var(--primaryColor);

}

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

组件国际化

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


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 中定义:


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 定义:


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

const componentMeta: Interfaces.ComponentMeta = {

fixTop: ({ componentInstance }) => true,

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

-

组件内吸顶

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

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

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

const componentMeta: Interfaces.ComponentMeta = {

fixTop: () => true,

fixTopInsideParent: () => true,

smoothlyFadeOut: () => true,

};

设置吸顶组件自定义样式

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


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


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 控制组件取数时机:


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 :


const componentMeta = {

componentName: "line-chart",

fetchonlyActive: () => true,

};

当组件开启此功能后:

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

组件回调事件

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


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

const componentMeta: Interfaces.ComponentMeta = {

eventConfigs: ({ componentInstance, pageSchema }) => [

{

type: "callback",

callbackName: "onClick",

},

],

};
  • callbackName :回调函数名。

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

事件 - 更新组件 Props

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


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 如下调用:


props.onClick("jack", 19);

setProps: (props, eventArgs) => {

return {

...props,

name: eventArgs[0],

age: eventArgs[1],

};

};

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

事件 - 更新取数参数

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


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 :


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

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

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

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

黄子毅
7k 声望9.5k 粉丝