当前期刊数: 165

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

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

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

组件加载

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

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

异步加载

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

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

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

组件异步、错误处理

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

异步加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 来处理异步中的状态。

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 即可:

1
2
3
4
5
export const yourComponentMeta: Interfaces.ComponentMeta = {
componentName: "yourComponent",
element: YourComponent,
isContainer: true,
};

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

1
2
3
const YourComponent = ({ children }) => {
return <div>{children}</div>;
};

多插槽容器组件

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

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

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

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

1
2
3
4
export const cardComponentMeta: Interfaces.ComponentMeta = {
componentName: "card",
element: CardComponent,
};

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 容器,可以是这么实现的:

1
2
3
4
5
const Div: Interfaces.ComponentElement = ({ children, style }) => {
return (
<div style={{ width: "100%", height: "100%", ...style }}>{children}</div>
);
};

Tabs 容器组件

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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% 的宽高:

1
2
3
4
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 中定义:

1
2
3
4
5
6
7
8
9
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
props: [
{
name: "title",
defaultValue: "标题",
},
],
};

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface MetaProps {
/**
* 属性名
*/
name: string;
/**
* 属性类型
*/
type?: string;
/**
* 属性描述
*/
description?: string;
/**
* 默认值
*/
defaultValue?: any;
}

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

组件配置表单

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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 中定义:

1
2
3
4
5
6
7
8
9
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
onPropsChange: ({ prevProps, currentProps, componentMeta }) => {
return {
...currentProps,
color: "red",
};
},
};
  • prevProps :上一次组件配置。
  • currentProps :当前组件配置。
  • componentMeta :组件元信息。
  • Return :新的组件配置。

跨组件关联配置更新

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 设置:

1
2
3
4
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
hide: ({ componentInstance, mode }) => true,
};
  • componentInstance :组件实例信息。
  • mode :当前模式,比如组件仅编辑模式隐藏,可以判断 ({ mode }) => mode === ‘edit’ 。

属性值类型 - JSSlot

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 参数设置为自定义函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 参数设置为自定义表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 函数将状态持久化到配置中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 实现:

1
2
3
4
5
6
7
8
9
10
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,可选。

动态组件嵌套

动态组件可以任意嵌套:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 时不渲染

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 抛出异常即可暂停取数:

1
2
3
4
5
6
7
8
9
10
11
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 访问到,组件和容器层都可以拦截:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 中定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 拿到数据了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 可以主动取数:

1
2
3
4
const NameList: Interfaces.ComponentElement = ({ fetchData }) => {
const { fetchData } = useDesigner();
return <button onClick={fetchData}>主动取数</button>;
};
  • fetchData :主动取数函数,调用后可以立即重新取数。

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

自定义取数参数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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 由组件自行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
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 可以主动取消取数:

1
2
3
4
const NameList: Interfaces.ComponentElement = ({ cancelFetch }) => {
const { cancelFetch } = useDesigner();
return <button onClick={cancelFetch}>取消取数</button>;
};
  • cancelFetch :取消取数函数,调用后立即生效。取数完成后再调用则无作用。

优化取数性能

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

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

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

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

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

组件取数事件钩子

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

取数完成后

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

1
2
3
4
5
6
7
8
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 实现定时自动取数功能:

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

组件强制取数

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

1
2
3
4
5
6
7
import { useDesigner } from "@alife/bi-designer";
export default () => {
const { forceFetch } = useDesigner();

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

组件筛选

触发筛选行为

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

1
2
3
4
5
6
7
8
9
10
11
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,只需要如下定义:

1
2
3
4
5
6
7
8
9
10
11
12
{
props: {
onChange: {
type: "JSExpression",
value: "this.onFilterChange"
},
value: {
type: "JSExpression",
value: "this.filterValue"
}
}
}

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

设置筛选作用的组件

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 节点:

1
2
3
4
5
6
7
8
9
10
{
eventConfigs: componentInstances?.filter(
// 不选中 root 节点
(each) =>
each.componentName !== "root" &&
// 不选中自己
each.componentId === componentInstance.id
);
// ...
}

传递额外筛选信息

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 定义:

1
2
3
4
5
6
7
8
9
10
11
12
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 定义:

1
2
3
4
5
6
7
8
9
10
11
12
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 定义:

1
2
3
4
5
6
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 模式

1
2
3
4
5
6
import { themeSelector, useDesigner } from "@alife/bi-designer";
const Component: Interfaces.ComponentElement = () => {
const { theme } = useDesigner(themeSelector());

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

CSS 模式

1
2
3
4
import "./index.scss";
const Component: Interfaces.ComponentElement = () => {
return <div className="custom-text">文本</div>;
};
1
2
3
.custom-text {
color: var(--primaryColor);
}

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

组件国际化

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 定义:

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

组件内吸顶

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

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

设置吸顶组件自定义样式

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

1
2
3
4
5
6
7
8
9
10
11
12
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 渲染完毕不是时机加载完毕时机时,可以选择主动上报:

1
2
3
4
5
6
7
8
9
10
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 控制组件取数时机:

1
2
3
4
5
6
7
8
9
10
11
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 :

1
2
3
4
const componentMeta = {
componentName: "line-chart",
fetchonlyActive: () => true,
};

当组件开启此功能后:

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

组件回调事件

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

1
2
3
4
5
6
7
8
9
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
eventConfigs: ({ componentInstance, pageSchema }) => [
{
type: "callback",
callbackName: "onClick",
},
],
};
  • callbackName :回调函数名。

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

事件 - 更新组件 Props

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 如下调用:

1
props.onClick("jack", 19);
1
2
3
4
5
6
7
setProps: (props, eventArgs) => {
return {
...props,
name: eventArgs[0],
age: eventArgs[1],
};
};

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

事件 - 更新取数参数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

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

关注 前端精读微信公众号

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


本站由 钟意 使用 Stellar 1.28.1 主题创建。
又拍云 提供CDN加速/云存储服务
vercel 提供托管服务
湘ICP备2023019799号-1
总访问 次 | 本页访问