从零设计可视化大屏搭建引擎

26,131

相关文章: 从零开发一款可视化大屏制作平台
演示地址: V6可视化大屏编辑器
注: ⚠️本文为掘金社区首发签约文章,未获授权禁止转载

几个月前我写了一篇关于从零开发一款可视化大屏制作平台 的文章, 简单概述了一下可视化大屏搭建平台的一些设计思路和效果演示, 这篇文章我会就 如何设计可视化大屏搭建引擎 这一主题, 详细介绍一下实现原理。

按照我一向的写作风格, 我会在下面列出文章的大纲,以便大家有选择且高效率的阅读和学习:

  • 快速了解数据可视化
  • 如何设计通用的大屏搭建引擎
  • 大屏搭建引擎核心功能实现
    • 拖拽器实现
    • 物料中心设计
    • 动态渲染器实现
    • 配置面板设计
    • 控制中心概述
    • 功能辅助设计
  • 可视化大屏后期规划和未来展望

大家可以轻松根据右侧的文章导航, 快速定位到自己想看的位置, 接下来我们开始进入正文。

快速了解数据可视化

说到数据可视化, 想必大家多多少少稍接触过, 从技术层面谈, 最直观的就是前端可视化框架, 比如:

  • echart
  • antv
  • Chart.js
  • D3.js
  • Vega

这些库都能帮我们轻松制作可视化图表。

从实用性的角度来谈, 其最主要的意义就在于帮助用户更好的分析和表达数据。所以说谈到数据可视化, 更多的是和各种图表打交道, 通过 数据 -> 图表组合 -> 可视化页面 这一业务流程, 就构成了我们今天要研究的话题——设计可视化大屏搭建引擎

如何设计通用的大屏搭建引擎

说到 “引擎” 这个词也许有种莫名的高大上, 其实在互联网技术中, 我们经常会听到各种相关的名词,比如 “浏览器渲染引擎” , “规则引擎” , “图像识别引擎” 等, 我觉得 “引擎” 的本质就是提供一套可靠的机制, 为系统提供源源不断的生产力。 所以我们今天谈的“可视化大屏搭建引擎”, 本质上也是提供一套搭建机制, 支撑我们设计各种复杂的可视化页面。

为了方便大家理解可视化搭建, 我这里展示2张可视化大屏的页面, 来和大家一起分析一下可视化大屏的组成要素:

当然实际应用中大屏展现的内容和形式远比这复杂, 我们从上图可以提炼出大屏页面的2个直观特征:

  • 可视化组件集
  • 空间坐标关系

因为我们可视化大屏载体是页面, 是html, 所以还有另外一个特征: 事件/交互。综上我们总结出了可视化大屏的必备要素:

我们只要充分的理解了可视化大屏的组成和特征, 我们才能更好的设计可视化大屏搭建引擎, 基于以上分析, 我设计了一张基础引擎的架构图:

接下来我就带大家一起来拆解并实现上面的搭建引擎。

大屏搭建引擎核心功能实现

俗话说: “好的拆解是成功的一半”, 任何一个复杂任务或者系统, 我们只要能将其拆解成很多细小的子模块, 就能很好的解决并实现它. (学习也是一样)

接下来我们就逐一解决上述基础引擎的几个核心子模块:

  • 拖拽器实现
  • 物料中心设计
  • 动态渲染器实现
  • 配置面板设计
  • 控制中心概述
  • 功能辅助设计

拖拽器实现

拖拽器是可视化搭建引擎的核心模块, 也是用来解决上述提到的大屏页面特征中的“空间坐标关系”这一问题。 我们先来看一下实现效果:

有关拖拽的技术实现, 我们可以利用原生 js 实现, 也可以使用第三方成熟的拖拽库, 比如:

  • DnD
  • React-Dragable
  • react-moveable

我之前也开源了一个轻量级自由拖拽库 rc-drag , 效果如下:

有关它的技术实现可以参考我的另一篇文章: 轻松教你搞定组件的拖拽, 缩放, 多控制点伸缩和拖拽数据上报。 大家也可以基于此做二次扩展和封装。

我们拖拽器的基本原型代码如下:

export default function DragBox(props) {
    const [x, y, config] = props;
    const [target, setTarget] = React.useState();
    const [elementGuidelines, setElementGuidelines] = React.useState([]);
    const [frame, setFrame] = React.useState({
        translate: [x, y],
    });
    React.useEffect(() => {
        setTarget(document.querySelector(".target")!);
    }, []);
    return <div className="container">
        <div className="target">拖拽内部组件, 比如图表/基础组件等</div>
        <Moveable
            target={target}
            elementGuidelines={elementGuidelines}
            snappable={true}
            snapThreshold={5}
            isDisplaySnapDigit={true}
            snapGap={true}
            snapElement={true}
            snapVertical={true}
            snapHorizontal={true}
            snapCenter={false}
            snapDigit={0}
            draggable={true}
            throttleDrag={0}
            startDragRotate={0}
            throttleDragRotate={0}
            zoom={1}
            origin={true}
            padding={{"left":0,"top":0,"right":0,"bottom":0}}
            onDragStart={e => {
                e.set(frame.translate);
                // 自定义的拖拽开始逻辑
            }}
            onDrag={e => {
                frame.translate = e.beforeTranslate;
                e.target.style.transform = `translate(${e.beforeTranslate[0]}px, ${e.beforeTranslate[1]}px)`;
                // 自定义的拖拽结束逻辑
            }}
        />
    </div>;
}

以上只是实现了基本的拖拽功能, 我们需要对拖拽位置信息做保存以便在预览是实现“所搭即所得”的效果。位置信息会和其他属性统一保存在组件的DSL数据中, 这块在接下来内容中会详细介绍。

对于拖拽器的进一步深入, 我们还可以设置参考线, 对齐线, 吸附等, 并且可以在拖拽的不同时期(比如onDragStartonDragEnd)做不同的业务逻辑。这些 Moveable 都提供了对应的api支持, 大家可以参考使用。

物料中心设计

物料中心主要为大屏页面提供“原材料”。为了设计健壮且通用的物料, 我们需要设计一套标准组件结构和属性协议。并且为了方便物料管理和查询, 我们还需要对物料进行分类, 我的分类如下:

  • 可视化组件 (柱状图, 饼图, 条形图, 地图可视化等)
  • 修饰型组件 (图片, 轮播图, 修饰素材等)
  • 文字类组件 (文本, 文本跑马灯, 文字看板)

具体的物料库演示如下:

这里我拿一个可视化组件的实现来举例说明:

import React, { memo, useEffect } from 'react'
import { Chart } from '@antv/g2'

import { colors } from '@/components/BasicShop/common'

import { ChartConfigType } from './schema'

interface ChartComponentProps extends ChartConfigType {
  id: string
}

const ChartComponent: React.FC<ChartComponentProps> = ({
  id, data, width, height,
  toggle, legendPosition, legendLayout, legendShape,
  labelColor, axisColor, multiColor, tipEvent, titleEvent,
  dataType, apiAddress, apiMethod, apiData, refreshTime,
}) => {
  useEffect(() => {
    let timer:any = null;
    const chart = new Chart({
      container: `chart-${id}`,
      autoFit: true,
      width,
      height
    })

    // 数据过滤, 接入
    const dataX = data.map(item => ({ ...item, value: Number(item.value) }))
    chart.data(dataX)
    
    // 图表属性组装
    chart.legend(
      toggle
        ? {
          position: legendPosition,
          layout: legendLayout,
          marker: {
            symbol: legendShape
          },
        }
        : false,
    )

    chart.tooltip({
      showTitle: false,
      showMarkers: false,
    })

    // 其他图表信息源配置, 方法雷同, 此处省略
    // ...

    chart.render()

  }, [])

  return <div id={`chart-${id}`} />
}

export default memo(ChartComponent)

以上就是我们的基础物料的实现模式, 可视化组件采用了g2, 当然大家也可以使用熟悉的echartD3.js等. 不同物料既有通用的 props , 也有专有的 props, 取决于我们如何定义物料的Schema

在设计 Schema 前我们需要明确组件的属性划分, 为了满足组件配置的灵活性和通用性, 我做了如下划分:

  • 外观属性 (组件宽高, 颜色, 标签, 展现模式等)
  • 数据配置 (静态数据, 动态数据)
  • 事件/交互 (如单击, 跳转等)

有了以上划分, 我们就可以轻松设计想要的通用Schema了。 我们先来看看实现后的配置面板:

这些属性项都是基于我们定义的schema配置项, 通过 解析引擎 动态渲染出来的, 有关 解析引擎 和配置面板, 我会在下面的章节和大家介绍。 我们先看看组件的 schema 结构:

const Chart: ChartSchema = {
  editAttrs: [
    {
      key: 'layerName',
      type: 'Text',
      cate: 'base',
    },
    {
      key: 'y',
      type: 'Number',
      cate: 'base',
    },
    ...DataConfig, // 数据配置项
    ...eventConfig, // 事件配置项
    
  ],
  config: {
    width: 200,
    height: 200,
    zIndex: 1,
    layerName: '柱状图',
    labelColor: 'rgba(188,200,212,1)',
    // ... 其他配置初始值
    multiColor: ['rgba(91, 143, 249, 1)', 'rgba(91, 143, 249, 1)', 'rgba(91, 143, 249,,1)', 'rgba(91, 143, 249, 1)'],
    data: [
      {
        name: 'A',
        value: 25,
      },
      {
        name: 'B',
        value: 66,
      }
    ],
  },
}

其中 editAttrs 表示可编辑的属性列表, config 为属性的初始值, 当然大家也可以根据自己的喜好, 设计类似的通用schema

我们通过以上设计的标准组件和标准schema, 就可以批量且高效的生产各种物料, 还可以轻松集成任何第三方可视化组件库。

动态渲染器实现

我们都知道, 一个页面中元素很多时会影响页面整体的加载速度, 因为浏览器渲染页面需要消耗CPU / GPU。对于可视化页面来说, 每一个可视化组件都需要渲染大量的信息元, 这无疑会对页面性能造成不小的影响, 所以我们需要设计一种机制, 让组件异步加载到画布上, 而不是一次性加载几十个几百个组件(这样的话页面会有大量的白屏时间, 用户体验极度下降)。

动态加载器就是提供了这样一种机制, 保证组件的加载都是异步的, 一方面可以减少页面体积, 另一方面用户可以更早的看到页面元素。目前我们熟的动态加载机制也有很多, VueReact 生态都提供了开箱即用的解决方案(虽然我们可以用 webpack 自行设计这样的动态模型, 此处为了提高行文效率, 我们直接基于现成方案封装)。我们先看一下动态渲染组件的过程:

上面的演示可以细微的看出从左侧组件菜单拖动某个组件图标到画布上后, 真正的组件才开始加载渲染。

这里我们以 umi3.0 提供的 dynamic 函数来最小化实现一个动态渲染器. 如果不熟悉 umi 生态的朋友, 也不用着急, 看完我的实现过程和原理之后, 就可以利用任何熟悉的动态加载机制实现它了。 实现如下:

import React, { useMemo, memo, FC } from 'react'
import { dynamic } from 'umi'

import LoadingComponent from '@/components/LoadingComponent'


const DynamicFunc = (cpName: string, category: string) => {
  return dynamic({
    async loader() {
      //  动态加载组件
      const { default: Graph } = await import(`@/components/materies/${cpName}`)

      return (props: DynamicType) => {
        const { config, id } = props
        return <Graph {...config} id={id} />
      }
    },
    loading: () => <LoadingComponent />
  })
}

const DynamicRenderEngine: FC<DynamicType> = memo((props) => {
  const { 
  type, 
  config, 
  // 其他配置... 
  } = props
  const Dynamic = useMemo(() => {
    return DynamicFunc(config)
  }, [config])

  return <Dynamic {...props} />
})

export default DynamicRenderEngine

是不是很简单? 当然我们也可以根据自身业务需要, 设计更复杂强大的动态渲染器。

配置面板设计

实现配置面板的前提是对组件 Schema 结构有一个系统的设计, 在介绍组件库实现中我们介绍了通用组件 schema 的一个设计案例, 我们基于这样的案例结构, 来实现 动态配置面板

由上图可以知道, 动态配置面板的一个核心要素就是 表单渲染器。 表单渲染器的目的就是基于属性配置列表 attrs 来动态渲染出对应的表单项。我之前写了一篇文章详细的介绍了表单设计器的技术实现的文章, 大家感兴趣也可以参考一下: Dooring可视化之从零实现动态表单设计器

我这里来简单实现一个基础的表单渲染器模型:

const FormEditor = (props: FormEditorProps) => {
  const { attrs, defaultValue, onSave } = props;

  const onFinish = (values: Store) => {
    // 保存配置项数据
    onSave && onSave(values);
  };
  
  const handlechange = (value) => {
    // 更新逻辑
  }

  const [form] = Form.useForm();

  return (
    <Form
      form={form}
      {...formItemLayout}
      onFinish={onFinish}
      initialValues={defaultValue}
      onValuesChange={handlechange}
    >
      {
        attrs.map((item, i) => {
        return (
          <React.Fragment key={i}>
            {item.type === 'Number' && (
              <Form.Item label={item.name} name={item.key}>
                <InputNumber />
              </Form.Item>
            )}
            {item.type === 'Text' && (
              <Form.Item label={item.name} name={item.key}>
                <Input placeholder={item.placeholder} />
              </Form.Item>
            )}
            {item.type === 'TextArea' && (
              <Form.Item label={item.name} name={item.key}>
                <TextArea rows={4} />
              </Form.Item>
            )}
            // 其他配置类型
          </React.Fragment>
        );
      })}
    </Form>
  );
};

如果大家想看更完整的配置面板实现, 可以参考开源项目 H5-Dooring | H5可视化编辑器

我们可以看看最终的配置面板实现效果:

控制中心概述 & 功能辅助设计

控制中心的实现主要是业务层的, 没有涉及太多复杂的技术, 所以这里我简单介绍一下。 因为可视化大屏页面展示的信息有些可能是私密数据, 只希望一部分人看到, 所以我们需要对页面的访问进行控制。 其次由于企业内部业务战略需求, 可能会对页面进行各种验证, 状态校验, 数据更新频率等, 所以我们需要设计一套控制中心来管理。 最基本的就是访问控制, 如下:

功能辅助设计 主要是一些用户操作上的优化, 比如快捷键, 画布缩放, 大屏快捷导航, 撤销重做等操作, 这块可以根据具体的产品需求来完善。 大家后期设计搭建产品时也可以参考实现。

可视化大屏后期规划和未来展望

为了实现更富有展现力, 满足更多场景的可视化大屏引擎, 我们一方面需要提高引擎扩展性, 一方面需要完善物料生态, 其次只要与时俱进, 提供更多智能化的场景功能, 比如搭建埋点, 数据预警等, 具体规划如下:

  • 丰富组件物料, 支持3D组件, 地理空间组件等
  • 搭建埋点, 方便后期对组件进行分析
  • 实现数据源, 事件机制闭环
  • 支持用户自定义组件

如果大家对可视化搭建或者低代码/零代码感兴趣, 也可以参考我往期的文章或者在评论区交流你的想法和心得。

往期文章