使用antv/G2生态半年有感

4,067 阅读10分钟

前言

因为业务需要, 之前在技术选型的时候, 关于图表这块儿, 主要有2个选项, 一个是echarts, 一个是G2. 关于这2个我相信大家都有所耳闻。 echarts的话用是用过, 但是谈不上深入, 就是一些配置项, 源码也没有看过。而G2则是几乎没用过, 想尝试下。 G2使用了几个月了, 接下来我就说说我的真实使用感受。

笔者背景

上海toB互联网公司前端开发, 工作经验接近3年吧。主要使用Angular技术栈。比较喜欢做开源相关。曾经给ng-zorro、ng-zorro-mobile、antv/x6等贡献过代码(非错别字纠正的, 都工作3年了早不干这种事情了...)

目前负责公司BI产品线的前端业务.

@antv/g2

聊到G2, 不得不说G2的生态体系。 G2其实只是一个repo, 但他更是一个品牌(看完后文对g2技术架构的分析就会明白为什么他是一个品牌)。 大家知道G2和G2Plot的区别吗?如果你打算使用G2体系,那G2Plot则是你非常需要了解的一块儿内容。

在echarts中, 想要实现一个折线图, 需要怎么做? 我相信大家都不擅长背API, 应该没多大印象, 但是一定记得在初始化图表的时候一开始就要给ecahrts你需要的是什么表

option = {
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      data: [150, 230, 224, 218, 135, 147, 260],
      type: 'line' // 点睛之笔, 这将告诉ecahrts 我需要的折线图
    }
  ]
};


而在G2中则不是这样的. 在G2中想要一个折线图, 需要像下面这样

fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/terrorism.json')
  .then(res => res.json())
  .then(data => {
    const ds = new DataSet();

    const chart = new Chart({
      container: 'container',
      autoFit: true,
      height: 500,
      syncViewPadding: true,
    });

    chart.scale({
      Deaths: {
        sync: true,
        nice: true,
      },
      death: {
        sync: true,
        nice: true,
      },
    });


    const dv1 = ds.createView().source(data);
    dv1.transform({
      type: 'map',
      callback: (row) => {
        if (typeof (row.Deaths) === 'string') {
          row.Deaths = row.Deaths.replace(',', '');
        }
        row.Deaths = parseInt(row.Deaths, 10);
        row.death = row.Deaths;
        row.year = row.Year;
        return row;
      }
    });
    const view1 = chart.createView();
    view1.data(dv1.rows);
    view1.axis('Year', {
      subTickLine: {
        count: 3,
        length: 3,
      },
      tickLine: {
        length: 6,
      },
    });
    view1.axis('Deaths', {
      label: {
        formatter: text => {
          return text.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,');
        }
      }
    });
    view1.line().position('Year*Deaths');


    const dv2 = ds.createView().source(dv1.rows);
    dv2.transform({
      type: 'regression',
      method: 'polynomial',
      fields: ['year', 'death'],
      bandwidth: 0.1,
      as: ['year', 'death']
    });

    const view2 = chart.createView();
    view2.axis(false);
    view2.data(dv2.rows);
    view2.line().position('year*death').style({
      stroke: '#969696',
      lineDash: [3, 3]
    })
      .tooltip(false);
    view1.annotation().text({
      content: '趋势线',
      position: ['1970', 2500],
      style: {
        fill: '#8c8c8c',
        fontSize: 14,
        fontWeight: 300
      },
      offsetY: -70
    });
    chart.render();
  });

我相信你是快速划过这段代码的, 因为你好像找不到哪里点明了需要折线图呢? 答案确实是没有, G2就是这样, 他是一个图形语法, 通过数据之间的关系, 来生成一张图. 所以G2的定制化确实更加强大, 但是!!!! 我们做业务的, 一般就是需要一个特定的图然后填充数据, 你让我去将数据和数据之间的关系描述出来, 这...说实话我得去学下图形语法相关的知识. G2官方也发现了这个问题, 所以G2官方造了一个G2Plot.

@antv/g2plot

在g2plot中, 需要一个折线图只需要像下面这样

import { Line } from '@antv/g2plot';

fetch('https://gw.alipayobjects.com/os/bmw-prod/1d565782-dde4-4bb6-8946-ea6a38ccf184.json')
  .then((res) => res.json())
  .then((data) => {
    const line = new Line('container', { // 我相信你很快就发现了这个点睛之笔
      data,
      padding: 'auto',
      xField: 'Date',
      yField: 'scales',
      annotations: [
        {
          type: 'regionFilter',
          start: ['min', 'median'],
          end: ['max', '0'],
          color: '#F4664A',
        },
        {
          type: 'text',
          position: ['min', 'median'],
          content: '中位数',
          offsetY: -4,
          style: {
            textBaseline: 'bottom',
          },
        },
        {
          type: 'line',
          start: ['min', 'median'],
          end: ['max', 'median'],
          style: {
            stroke: '#F4664A',
            lineDash: [2, 2],
          },
        },
      ],
    });

    line.render();
  });

没错, g2plot就是g2版本的echarts. 他降低了开发者的使用门槛. 那么问题来了, 既然有了g2plot, 为什么还需要g2呢?

事实上, g2plot与g2是上下游的关系. g2plot是将你的配置, 转换成G2的配置, 实际渲染的还是g2. 我们可以在g2plot的源码中找到下面这段代码

图片.png

我们在最后一行代码会看到, g2plot在容器上打上了自己的专属标签, 我们对比下渲染结果就可以确定, 这段代码确实是执行了的.

图片.png

那么此刻, 你的脑海中, 应该有了一个基本的意识, g2plot就是个黑盒, 你将数据和配置丢给g2plot, 他会将其转化为g2的配置. 仅此而已吗? 不, 你太小看g2生态了.

@antv/component

大家想象一下, 以tooltip组件为例, 也就是下面这个hover的时候会出现的框框.

图片.png

是不是折线图有, 柱状图也有? 很多图都有吧? 而你查看元素会发现, 这些东西并不是canvas, 而是纯纯的html

图片.png

我们在g2源码中可以找到如下代码块儿

图片.png

图片.png

g2将这些很常用的小组件抽象成了独立的库, 叫@antv/component, 以此来复用代码.

套娃集结

至此, g2的技术架构确定了, g2plot使用了g2, g2使用了component. 实际上, component还依赖了其他的antv的库, 我随便抄了一段package.json的代码如下.

"@antv/color-util": "^2.0.3",

"@antv/dom-util": "~2.0.1",

"@antv/g-base": "0.5.6",

"@antv/matrix-util": "^3.1.0-beta.1",

"@antv/path-util": "~2.0.7",

"@antv/scale": "~0.3.1",

"@antv/util": "~2.0.0",

因此, g2还是g2吗? 确切的来说, g2只是一个牌子, 他是众多repo的集结.

Bug之Tooltip

我们知道, 写业务的开发者免不了需求定制化. 以tooltip为例, G2则给我们提供了定制化的能力. 关键词是customContent. 但是, 是否自定义在动画上缺有区别, 明显自定义的不够顺畅, 可以看下面这个gif.

图片.png

这是为什么呢?

初步分析

首先我们知道, 一个元素从一个地方移动到另一个地方, 看起来比较顺滑, 所谓的顺滑无非就是transition. 那么我们查看这2个场景下的dom变化.

原生的tooltip

图片.png

发现了什么? 一个叫g2-tooltip的dom, 始终只有lefttop属性在变化,标签本身是没闪烁的. 而容器内部的div则是不停地闪烁, 说明是时刻在重新生成的. 我们大概可以猜到鼠标划过时

  • 修改g2-tooltip的位置
  • 清空g2-tooltip的子元素
  • 将原生的tooltip内容appendg2-tooltip中.

接下来我们看自定义tooltip的表现

图片.png

这次, 不存在那个g2-tooltip的容器了. 而且定位的样式, 是直接附加在自定义的内容块上的, 并且自定义的元素标签一直在闪现, 也就是说, 每次滑动, 元素都是重新生成的, 自然动画看起来就不这么自然.

定位bug

那么接下来免不了去窥探源码了, 但是问题来了. 我们知道问题出在哪么? 到底是g2plot的问题还是g2的问题还是component的问题呢? 其实在前文对g2生态做过分析, 相信大家很容易想到问题应该出在component这个库当中.

其实我是先了解到这个bug, 而不是先知道的技术架构. 我是花了一整晚的时间去研究他的源码执行逻辑才搞明白所谓的架构的, 那源码看的真心累

接下来就免不了对@antv/component库做调试了, 这里我就不展开太多了, 因为确实比较复杂, 需要用到debug和一些开发技巧、经验. 我直接给结论吧. 注释我已经写上了, 相信你能看懂.

图片.png

其实在发现这个点的时候, 我就已经挺兴奋的了, 因为确实花了很多时间去调试. 但是沮丧也随之而来, 因为我不知道该怎么改. 我上了个厕所, 冷静了下(程序员的万能技巧, 没有灵感就去尿尿)

果然, 在厕所想到了解决方案, 我是不是可以像下面这样呢?

// 这是他一开始会执行的一个函数, 用于生成容器
protected initContainer() {
    let container = this.get('container');
    if (this.get('customContent')) {
        // 在生成container的时候, 如果是自定义内容的tooltip, 则生成一个恒定的容器, 用于定位
        container = document.createElement('div');
        container.className = 'g2-tooltip-position';
    } else {
        // 未指定 container 则创建
        container = this.createDom();
    }
    // 后面的省略
}

// 根据 customContent 渲染
private renderCustomContent() {
    const node = this.getHtmlContentNode();
    const container: HTMLElement = this.get('container');
    container.innerHTML = '';
    container.appendChild(node);
    this.resetStyles();
    this.applyStyles();
}

我的思路是, 保持容器不变, 容器的class叫g2-tooltip-position, 然后每次mousemove都只是调整他位置, 自定义tooltip的内容就通过append的方式放进去. 这样不就解决了吗?

之后我就改了下并且提了pr(实际上改这个需要改单测, 这里就不列出来了, 又是花了一晚上的时间).

yalc YYDS

在这里有一个问题, 我是没有办法去测试我到底有没有改好的. 为什么呢, 因为g2和g2plot都有一个scriptsite:develop, 通过这个命令就可以跑起来. 而component这个库没有, 他只是一个工具人. 因此我问了官方团队, 官方和我说可以试下本地build再link过去.

说实话, 我还真没使用过什么link, 之后同事推荐了yalc这个工具, 他可以非常好的将某个lib的依赖从node_modules转为本地文件, 在这里不多做介绍了.

这不试不知道, 一试发现我的更改导致了新的bug, 比如tooltip边界检测失效了(tooltip不应该超出canvas范围), 还有一些其他的问题, 最终我close掉了pr. 因为他们以前的种种设计都是基于之前的设定, 现在我增加了一个恒定容器, 和他们之前的一些设计不符了. 那么我还需要去改相应的一些设计, 但是这样下去我担心会产生新的问题, 改起来无穷无尽了, 所以我选择放弃, 不改了, 太累了.

低回复效率的社区

在一些第三方库的使用过程当中, 社区活跃度是非常重要的. 在G2的使用过程中, 根据我自己的观察和自己的体验, 官方回复效率比较慢. 大量issue处于无人问津的状态. 既不close也不追问你. 这点我觉得G2做的不好.

可能有人会觉得因为G2项目太庞大, 来不及处理issue. 那么angular这个repo你觉得大吗? angular的issue在2周之内必给回复. 并且是一点一点非常认真的在和我讨论, 人家可不会因为你问的问题愚蠢就不屌你. 下面这个链接就是最近一个angular官方团队教我写代码的案例.

Support transform DOM to ViewContainerRef

在前文就提到了我目前是负责BI产品, 那么可视化相关的东西少不了. 除了G2, 还使用到了一个antv/x6.

图片.png

主要是做图相关的, 之前在给antv/x6提issue、pr的时候, 官方团队跟进的速度是非常快的, 这给我的感觉很好, 帮助也很大, 能够让我快速解决问题, 我也更加乐意帮助他们去完善x6.

之前我帮他们扩展了一个package, 后来有人在使用这个插件的时候遇到了一些问题. 大家可以看下下面这个issue时间线, 26号有人提出问题, 当天官方团队就艾特了我, 我在27号就修复了这个bug, 3天后pr就被合并了. 这才叫跟进, 这才叫在意社区质量.

angular项目中改变自定义节点元素数据,画布不更新

可能有同学会想, G2是开源的, 你爱用不用, 人家又没收你钱. 诶, 你说对了. 所以我这也只是客观评价G2社区的活跃度, 至于issue回复效率等, 我可没有要求什么, 爱回复不回复, 我只是因为使用了G2, 当然希望他能更好. 但是本质上, 和我是没啥利益关系的.

总结

总的来说, G2生态算是比较不错的图表库的, 文档质量谈不上特别好, 凑合看吧, 基本上能解决业务问题. 社区质量一般, 基本上爱理不理的. 源码的技术架构还挺有意思的, 值得去研究. 不过代码质量我个人感觉一般, TS的类型校验使用的比较一般. 一大堆函数没有返回值、参数没有类型等, 在这里就不一一列举了. 如果你是一个新人, 不建议使用G2, 如果你有2年以上工作经验, 建议直接使用G2plot.