低代码数据可视化项目技术讲解

本文对笔者参与开发的低代码数据可视化项目中的重点技术的实现进行讲解。

1.依据JSON Schema生成右侧的配置栏

Schema简介

我们需要知道哪些字段是预期的,以及这些值是如何表示的。这便是 JSON Schema 的用武之地。

{
  "first_name": "George",
  "last_name": "Washington",
  "birthday": "1732-02-22",
  "address": {
    "street_address": "3200 Mount Vernon Memorial Highway",
    "city": "Mount Vernon",
    "state": "Virginia",
    "country": "United States"
  }
}

JSON Schema 片段描述了上述JSON示例的结构

{
  "type": "object",
  "properties": {
    "first_name": { "type": "string" },
    "last_name": { "type": "string" },
    "birthday": { "type": "string", "format": "date" },
    "address": {
      "type": "object",
      "properties": {
        "street_address": { "type": "string" },
        "city": { "type": "string" },
        "state": { "type": "string" },
        "country": { "type" : "string" }
      }
    }
  }
}

不同类型的组件的样式配置不同,我们使用schema作为生成各类组件属性配置组件面板的依据。
在这里插入图片描述
在这里插入图片描述
如下图所示,是通用标题组件的schema片段

/**
 * @flow
 */
import {
  fontFamilyOpts,
  fontWeightOpts,
  justifyContentOpts,
  alignOpts,
} from '../../../../data/select-options';
import { fontSize, color } from '../../../commonConfig';

const config = {
  content: {
    type: 'string',
    title: '标题内容',
    'x-component': 'Input',
    'x-span': 24,
  },
  ellipsis: {
    title: '省略号',
    type: 'boolean',
    'x-component': 'Switch',
    'x-span': 24,
  },
  textStyle: {
    title: '文本样式',
    type: 'object',
    properties: {
      fontFamily: {
        title: '字体',
        type: 'string',
        'x-component': 'Select',
        'x-componentProps': {
          options: fontFamilyOpts,
        },
        'x-span': 12,
      },
      fontWeight: {
        title: '字体粗细',
        type: 'string',
        'x-component': 'Select',
        'x-componentProps': {
          options: fontWeightOpts,
        },
        'x-span': 12,
      },
      fontSize: {
        ...fontSize,
        title: '字体大小',
      },
      color: {
        ...color,
        'x-span': 12,
      },
    },
  },
  textAlign: {
    title: '文本对齐',
    type: 'object',
    properties: {
      hAlign: {
        title: '水平对齐',
        type: 'string',
        'x-component': 'Select',
        'x-componentProps': {
          options: justifyContentOpts,
        },
        'x-span': 12,
      },
      vAlign: {
        title: '垂直对齐',
        type: 'string',
        'x-component': 'Select',
        'x-componentProps': {
          options: alignOpts,
        },
        'x-span': 12,
      },
    },
  },
  urlConfig: {
    title: '超链接配置',
    type: 'object',
    properties: {
      url: {
        title: '超链接',
        type: 'string',
        'x-component': 'Input',
        'x-span': 24,
      },
      isBlank: {
        title: '是否在新窗口中打开',
        type: 'boolean',
        'x-component': 'Switch',
        'x-span': 24,
      },
    },
  },
};

export default config;

schema用来描述通用标题组件对象的props属性的结构,同时也是用于生成右侧栏组件属性配置的依据。

type Props = {
  widget: {
    props: {
      content: string,
      ellipsis: boolean,
      textAlign: {
        hAlign: string,
        vAlign: string,
      },
      textStyle: {
        color: DataVColor,
        fontFamily: string,
        fontSize: number,
        fontWeight: string,
      },
      urlConfig: {
        url: string,
        isBlank: boolean,
      },
    },
    attr: {
      x: number,
      y: number,
      h: number,
      w: number,
      opacity: number,
    },
  },
  data: {
    url: string,
    value: string,
  },
};

如何根据schema去生成右侧的属性配置组件,并且用户修改属性能同时改动组件的属性呢?
1.公共属性配置:
在这里插入图片描述

将每个选中的画布组件对象widget(该选中组件的数据是canvasStore的rootWidget数据计算出来的)传递给基本配置组件BasicSetting,基本配置组件根据widget对象的值去展示值,在用户修改公共属性时,通过onChange,onBlur事件回调去修改rootWidget组件树中的对应的组件对象widget。(画布、图层和右侧属性配置栏共享canvasStore中rootWidget组件树数据,组件对象的数据是存储在公共状态中的,所以能实现同步)
2.个性属性配置:
在这里插入图片描述

1.schema中有x-component属性的,根据x-component中类型不同,渲染不同的组件如Input,Switch
在每个底层的Input,Switch组件onChange事件回调中,将事件对象的value抛上去;
但是呢,这些底层组件都会被统一封装一层,因为要统一处理onChange事件,会将keyChain(属性修改链路)和value在统一封装的那一层,将keyChain和value组合到一起,抛上去。在最上面的一层,把key,keyChain,value三者组合到一起,去修改canvasStore的rootWidget数据。
原理是一样的,通过widget对象对应的属性值进行展示,在onChange,onBlur事件回调去修改rootWidget中的对应的组件对象widget。
2.没有x-component属性的,比如是textStyle文本样式这种,这种是先在外层包一层collapse,textStyle下的fontsize底层是BaseField,然后被ObjectField包了一层,ObjectField这层是用来确定span的占据的宽度,最后就是被Collapse包裹一层了。
Collapse(textStyle层)=>ObjectField(确定占据宽度)=>BaseField(Input,Slider等的类同fontSize的底层)
原理也是一样的,通过widget对象对应的属性值进行展示,在onChange,onBlur事件回调去修改rootWidget中的对应的组件对象widget。

3.重难点:如何做的配置项依赖处理?
文档链接如下:
https://react.formilyjs.org/api/shared/schema#schemareactions

axisLabelWithTimeFormat('options.axis.xAxis.type')

export const axisLabelWithTimeFormat = timeDeps => {
  return {
    ...axisLabel,
    properties: {
      ...axisLabel.properties,
      display: {
        ...axisLabel.properties.display,
        properties: {
          timeFormat: {
            ...labelTimeFormat,
            'x-span': 24,
            // 字段联动,依赖项不是时间的时候,隐藏自己。
            'x-reactions': [
              {
                dependencies: [timeDeps],
                fulfill: {
                  schema: {
                    'x-hidden': '{{$deps[0] !== "time"}}',
                  },
                },
              },
            ],
          },
          ...axisLabel.properties.display.properties,
        },
      },
    },
  };
};

在底层组件BaseField这里,去获取到这个底层组件对应的schema的reaction,根据reaction依赖的属性,找到这个底层组件依赖的属性的值,去确定底层组件的显示和隐藏。
这个操作在底层组件初始化的时候会去做,监听到画布组件的每个属性变化的时候,每个属性都会去重新计算该属性对应的组件的显示和隐藏。

2.组件数据的渲染是如何做到的?

(1)画布中数据展示

每个画布中的组件,都会调用connectData这个高阶组件,接收不同类型的组件作为参数,返回一个传入了data的高阶组件。
那么connectData这个高阶组件中,主要做的事情,就是调用了useDataCenter这个hook,然后从dataSource这个store中取到dataMap中这个画布组件的数据,作为data,传入高阶组件当中去。

useDataCenter作用:
数据初始化:调用setComponentData函数
数据实时刷新:用定时器调用setComponentData函数
总之,核心函数是setComponentData函数。

setComponentData函数的作用是计算数据源最终的数据结果:
第一步,计算初步结果:
(1)静态数据
(2)全局变量(从dataMap中获取全局变量最终数据)
(3)api(发送请求获取数据)
(4)物联中台数据(发送请求获取数据)
第二步:过滤器处理
第三步:最后,将最终得到的数据结果挂载在dataSourceStore的dataMap上。

(2)预览中数据展示

初始化三个Store:
stores.canvasStore.init(config.rootWidget, panelInfo);
stores.converterStore.init(config.converter);
stores.globalVariableStore.init(config.globalVariable);

全局变量是什么时候去挂载到datamap上的?
globalVariableStore在init的时候,就去对每个全局变量setComponentData,将全局变量计算的最终结果都挂载到dataSourceStore的dataMap对象上,并且使用mobx的reaction来监听dataSourceStore的dataMap的这个全局变量的变化,如果有变化,那么重新计算引用了这个全局变量的组件或者全局变量的数据(数据的联动处理(全局变量引用全局变量,组件引用全局变量))。

转换器初始化和相关的联动处理是如何做的?
转换器的内容都存在converterStore中,是固定的内容。
转换器的内容变化的话,是如何重新计算引用了该转换器的全局变量和数据源的值的?
转换器内容修改成功的时候,去重新计算引用了该转换器的全局变量和数据源的值。
组件数据和全局变量进行初始化的时候,转换器内容是一定在的,因为是一起init的,所以初始化的时候一定是能获取到转换器的内容的。
(数据的联动处理(组件数据源引用转换器,全局变量引用转换器))
在这里插入图片描述

3.每个组件库的组件是如何维护的?

创建一个widgetRegister的class,在这个class中去管理维护所有的组件,
这个class中有widgetMap数据,key是不同类型的组件的名称,value是对应的组件的各种信息(包括组件,包括schema等)
在这里插入图片描述
每个组件都有一个install方法,这个方法是一个函数,调用传入的参数的register注册方法,注册方法的第二个参数是把这个组件的所有相关的内容都放在对象中的不同属性值上了。
在这里插入图片描述
在这里插入图片描述
如上操作后,我们可以使用widgets.getWidget方法获取到各组件库组件的相关信息。

以上内容,最重要的是我们如何把各个类型的组件都挂载到widgetRegister的class的实例上这个是关键了,我们总结下做法:
1.通过给组件挂载install方法,在这个方法里收集好这个组件的所有相关资料;
2.我们在外部获取到所有的组件,通过调用widgetRegister的class的实例的方法,将组件传入,然后就可以通过调用各个组件的install方法获取到组件的所有相关资料。

4.画布的布局

(1)ui层和拖拽层:

看起来我们的画布中的组件是,操作框里嵌套着组件,但是实际上他们不是嵌套的关系,为什么呢,因为如果是嵌套的关系,像成组的组件,设置组的透明度,会导致里面的组件的操作框都受到影响,都看不见了。
我们理想的情况是透明度是不能影响操作框的,操作框和里面组件UI是分离开的,是同层级结构,而不是嵌套的父子层级关系。
我们在操作框去进行各种画布组件的操作(拖拽组件,放大缩小组件,选中组件等)
组件UI只负责组件内容的UI展示。
因为两者的数据都是CanvasStore里的公共数据,操作框这里的操作去改变公共数据里的属性,那么组件UI那里也能同步更新。
操作框拖拽操作去改变了公共数据里组件的位置信息,组件UI那里同步的位置也跟随变化。

在这里插入图片描述

(3)画布结构布局

在这里插入图片描述
画布的父组件是包含比例尺的容器,这个容器是相对定位,所以画布是相对于这个容器定位的,top和left都是60px。
画布的尺寸和用户设定的尺寸是完全一样的,通过transform的scale属性放大或者缩小画布。
每个画布的UI组件是在基础组件外套了两层div,第一层div用来设定组件的尺寸位置透明度,第二层div用来设定组件在缩放过程中的样式。
每个UI组件都是绝对定位,top和left都是0,通过transform的translateX和translateY属性设置每个画布组件在画布中的位置的。
也就是说每个UI组件以第一个定位祖先元素的左上角为起点,进行transform的translateX和translateY的位移的。
如果是成组组件,其内部组件是以父级组件的左上角为起点,进行transform的translateX和translateY的位移的。
如果没有父级组件,那么因为画布是绝对定位的,所以没有父级组件的组件是以画布的左上角为起点,进行transform的translateX和translateY的位移的。

5.成组的思路

根据被选中的多个组件,计算成组组件的尺寸和位置,把选中的组件加入到成组组件下,
把选中的组件从原有的父节点中删除,在原有的父节点下加入新建的成组组件。
那么是如何做到最多有9层组,成组是如何嵌套的?
成组组件的话,有一层datav-layer的div来包裹所有的子组件,然后其下是各子组件对应有slider-item的div进行包裹,再其下的子组件这里就是递归调用组件去实现了。
在这里插入图片描述

6.组件拖拽

在这里插入图片描述
onDown事件中禁止冒泡,永远是最内层的组件接收到,
但是至于要让哪个组件去选中,是要根据祖先组件的选中关系,去决定是哪个组件被选中。
什么时候会去选中这个最内层的组件?
(1)无父组件
(2)有父组件,自己的同级或者父组件的更内部组件被选中了,但是自己没有被选中,那么选中自己。
其他情况呢?
有父组件,并且自己的同级和父组件更内部子级是没有被选中的,
那么找到最远的一个内部没有选中任何组件的祖先组件,选中该祖先组件。

7.发布页面的代码思路

在这里插入图片描述

这样每次刷新页面,如果会话里有密码,从会话中取密码去查验密码,不用用户每次都手动输入了。
token和refreshToken的作用就是访问一些需要权限校验的接口,比如组件数据来源于需要权限校验的中后台接口,那么我们就带着token去获取便可以获取到数据。

8.添加组件到画布中的思路

onDrop事件或者点击事件中,去根据组件名找到对应的组件的class,创建组件实例,添加到CanvasStore的rootWidget.children中去。

9.图层移动的思路

在这里插入图片描述

10.右键菜单的思路

在这里插入图片描述
我们创建useContextMenu的hook,作用是导出菜单用的函数和状态。
因为不仅是菜单会用到,图层栏的操作也会用到同样的函数,所以我们用自定义的hook来抽离公共的函数和状态。

11.全局状态介绍

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值