乐趣区

关于前端:精读数据搭建引擎-bidesigner-API设计器

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

bi-designer 目前没有开源,因而文中应用的公有 npm 源 @alife/bi-designer 是无奈在公网拜访的。

本文介绍 bi-designer 设计器的应用 API。

bi-designer 设计有如下几个特点:

  • 心智对立:编辑模式与渲染模式对立
  • 通用搭建:反对接入任意通用 npm 组件
  • 低入侵:围绕数据分析能力做了加强,但对组件代码无入侵

渲染画布

做搭建,第一步是将画布渲染进去,须要用到 DesignerCanvas 组件:

import {Designer, Canvas} from '@alife/bi-designer'export () => (  <Designer>    <Canvas />  </Designer>)
  • Designer:数据容器,用于治理渲染引擎数据流。

    • 参数 defaultPageSchema:页面 DSL 默认值。
    • 参数 defaultMode:管制编辑渲染状态,edit or render
  • Canvas:渲染画布的所有组件,会依据 DSL 构造将组件一一渲染进去。

编辑模式

编辑模式 = 渲染画布(编辑模式)+ 拓展一些自定义面板。

import {Designer, Canvas} from '@alife/bi-designer'export () => (  <Designer defaultMode="edit">    <div>Header</div>    <Canvas />    <div>Footer</div>  </Designer>)

编辑模式的拓展采纳了 JSX 模式,没有减少任何新的语法,只有搁置任意数量的组件,并将画布 Canvas 摆放在想要的地位即可。

defaultMode 形容了以后引擎所处状态,有 editrender 两个可选值,能够通过 {mode} = useDesigner(modeSelector) 获取。bi-designer 没有对 mode 做任何非凡解决,咱们能够在 panel、组件中判断不同的 mode 走不同的逻辑,以此辨别编辑与渲染态。

页面 DSL 构造

pageSchema 形容了页面 DSL 信息,其构造是一个 Map< 组件 id, 组件实例信息 >

这里对立一下名词:

  • 组件实例信息:componentInstance
  • 组件元信息:componentMeta

那么 pageSchema 的构造大抵如下:

{"componentInstances": {    "1": {      "id": "1",      "componentName": "root",},    "2": {"id": "2",      "parentId": "1",      "componentName": "button",}  }}

依据 id parentId 关系形容了组件父子关系,对于同一个父节点在流式布局下的程序,还会减少 index 标记程序。

注册组件

DSL 形容信息中最重要的是 componentName,为了通知渲染引擎这个组件是什么,咱们须要将组件元信息(componentMetas)传递给 Designer

import {Designer, Canvas, Interfaces} from '@alife/bi-designer'export () => (  <Designer componentMetas={componentMetas}>    <Canvas />  </Designer>)const componentMetas: Interfaces.ComponentMetas = {button: {    componentName: 'button',    element: Button}}

对于 componentMeta 会在下一篇精读具体介绍,这里只阐明两个最重要的属性:

  • componentName:组件名,惟一。
  • element:组件 UI 对象,对应一个 React 组件实例。

留神这里就留下了不少拓展空间,componentMetas 能够存储在服务端,element 能够近程异步加载,也能够在我的项目代码中固化,但传递给渲染引擎的 API 是固定的。

布局

bi-designer 反对流式布局、磁贴布局、自在布局三种模式,通过 Designer.layout 属性定义:

import {Designer, Canvas, Interfaces} from '@alife/bi-designer'import {LayoutMover} from '@alife/bi-designer-stream-layout'export () => (  <Designer layout={LayoutMover}>    <Canvas />  </Designer>)

咱们提供了三种不同的布局包,切换对应的包即可切换布局,你甚至能够再包裹一层,通过代码管制在运行时切换布局。

layout 会包裹在每个组件外层,无论是流式、磁贴还是自在布局,都能够通过附着在每个组件外层来实现。

操作 / 获取画布内容

只有在数据容器 Designer 下,就能够通过 useDesigner() 获取画布信息或者批改画布内容。

举个例子,比方实现组件配置面板,须要获取到 以后选中组件 ,以及实现操作 更新 DSL 中某个组件信息

import {Designer, Canvas, useDesigner, selectedComponentsSelector} from '@alife/bi-designer';const EditPanel = () => {  const { updateComponentById, selectedComponents} =     useDesigner(selectedComponentsSelector());  // 在适合的时候调用 updateComponentById 更新 selectedComponents    // 渲染组件配置表单..}export () => (  <Designer>    <Canvas />    <EditPanel />  </Designer>)

咱们在 Canvas 上面渲染了一个自定义组件 EditPanel 作为组件配置面板,这个配置面板中,最重要的是这块代码:

import {useDesigner, selectedComponentsSelector} from '@alife/bi-designer';const {updateComponentById, selectedComponents} =     useDesigner(selectedComponentsSelector());
  • useDesigner 是 React Hook,导出的函数都是动态的,不会因为画布信息变更而导致组件重渲染。
  • 如果须要监听一些会变动的元素,比方以后选中组件,就须要用 Selector 实现,当这些信息变更时,应用了这些 Selector 的组件也会重渲染,具体 Selector 有很多,比方:

    • selectedComponentsSelector: 以后选中的组件。
    • pageSchemaSelector: 以后画布 DSL。
    • modeSelector: 以后渲染模式。等等。
  • 对画布组件操作有几个重要的静态方法,包含:

    • updateComponentById: 更新某个 id 组件信息。
    • addComponent: 增加组件。
    • deleteComponent: 删除组件。
    • moveComponent: 挪动组件。等等。
  • 除此之外,useDesigner 还提供了很多有用的办法,在用到时再介绍。

主题格调

通过 pageSchema.theme 设置主题格调:

import {Designer} from '@alife/bi-designer'const App = () => (  <Designer    defaultPageSchema={{      theme: { primaryColor: '#333'}    }}  />)

咱们也能够在运行时应用 setTheme 动静批改主题格调,做到动静切换主题:

const {setTheme, theme} = useDesigner();return <Button onClick={() => {setTheme({    ...theme,    primaryColor: '#ffffff'})}} />

这些主题色彩,组件能够通过 css 变量拿到:

.ok-button {color: var(--primaryColor);}

获取组件数据

数据分析引擎中,组件是由数据驱动展现的,这些数据可能来自 OLAP 数据集,或者一般 URL 接口,但无论如何数据都是一个组件重要组成部分,因而对组件的取数与数据操作是 bi-designer 的一个重点。

能够利用 fetchStateSelector 获取任意组件的数据信息,包含取数状态、数据、是否有查问谬误等:

import {useDesigner, fetchStateSelector} from '@alife/bi-designer';const App = () => {  const { fetchState} = useDesigner(fetchStateSelector(componentInstance.id));    console.log(fetchState.isFetching, // 是否在取数中    fetchState.isFilterReady, // 筛选条件是否筹备好了    fetchState.data, // 取数后果    fetchState.error, // 取数谬误,如果取数阶段报错的话)}

bi-designer 将所有组件的取数状态对立治理,因而能够跨组件获取数据信息,实现一些简单需要:比方某些组件配置面板要获取组件取数后果填充配置表单。

组件加载器

组件加载器 ComponentLoader 能够加载任意组件,Canvas 就是基于此实现的。

加载画布中已有组件

通过申明 id 加载一个画布中已有组件,与其共享同一套数据:

import {ComponentLoader} from '@alife/bi-designer'const App = () => {  return <ComponentLoader id="some-id-already-exist" />}

加载一个额定的新组件

如果这个组件不须要响应事件,只是做简略的渲染,那就不须要记录到数据流中,此时仅申明 componentName 即可:

import {ComponentLoader} from '@alife/bi-designer'const App = () => {  return <ComponentLoader componentName="button" />}

但这种形式加载的组件存在如下问题:

  • 其组件 id 不会存储到 pageSchema,后端可能无奈做一些校验。
  • 无奈响应事件,因为事件响应前提是组件信息存在于 pageSchema 中。

加载一个有事件性能的额定新组件

通过申明 idcomponentName 加载一个全新组件,为了在其销毁时做无效清理,请将其 id 记录到 useKeepComponentLoaders 中。

import {ComponentLoader, useDesigner} from '@alife/bi-designer'const App = () => {  const { useKeepComponentLoaders} = useDesigner();  useKeepComponentLoaders(["1"])    return <ComponentLoader id="1" componentName="button" />}

通过此形式加载的组件会在其渲染时记录到 pageSchema 中。

留神,此时 id 与仅写一个 id 时含意不同,这个 id 在以后父组件作用域下惟一就能够。

全屏性能

所有组件实例都能够存在正本,共享一套状态数据,能够通过 ComponentLoader 随时渲染一个组件正本:

import {ComponentLoader} from '@alife/bi-designer'// ... 任意可拿到 componentInstance 处 return (<ComponentLoader id={componentInstance.id} />)

那么全屏就是将组件渲染到一个新容器内,十分 easy。

部分配置笼罩

能够通过 DesignerProvider 实现干预其子元素 useDesigner 获取信息的能力:

import {DesignerProvider, ComponentLoader} from '@alife/bi-designer';// 某个组件内,或者某个 UI 内以 render 模式加载组件 // ...return (<DesignerProvider mode="render">    <ComponentLoader id={id} />  </DesignerProvider>)

举个例子,比方在编辑模式下要全屏预览组件,能够通过 ComponentLoader + id 把某个画布组件实例渲染到弹出的 Modal 中,但问题是以后属于编辑模式,组件还能够被拖拽甚至响应编辑成果,咱们只想让部分变成渲染状态,怎么做呢?

答案就是通过 DesignerProvider 包裹这个 Modal,这个 Modal 外部无论是组件还是其余 Panel 代码通过 const {mode} = useDesigner(modeSelector) 拿到的值都会被强制笼罩为 render

配置国际化

国际化信息在 pageSchema.i18n 定义:

import {Designer} from '@alife/bi-designer'const App = () => (  <Designer    defaultPageSchema={{      i18n: {        "zh-CN": {          你好: "你好",          中国: "中国"},        "en-US": {你好: "Hello",          中国: "China"}      }    }}    defaultLocaleKey="zh-CN"  />)
  • defaultLocaleKey: 默认国际化语言,能够通过 {setLocaleKey} = useDesigner() 动静扭转。

这样在 DSL 中通过形容 JSExpression 表达式的 this.i18n 拜访:

{"componentInstances": {    "1": {      "id": "1",      "componentName": "button",      "props": {        "text": {          "type": "JSExpression",          "value": "this.i18n[' 你好 ']"        }      }    }  }}

容器拓展组件 props

componentMeta.container 能够定义组件外层容器, 但有的时候咱们想在容器做一点事件,比方获取宽高,以 props 的形式传递给子组件。

因为子组件以 children 的形式书写不易拓展,因而提供了 PropsProvider 来拓展子组件拿到的 props:

import {Interfaces, PropsProvider} from '@alife/bi-designer'const ComponentContainer = ({children}) => {return (    // 注入 width 和 height    <PropsProvider width={100} height={100}>      {children}    </PropsProvider>  )}const Element = ({width, height}) => {// width=100  // height=100}const componentMeta: Interfaces.ComponentMeta = {element: Element,  container: ComponentContainer};

下面的例子中,因为 container 注入了 width,因而组件能够通过 props.width 拿到容器注入的值。

撤销重做

撤销重做按钮在基于每个搭建零碎都有,在 bi-designer 的应用形式是这样:

import {useDesigner} from '@alife/bi-designer'export default () => {  const { undo, redo} = useDesigner()    // 撤销调用 undo()  // 重做调用 redo()}

是不是感觉很简略?是的,因为所有值得撤销重做的操作在引擎外部应用了 HistoryManager 治理,因而引擎晓得每一个能够被撤销或者重做的操作,间接调用函数即可。

组件复制

执行 copyComponent 命令即可复制组件,比方:

const App() {  const { copyComponent} = useDesigner()    // 复制组件 copyComponent(componentInstance)  }

copyComponent 的参数别离为:

function copyComponent(componentInstance?: ComponentInstance,  parentId?: string,  index?: number)
  • 如不指定 parentId,默认复制到本人父元素下。
  • 如不指定 index,默认复制到以后元素下方。

组件模版

如果感觉某些组件配置可能被复用,能够在画布组件右上角减少一个“增加到组件模版”按钮,bi-designer 也提供了生成、增加组件模版的办法。

创立组件模版

利用 createCombine 函数从画布中已有组件创立出组件模版,也能够将其生成后果长久化,作为一个固定的组件模版:

const ComponentContainer: Interfaces.InnerComponentElement = ({componentInstance}) => {const { createCombine} = useDesigner();    const setToCombine = React.useCallback(() => {// 创立组件模版    const combine = createCombine(componentInstance.id)  }, [createCombine]);}

createCombine 的参数就是画布中组件的 id

增加组件模版到画布

利用 addCombine 函数将组件模版增加到画布,第一个参数就是下面生成的 combine 对象:

const App = () => {  const { addCombine} = useDesigner();    const addComponent = React.useCallback(() => {// 创立组件模版    const combine = addCombine(combine, parentId)  }, [addCombine]);}

渲染实现标识

当画布中所有组件都实现渲染了,可能要做一些监控上报,或者通知截图软件能够截图了,bi-designer 提供了这种回调机会 onRendered:

import {Designer} from '@alife/bi-designer'const App = () => (  <Designer    onRendered={errors => {      errors.map(each => {        // 谬误组件 id        console.log(each.id)                // 错误信息        console.log(each.error)      })      // 渲染结束    }}  />)
  • errors: 如果有组件代码报错,引擎会吞掉这个谬误保障其余组件失常渲染,并把谬误组件的 id 和错误信息返回到这里。

自定义数据流

如果 useDesigner 提供的数据流无奈满足业务须要,能够通过进行自定义拓展。

1. 拓展字段

举个例子,咱们须要新增一个 edges 字段形容以后画布中有哪些“边节点”:

import {Designer} from '@alife/bi-designer';const App = ({defaultPageSchema}) => (<Designer defaultPageSchema={{    ...defaultPageSchema,    edges: []  }} />)

能够看到,只有任意拓展 pageSchema 即可。

2. 通过 useDesigner 拿到拓展字段

首先定义一个 edgesSelector

import {DesignerState} from '@alife/bi-designer';export const edgesSelector = () => (state: DesignerState) => {return {    // 从 pageSchema.edges 读取 edges    edges: state.pageSchema?.edges as Edge[],  };};

在须要读取的中央联合 useDesigner

import {useDesigner} from '@alife/bi-designer';import {edgesSelector} from './selector'const Panel = () => {  // 自带类型  const { edges} = useDesigner(edgesSelector())}

3. 通过 useDesigner 批改拓展字段

通过 setPageSchema 更新拓展字段:

import {useDesigner} from '@alife/bi-designer';const Panel = () => {  const { setPageSchema} = useDesigner()    const handleChangeEdges = React.useCallback(newEdges => {    setPageSchema(pageSchema => ({      ...pageSchema,      newEdges}))  }, [setPageSchema])}

总结一下,这个拓展字段由业务定义,透过 useDesigner 读与改,使业务数据管理形式更聚合。

存储长期非结构化数据

对于非结构化数据比方组件 ref 是不能存储到数据流的,既不能应用 setPageSchema,也不能调用 updateComponentId 存储到 componentInstance 中。

此时能够利用 temporary 进行长期数据存取,要留神非结构化数据是无奈监听变动的,援用永远放弃不变:

import {useDesigner} from '@alife/bi-designer';const App = () => (  const { temporary} = useDesigner()  // 写  temporary.set('component1', ref)  // 读  console.log(temporary.get('component1')))

temporary 实质是个 Map,所以领有 Map 类型所有语法。

拦挡画布操作

如果你限度某个低配版本只能在画布应用最多 50 个组件,咱们须要阻止画布超过 50 个组件的增加,这个场景能够通过 DesignerProps 生命周期能够对画布操作进行拦挡。

shouldAddComponents() 返回 false 能够阻止画布增加组件:

import {Designer} from '@alife/bi-designer'const App = () => (  <Designer    shouldAddComponents={({addedComponentInstancesArray, pageSchema}) => {// 阻止增加      return false}}  />)
  • addedComponentInstancesArray:增加的组件,ComponentInstance[] 类型。

shouldMoveComponents() 返回 false 能够阻止画布挪动组件:

import {Designer} from '@alife/bi-designer'const App = () => (  <Designer    shouldmoveComponents={({movedComponentInstancesArray, targetComponentInstance, pageSchema}) => {// 阻止挪动      return false}}  />)
  • movedComponentInstancesArray:挪动的组件,ComponentInstance[] 类型。
  • taragetComponentInstance:要挪动到的父组件实例信息,ComponentInstance 类型。

shouldDeleteComponents() 返回 false 能够阻止画布删除组件:

import {Designer} from '@alife/bi-designer'const App = () => (  <Designer    shouldAddComponents={({deletedComponentInstancesArray, pageSchema}) => {// 阻止删除      return false}}  />)
  • deletedComponentInstancesArray:删除的组件,ComponentInstance[] 类型。

仅刷新可视区域组件

默认组件都会以按需加载的形式渲染,即对于不在可视区域的组件,不会触发任何重渲染,以此晋升交互操作的效率,以及首屏速度。

对于筛选条件等可能影响到其余组件的组件,能够通过 ComponentMeta.keepActive 强制放弃激活状态:

import {Interfaces} from '@alife/bi-designer'const componentMeta: Interfaces.ComponentMeta = {keepActive: true}
  • keepActive:组件始终保持激活状态,即不呈现在可视区域也会被渲染与响应刷新,默认敞开。

对于非凡场景比方截图,可能要求所有组件强制为 active 状态,能够通过 forceActive 函数实现:

import {Interfaces, useDesigner} from '@alife/bi-designer'const Test: Interfaces.ComponentElement = () => {  const { forceActive, cancelForceActive} = useDesigner()    // forceActive() 强制所有组件 active  // cancelForceActive() 勾销强制 active,组件依据理论状况 active};

能够通过 getSnapshot().actives 获取任意组件以后刹时 active 状态:

import {useDesigner} from '@alife/bi-designer'const Test = () => {  const { getSnapshot, id} = useDesigner()    // 以后组件激活状态  const active = getSnapshot().actives[id]};

上下文数据对象

组件 DSL 形容中,表达式类型(JSExpression)能够通过 this. 拜访到上下文数据对象。上下文数据对象合乎如下规定:

  • 任何组件都通过配置 ComponentMeta.stateful 持有上下文。
  • 画布根节点 root 肯定是 stateful 的。
  • JSFunctionJSExpression 都可通过 this.state 拜访上下文,this.setState 批改上下文。

举例子:

// 初始化 pageSchemaconst defaultPageSchema: Interfaces.PageSchema = {componentInstances: {    test1: {      id: 'test1',      componentName: 'test',      parentId: 'jtw4x8ns',      index: 0,      props: {        variable: {          type: 'JSExpression',          value: 'this.state.variable +"%"',},        onClick: {type:'JSFunction',          value:'function onClick() {this.setState({ variable: 5}) }',        },      },    }  }};

这个例子中,组件调用 this.props.onClick 会批改上下文 a=5,触发后,其 this.props.variable 拿到的值会变为 5%。

任何组件或容器只有设置了 stateful 就能够持有状态:

import {Interfaces} from '@alife/bi-designer'const statefulComponentMeta: Interfaces.ComponentMeta = {stateful: true}

被有状态的容器包裹的组件 this.statethis.setState 都局限在以后状态容器内,也就是以后状态容器内组件的 state 是互通的,且一个有状态容器与外部环境是隔离的,能够独立运行。

工具类拓展

工具类拓展能够通过上下文拜访,如下是拓展形式:

import {Interfaces} from '@alife/bi-designer'// DSL 中减少 utils 形容 const defaultPageSchema: Interfaces.PageSchema = {utils: [    {      name: 'format',      type: 'function',      content: `function format(str){return str + '%'}`,    },  ]};
  • name:工具函数名。
  • type:类型,包含 npmumdfunction
  • content:内容。

用法:

JSFunction 与 JSExpression 都能够通过 this.utils 拜访工具类拓展函数,比方 // DSL 中减少 Expression 形容 const defaultPageSchema: Interfaces.PageSchema = {componentInstances: {    test: {      id: 'tg43g42f',      componentName: 'expressionComponent',      index: 0,      props: {        variable: {          type: 'JSExpression',          value: 'this.utils.format("100")',        }      },    },  },};

下面的例子中,组件拿到的 props.variable 值为 100%。

总结

如果你认真看完了全文,就会发现,bi-designer 是一个集成了数据流的开发框架,而不仅是一个渲染引擎,但却能够和你现有的业务代码友好相处,没有入侵性。

像渲染实现标识、按需渲染、组件加载器、部分配置笼罩等性能是强依赖渲染引擎存在的,因而较难在剥离渲染引擎的条件下转换为代码,因为做 BI 剖析工具毕竟不是做研发提效用,业务上没有出码的必要,因而咱们会做许多依赖渲染引擎的能力加强。

更多数据分析个性的性能将在下一个话题 API 之组件阐明。

探讨地址是:精读《数据搭建引擎 bi-designer API- 设计器》· Issue #267 · dt-fe/weekly

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

关注 前端精读微信公众号

版权申明:自在转载 - 非商用 - 非衍生 - 放弃署名(创意共享 3.0 许可证)

退出移动版