当卧龙遇上凤雏:钉钉小程序+F2图表库的踩坑指南

作者:千梦凯

前言

古茗加盟商在经营店铺的过程中,需要一款可视化的工具来将门店的经营情况。通过图表将数据呈现得更加直观、易懂;并且可以根据数据趋势,分析门店经营情况。

目前古茗主要通过钉钉小程序来辅助加盟商经营,并且开发使用Taro+React的方式进行开发小程序,本文将介绍如何使用Taro在钉钉小程序中实现数据图表化展示。

可视化图表库技术选型

目前市面上比较流行的可视化工具库有

  • ECharts:一个基于 JavaScript 的开源可视化图表库官网地址
  • Chart.js:基于HTML5的简单易用的JavaScript图表库 官方网站
  • D3.js:一个数据驱动的可视化库,通过使用HTML、SVG和CSS来操作数据,创造出交互式和动态的图表官方网站
  • AntV:蚂蚁企业级数据可视化解决方案官网地址

在wbe端我们使用任何一个库都可以实现可视化展示。不过针对钉钉小程序,我们要根据一下几个关键字来进行筛选使用的图表库(移动端小程序手势交互

  • Echarts上面有丰富的图表库,但是代码体积过大。全量代码将近1000k,选择常用功能也要500k左右,在小程序主包只有2m的情况下还是占用了太多的内容。
  • Chart.js 依赖于DOM API,而钉钉小程序的环境并不提供完整的DOM API。所以无法直接使用
  • D3.js是通过使用SVG来支持图表,但是目前小程序不支持使用SVG,所以无法在小程序中使用
  • Antv/F2 F2 是一个专注于移动端,面向常规统计图表,开箱即用的可视化引擎,完美支持 H5 环境同时兼容多种环境(Node, 小程序)

对比之下,F2图表方案更适合在小程序中使用。

由于我们在接入图表库时,4.x版本尚未更新,所以第一版本使用了3.x版本。在后续F2的更新迭代中,由于图表库是在主包中展示,考虑到文件大小的原因,最终使用了4.x版本。

如果是初次接入,推荐直接使用最新版本,功能更加强大。

在Taro+React中使用F2 4.x

在查看F2的文档时,发现文档中有React和小程序的接入,并没有教程说明如何使用Taro的接入。既然可以在小程序和React工程中使用,理论上在Taro+React工程中也可以使用。说干就干,让我们先跟着文档上面的React教程进行接入。

创建Taro+React工程

根据Taro文档,创建React工程,并且新增钉钉小程序编译选项文档教程

安装F2相关依赖

PowerShell 复制代码
npm install @antv/f2 --save
npm install @antv/f2-react --save

在index文件中复制F2官网Demo

typescript 复制代码
import Canvas from '@antv/f2-react';
import { Chart, Interval } from '@antv/f2';

const data = [
  { genre: 'Sports', sold: 275 },
  { genre: 'Strategy', sold: 115 },
  { genre: 'Action', sold: 120 },
  { genre: 'Shooter', sold: 350 },
  { genre: 'Other', sold: 150 },
];

export default () => <Canvas>
  <Chart data={data}>
    <Interval x='genre' y='sold' />
  </Chart>
</Canvas>

执行代码

通过上述代码,执行后,钉钉小程序控制台报错

创建Canvas绘图上下文

查看错误代码,发现错误文件为@antv/f2-react报错。通过查看代码,发现钉钉小程序是通过调用dd.createCanvasContext(canvasId) 创建canvas绘图上下文,调整相关代码

typescript 复制代码
class ReactCanvas extends React.Component<CanvasProps> {
// 81行
  getProps = () => {
    const { canvasRef, props } = this;
    // 删除改代码 start
    // const canvasEl = canvasRef.current;
    // const context = canvasEl.getContext('2d');
    // 删除结束 end
    // context根据canvasId获取
   const context = Taro.createCanvasContext(props.id ?? 'canvasId');
  };
  render: () => {
    const { props } = this;

    return React.createElement(TaroCanvas, 
      // 添加canvas属性Id,获取上下文
      id: props.id ?? 'f2Canvas',
    });
  }
}

调整代码后,控制台不报错了,并且也渲染出来图表的,不过图表渲染的有点奇怪,只有左上角一点点图形

通过查看F2源码,发现在初始化图表时,需要获取Canvas的宽高。如果外部没有传入宽高,代码中通过DOM API获取元素宽高,但是小程序不支持该方式,所以显示图形异常。以下为相关代码

获取Canvas宽高

scala 复制代码
class Canvas extends EventEmit {

  _initCanvas() {
    // 获取canvas的宽高,对于小程序,如果外部不传入,则宽高为0
    const width = this.get('width') || getWidth(canvas) || canvas.width;

    const height = this.get('height') || getHeight(canvas) || canvas.height;
  }
}

export default Canvas;

初始化设置canvas宽高

既然无法默认获取元素宽高,那我们可以使用小程序提供获取元素宽高的方式,来手动的获取元素的宽高,并且赋值给Props对象

scala 复制代码
// 调整f2-canvas相关代码
class ReactCanvas extends React.Component<CanvasProps> {

 renderOrUpdateCanvas = async (type: 'init' | 'update') => {
    // 接收一个id参数,并且通过小程序API获取元素宽高
    const canvasId = props.id ?? 'f2Canvas'

   // 伪代码,获取元素的宽高
    const { width, height } = await getWidthAndHeight(canvasId)

    return {
      width, 
      height,
    };
  };
  
  componentDidMount() {
    // 元素可能没有渲染出来,等下一帧执行
    nextTick(() => {
      this.renderOrUpdateCanvas("init")
    })
  }
}

设置宽高后,柱状图可以正常显示

虽然柱状图正常显示出来了,不过图表很模糊,像是带了老花镜看似的。通过查看F2文档,发现可以通过配置pixelRatio来设置图表清晰度。并且钉钉小程序可以通过getSystemInfoSync获取设备的分辨率

pixelRatio方案设置

scala 复制代码
// 调整f2-canvas相关代码
class ReactCanvas extends React.Component<CanvasProps> {

  pixelRatio: number;

 renderOrUpdateCanvas = async (type: 'init' | 'update') => {
    const config = {
      // 已经有高清方案,这里使用设备分辨率
      pixelRatio: this.pixelRatio,
      ...props,
    };
  };
  
  componentDidMount() {
    // 获取设备的分辨率
    this.pixelRatio = Taro.getSystemInfoSync().pixelRatio
    // 元素可能没有渲染出来,等下一帧执行
    nextTick(() => {
      this.renderOrUpdateCanvas("init")
    })
  }
}

设置完成之后,查看柱状图,直接不显示数据了。

通过查找钉钉小程序API,没有找到任何关于Canvas精确度的问题。不过最终通过查看钉钉小程序 老大哥支付宝小程序 的开发文档,发现了相关内容 支付宝小程序文档-canvas画布问题,通过给Canvas元素设置高分辨率宽高来解决

kotlin 复制代码
// 调整f2-canvas相关代码
class ReactCanvas extends React.Component<CanvasProps> {
  pixelRatio: number;
  canvasIsInit: boolean;

  // 保存变量值
  state: Readonly<{ width: number; height: number }>;

  renderCanvas = async () => {
    return {
      // 已经有高清方案,这里默认用1
      pixelRatio: this.pixelRatio,
    };
  };
  
componentDidMount() {
    this.pixelRatio = Taro.getSystemInfoSync().pixelRatio

    nextTick(() => {
      getWidthAndHeight(this.props.id ?? 'f2Canvas').then(res => {
        // 设置高精度宽高
        this.setState({
          width: res.width * this.pixelRatio,
          height: res.height * this.pixelRatio,
        })
      })
    })
  }

  render() {
    const { props } = this;

    return React.createElement(TaroCanvas, {
      id: props.id ?? 'f2Canvas',
      // 设置Canvas的宽高,用于高清展示图表
      width: this.state.width || '100%',
      height: this.state.height || '100%',
    });
  }
}

设置完成之后,图表就可以高清展示了

notice:设置Canvas宽高后,TS会报错,width属性不存在。是因为Taro中没有定义Canvas的width和height属性。可以手动添加一下ts文件

typescript 复制代码
declare module '@tarojs/components' {
  export * from '@tarojs/components/types/index';

  import { CanvasProps } from '@tarojs/components/types/Canvas';

  export const Canvas: ComponentType<CanvasProps & { width?: number | string; height?: number | string }>;
}

抹平context差异

图表虽然可以正常展示了,不过并没有坐标信息。当我们尝试给图标添加坐标信息时,发现页面代码报错会报错

typescript 复制代码
    <F2Canvas id='wrap'>
      <Chart data={data}>
        <Interval x='genre' y='sold' />
        <Axis field='genre' /> 
      </Chart>
    </F2Canvas>

通过查看在小程序中使用F2相关文档,发现F2 是基于 CanvasRenderingContext2D 的标准接口绘制的,但是小程序中给的 context 对象不是标准的 CanvasRenderingContext2D , 所以需要将context对象进行封装兼容处理,详情可见: github.com/antvis/f2-c..., 其他小程序也可以按同样的思路封装。继续修改相关代码,抹平小程序context差异。

scala 复制代码
import { my } from '@antv/f2-context'
class ReactCanvas extends React.Component<CanvasProps> {

  renderCanvas = async () => {
    // 抹平context实例,引用f2-context文件
    const context = my(Taro.createCanvasContext(canvasId));
    return {
      // 上下文
      context,
    };
  };

  componentDidUpdate() {
    this.renderCanvas()
  }
}

当我们完成所有操作后,图表和坐标信息就可以完整的展示出来了

事件传递

图表已经可以正常展示,不过当我们使用Tooltip组件时,我们所有的事件都没有作用。

通过查看f-my代码时,我们需要当触发Canvas容器组件事件时,触发图表组件事件

typescript 复制代码
function wrapEvent(e) {
  if (e && !e.preventDefault) {
    e.preventDefault = function () {};
  }
  return e;
}

class ReactCanvas extends React.Component<CanvasProps> {
  canvasEl;
  handleTouchStart = (e) => {
    const canvasEl = this.canvasEl;
    if (!canvasEl) {
      return;
    }
    canvasEl.dispatchEvent('touchstart', wrapEvent(e));
  };
  render() {
    return React.createElement(TaroCanvas, {
      onTouchStart: this.handleTouchStart,
    });
  }
}

事件传递后,可以正常显示文案提示

小结

本章节在Taro+React中接入F2的过程中遇到了不少问题。通过查看React与小程序的接入方式,一步一步的解决下面的问题

  • 在小程序中Canvas的上下文获取方式和web不一致
  • 每次执行代码时需要手动获取Canvas的宽高(无法通过DOM API获取)
  • 小程序模糊问题(pixelRatio的设置)
  • 小程序中Canvas的context不是标准的CanvasRenderingContext2D对象,需要对齐添加补丁。目前查询支付宝小程序文档,发现最新版本已经调整为标准对象了,不过钉钉小程序目前还不支持
  • 小程序事件需要显式定义,并且传递给图表库

目前已经有人封装好了对应的接入代码,我们可以直接在github中查看使用 Taro+React+F2

实战使用

当我们完成上述代码后,就可以正常的根据F2的官网示例在钉钉小程序中使用图表了。不过部分功能需要额外开发

自定义Tooltip

目前F2中默认的Tooltip提示都是在图表顶部显示,并且展示上面只能设置部分属性,不太满足这边的UI规范。好在Tooltip提供了自定义实现的方式,让我们可以自定义显示Tooltip提示。目前古茗通过使用View标签,并且绝对定位的方式来显示对应的文本信息

自定义配置方式

通过设置属性custom,F2不会显示默认Tooltip。通过onChange获取当前的选中的元素信息,可以拿到对应的位置信息与数据信息。从而可以自定义显示对应文本

ini 复制代码
// 自定义配置 
<Tooltip custom ref={tooltipRef} triggerOn="click" onChange={handleTooltipChange} />

View标签位置获取

ini 复制代码
import { View } from '@tarojs/components';
import { nextTick } from '@tarojs/taro';
import classNames from 'classnames';
import { Ref, forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { CustomTooltipRef, TooltipChangeEvent } from './types';
import { PREFIX, getEleClientRect } from '../../../../utils';
import './index.less';

/**
样式设置
.custom-tooltip {
  position: absolute;
  z-index: 2;
  display: none;
  padding: 16px 20px;
  color: #ffffff;
  font-weight: 400;
  background: rgba(12, 12, 12, 0.7);
  border-radius: 4px;
  transform: translateY(50%);
  pointer-events: none;
}

*/

let num = 0;
const cls = `custom-tooltip`;
export const CustomTooltip = forwardRef(
  <T extends {}>(
    props: {
      children: React.ReactNode;
      placement?: 'center';
    },
    ref?: Ref<CustomTooltipRef>
  ) => {
    const { children, placement } = props;
    const customTooltipRef = useRef<HTMLDivElement>(null);
    const [classNum] = useState(num++);

    const handleTooltipChange = (_items: Array<TooltipChangeEvent<T> & T>) => {
      // 优先取第一条数据
      const [{ x, y, yMax }] = _items;
      const otherY = _items?.[1]?.y;
      let yTop = y || otherY;
      /* 在图片中间位置显示 */
      if (placement === 'center') {
        yTop = (yMax || _items?.[1]?.yMax || y) / 2;
      }
      // 处理 props 并更新状态
      // 更新 Tooltip 样式
      const elStyle = customTooltipRef.current?.style;
      if (elStyle) {
        elStyle.left = '0';
        elStyle.top = String(yTop);
        elStyle.visibility = 'hidden';
        elStyle.display = 'block';
        nextTick(() => {
          getEleClientRect(`.${cls}-${classNum}`).then((res) => {
            elStyle.left = String(x > res.width ? x - res.width : x);
            elStyle.visibility = 'visible';
          });
        });
      }
    };

    const hide = () => {
      const elStyle = customTooltipRef.current?.style;
      if (elStyle) {
        elStyle.display = 'none';
      }
    };

    useImperativeHandle(ref, () => ({
      handleTooltipChange,
      hide,
    }));

    return (
      <View ref={customTooltipRef} className={classNames(cls, `${cls}-${classNum}`)}>
        {children}
      </View>
    );
  }
);

实现效果

使用过程中存在的"坑"

示例

横坐标为0,无法触发Tooltip事件
当Axis坐标轴的值为0时,Tooltip点击事件不能点击执行

折线图反转后表现不一致
折线图反转后,空值直接链接了。没有截断处理

解决方案

一般上解决三方库中的问题

  • 对于不在维护的库,通过patch的方式进行处理
  • 升级库版本解决
  • 对于class组件,可以通过本地覆盖式更新代码

目前F2上面所有的组件都是class组件,所以可以通过继承或者修改原型链的方式来解决上述相关问题。 因为上面两个问题属于明显的bug,在我们这边采用修改原型链上面的方法来解决bug。

横坐标为0问题分析

因为是Tooltip的show方法没有执行,通过寻找代码,找到最终原因为判断date值时,没有处理0导致的。5.x已优化该问题

Tooltip中withTooltip的show方法

arduino 复制代码
  show(point, _ev?) {
      const { props } = this;
      const { chart } = props;
      // 该代码获取坐标相关位置
      const snapRecords = chart.getSnapRecords(point, true); // 超出边界会自动调整

      this.showSnapRecords(snapRecords);
  }

Chart的getSnapRecords方法

kotlin 复制代码
  getSnapRecords(point, inCoordRange?) {
    const geometrys = this.getGeometrys();
    if (!geometrys.length) return;
    // geometrys[0]为点击的相关线Line
    return geometrys[0].getSnapRecords(point, inCoordRange);
  }

Line的getSnapRecords方法继承Geometry中的getSnapRecords

arduino 复制代码
  getSnapRecords(point, inCoordRange?): any[] {
    // 该处理没有对value等于0的判断,导致没有返回坐标轴相关信息
    if (!value) {
      return rst;
    }
  }
解决方案,重写Geometry的getSnapRecords方法
javascript 复制代码
const resetGeometryGetSnapRecords = () => {
  Geometry.prototype.getSnapRecords = function (point, inCoordRange?) {
   // 省略代码
    const value = this._getXSnap(invertPoint.x);
    // 放过value为0的场景
    if (!value && value !== 0) {
      return rst;
    }
    return rst;
  }
}

折线图反转后表现不一致

该问题为折线图展示的线不一致问题。第一个图为两条线,第二个图为一条线。查看Line相关代码,发现折线图在render的时候通过this.mapping()获取了对应的记录点

折线图没有处理坐标反转时的坐标

javascript 复制代码
import { jsx } from '../../jsx';
import { isArray } from '@antv/util';
import Geometry from '../geometry';
import { LineProps } from './types';

export default (View) => {
  return class Line extends Geometry<LineProps> {

    splitNulls(points, connectNulls) {
      // 该方法只是判断了y轴是否为空,但是坐标轴反转的时候需要判断x轴是否为空
      for (let i = 0, len = points.length; i < len; i++) {
        const point = points[i];
        const { y } = point;
        if (isArray(y)) {
          if (isNaN(y[0])) {
            if (tmpPoints.length) {
              result.push(tmpPoints);
              tmpPoints = [];
            }
            continue;
          }
          tmpPoints.push(point);
          continue;
        }
        if (isNaN(y)) {
          if (tmpPoints.length) {
            result.push(tmpPoints);
            tmpPoints = [];
          }
          continue;
        }
        tmpPoints.push(point);
      }
      if (tmpPoints.length) {
        result.push(tmpPoints);
      }
      return result;
    }

    mapping() {
  
      return records.map((record) => {
        // 获取坐标点位
        const splitPoints = this.splitNulls(points, connectNulls);
      });
    }

    render() {
      // 获取点位信息
      const records = this.mapping();
      // 省略其他代码...
      return <View {...props} coord={coord} records={records} clip={clip} />;
    }
  };
};
解决方案,重写Line的splitNulls方法
ini 复制代码
  Line.prototype.splitNulls = function (points, connectNulls) {
    const result = [];
    let tmpPoints = [];
    for (let i = 0, len = points.length; i < len; i++) {
      const point = points[i];
      const { x, y } = point;
      /* start   打补丁,处理坐标轴转换引起折线渲染链接问题 */
      if (this.props.coord.transposed) {
        if (Array.isArray(x)) {
          if (isNaN(x[0])) {
            if (tmpPoints.length) {
              result.push(tmpPoints);
              tmpPoints = [];
            }
            continue;
          }
          tmpPoints.push(point);
          continue;
        }
        if (isNaN(x)) {
          if (tmpPoints.length) {
            result.push(tmpPoints);
            tmpPoints = [];
          }
          continue;
        }
      }
      /* end   打补丁,处理坐标轴转换引起折线渲染链接问题 */
    }
    return result;
  };

总结

以上我们通过分析世面上的图表库,选择了在钉钉小程序中使用F2图表库进行开发。因为小程序不支持DOM相关API和Canvas不是标准的CanvasRenderingContext2D对象,接入过程中踩了不少的坑,对新人不太友好。不过最后还是根据官方的接入文档完成了小程序的接入。强烈建议F2官网可以在官网中加入Taro的接入,降低使用门槛

小茗推荐

最后

关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享。

相关推荐
HEX9CF15 分钟前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
凌云行者27 分钟前
使用rust写一个Web服务器——单线程版本
服务器·前端·rust
华农第一蒟蒻43 分钟前
Java中JWT(JSON Web Token)的运用
java·前端·spring boot·json·token
积水成江1 小时前
关于Generator,async 和 await的介绍
前端·javascript·vue.js
___Dream1 小时前
【黑马软件测试三】web功能测试、抓包
前端·功能测试
金灰1 小时前
CSS3练习--电商web
前端·css·css3
人生の三重奏1 小时前
前端——js补充
开发语言·前端·javascript
Tandy12356_1 小时前
js逆向——webpack实战案例(一)
前端·javascript·安全·webpack
TonyH20021 小时前
webpack 4 的 30 个步骤构建 react 开发环境
前端·css·react.js·webpack·postcss·打包
你会发光哎u1 小时前
Webpack模式-Resolve-本地服务器
服务器·前端·webpack