当前期刊数: 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 处理了组件抛出的任何错误。
容器组件
容器元素可以被拖入子元素,只要将 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>; };
|
多插槽容器组件
多插槽容器即一个容器内部有多个位置可响应拖拽。
实现多插槽容器组件注意两点即可:
- 这个大容器组件本身不为容器类型,因为我们要拖入到子元素,不需要拖入到它自己本身。
- 内部通过 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, 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";
const ComponentWithJSSlot: Interfaces.ComponentElement = ({ header }) => { return ( <div> header 元素: {header} </div> ); };
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";
const FunctionComponent: Interfaces.ComponentElement = ({ onClick }) => { return <div onClick={onClick} />; };
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";
const ExpressionComponent: Interfaces.ComponentElement = ({ variable }) => { return <div>JSExpression:{variable}</div>; };
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>; }
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") { 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 = { autoFetch: ({ componentInstance, componentMeta }) => true, initFetch: ({ componentInstance, componentMeta }) => true, getFetchParam: ({ componentInstance, componentMeta, filters, context }) => { return { name: componentInstance?.props?.name }; }, fetcher: async ({ componentMeta, param, context }) => { 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, });
}, [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 }; }, };
|
但是下面两种情况可能会产生性能问题:
- 返回值数据结构非常大,导致频繁 deepEqual 开销明显增大。
- 生成取数参数的逻辑本身就耗时,导致频繁执行 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 :强制取数函数,传参为组件 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) ?.filter((each) => each.componentName === "name-list") ?.map((each) => ({ type: "filterFetch", source: componentInstance.id, 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( (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) ?.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) ?.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, };
|
注意此为筛选条件默认值,后续筛选条件变化不会再受此参数控制。
组件主题风格
组件可以通过两种方式读取主题风格配置:
- JS:通过例如 props.theme.primaryColor 读取。
- 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", }, ], };
|
定义了回调时机后,我们可以触发一些 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 许可证)