前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

Taro中使用ECharts小结 小程序与H5双端图表组件封装

首页2019-08-31 17:30:21Front-End
TaroECharts小程序

需求是给一个健康类小程序加折线图,展示用户一周的静息心率。听起来十分钟的活,实际卡了大半天。小程序里没有 DOM,ECharts 那句 echarts.init(document.getElementById(...)) 直接就跑不通;换成小程序的 canvas 组件,Taro 又会把 ECharts 那个巨大的单文件一起编译一遍,构建慢到怀疑人生。更麻烦的是这个页面 H5 端也要有,两端不能维护两套图表配置。这篇把当时的解法完整记一遍,从接入 echarts-for-weixin、排除编译、定制构建减包,到封装一个小程序和 H5 共用同一份 option 的图表组件。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 小程序里为什么不能直接用 ECharts,echarts-for-weixin 补了哪一层
  • 接入的三步,拷贝 ec-canvas、排除 Taro 编译、定制构建减包
  • 怎么封装一个双端共用的 Chart 组件,两端的 init 差异藏在哪
  • 用 shouldComponentUpdate 和深比较控制图表刷新,避免无意义的重绘
  • 一个完整的折线图 option 长什么样,哪些字段是小程序端必须调的
  • tooltip 贴边、loading 状态、按端切换这几个常见问题怎么处理

# 一、小程序里画图表卡在哪

ECharts 是给浏览器写的,它要一个真实的 DOM 节点,内部还会摸 window、document 这些全局对象。小程序的逻辑层跑在一个没有 DOM 的 JS 环境里,视图层是另一套渲染引擎,两边靠 setData 通信。这套结构决定了 ECharts 的初始化那一步天然就走不通。

小程序官方给的画图能力是 canvas 组件。它有 canvas 上下文,能画线能填色,但它的 API 和浏览器的 CanvasRenderingContext2D 并不完全一致,事件系统也不一样。

echarts-for-weixin 补的就是中间这一层。它提供一个叫 ec-canvas 的自定义组件,内部把小程序的 canvas 包装成 ECharts 认得的样子,再把小程序的触摸事件转成 ECharts 的鼠标事件。ECharts 那边基本不用改,改的是它下面那层画布。

理解了这个分层,后面的接入步骤和踩坑就都好解释了。

# 二、接入 echarts-for-weixin

# 2.1 把 ec-canvas 拷进项目

下载 https://github.com/ecomfe/echarts-for-weixin 下的 ec-canvas 文件夹到项目的 components 中。

这一步是拷贝源码而不是装 npm 包,原因是 ec-canvas 本身是一个小程序原生自定义组件,需要作为原生组件被引用,走不了常规的模块打包流程。

# 2.2 排除 Taro 编译选项

在 config/index.js 配置文件中找到并加上:

...
compile: {
    exclude: ['src/components/ec-canvas/echarts.js']
}
@前端进阶之旅: 代码已经复制到剪贴板

这一步是整个接入过程里最容易漏、漏了最难查的。echarts.js 是一个几万行的单文件产物,本身已经是编译好的,再让 Taro 的编译链路过一遍,一是没有任何收益,二是构建时间会直接翻几倍,三是压缩或转译过程有可能把它改坏。加上 exclude 之后,这个文件会被原样拷贝过去。

我当时就是没配这条,看着构建卡在那里以为是机器问题,重启了两次才想明白。

# 2.3 定制 echarts 减小包体积

完整版 ECharts 把所有图表类型、所有组件都打进去了,体积相当可观,而小程序主包有明确的大小上限。所以按需定制一份是必要的,不是可选优化。

在线定制入口在这里,勾选你实际用到的图表类型和组件,生成之后替换 src/components/ec-canvas/echarts.js 文件即可:

https://www.echartsjs.com/zh/builder.html

我们那个项目只用了折线图和柱状图,加上 tooltip、legend、grid 这几个组件,定制之后的体积比完整版小了一大截。判断标准很简单,你的图表里没出现过的类型就别勾。

有一点要提醒:以后新增图表类型的时候,得记得回来重新定制一次,否则线上会静默地画不出来,控制台还不一定给你清楚的报错。这个坑建议在项目 README 里写一行,不然过几个月换了人接手根本想不到。

# 三、封装一个双端复用的图表组件

目标很明确:业务代码只写一份 option,小程序和 H5 两端都能画出来,数据变了图表自动刷新。

差异集中在初始化那一步,H5 端要拿 DOM 节点,小程序端要走 ec-canvas 的 init 回调。把这块差异挡住,上层就统一了。

# 3.1 依赖与运行时的 Taro 引用

// src/compoments/Chart.js

import Taro, { Component } from '@tarojs/taro'
import { View } from '@tarojs/components'
import PropTypes from 'prop-types'
import _isEqual from 'lodash/isEqual.js'
import Nerv from 'nervjs'
import * as echarts from '../ec-canvas/echarts'

let Taro_ = Taro
if (process.env.TARO_ENV === 'h5') {
  Taro_ = require('@tarojs/taro-h5').default
}
@前端进阶之旅: 代码已经复制到剪贴板

最后这三行是当时 Taro 1.x 的一个适配写法。H5 端的组件基类来自 @tarojs/taro-h5,跟小程序端不是同一个,所以要按 process.env.TARO_ENV 换一下。前面讲过这个变量是编译期就被替换掉的,所以这个 if 在产物里只会保留一个分支。

Taro 3 之后运行时架构变了,这段适配已经不需要,直接继承 React 的 Component 就行。这里保留原始写法,是因为它能说明当时为什么要这么绕。

# 3.2 两端各自的初始化逻辑

先抽一个两端共用的收尾函数,它做三件事:调用外部传进来的钩子、把图表实例存起来、按 loading 决定是显示加载态还是直接 setOption。

const commonFunc = (_this, chart) => {
  const { option, loading, loadingConf } = _this.props
  _this.beforeSetOption()
  _this.chartInstance = chart
  if (loading) {
    _this.chartInstance.showLoading('default', loadingConf)
  } else {
    _this.chartInstance.setOption(option)
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

beforeSetOption 这个钩子留得很有必要。ECharts 有些能力需要拿到 echarts 这个模块对象本身才能用,比如注册主题、注册地图、构造渐变色。把它作为参数抛给外面,业务方就能在 setOption 之前插一脚。

然后是两端的 init。这里用了一个立即执行函数,在模块加载时就根据编译目标选好实现,避免每次初始化都判断一次:

const initChart = ((type) => {
  switch (type) {
    case 'h5':
      return (_this) => {
        const { chartId } = _this.props
        let node = document.getElementById(chartId)
        let chart = echarts.init(node)
        commonFunc(_this, chart)
      }
    case 'weapp':
      return (_this) => {
        _this.chartRef.init((canvas, width, height) => {
          const chart = echarts.init(canvas, null, {
            width: width,
            height: height
          })
          canvas.setChart(chart)
          commonFunc(_this, chart)
          return chart
        })
      }
  }
})(process.env.TARO_ENV)
@前端进阶之旅: 代码已经复制到剪贴板

两边的差别值得说一下。H5 那条就是标准的 ECharts 用法,拿节点、init、设 option。小程序那条要经过 ec-canvas 的 init 回调,回调会把 canvas 对象和它的实际宽高交给你,echarts.init 的第三个参数必须显式传宽高,因为小程序端读不到 DOM 的尺寸。canvas.setChart(chart) 这句也不能省,它是把图表实例回填给 ec-canvas,触摸事件的转发要靠它。

小程序端的 canvas 尺寸拿不到就画不出来,这一点是和 Web 最不一样的地方。

# 3.3 组件主体与刷新策略

export default class Chart extends Taro_.Component {

  config = {
    component: true,
    usingComponents: {
      'ec-canvas': '../ec-canvas/ec-canvas'
    }
  }

  componentDidMount() {
    initChart(this)
  }

  componentWillReceiveProps(nextProps) {
    const { option: newOption } = nextProps
    if (!_isEqual(nextProps, this.props)) {
      this.refreshChart(newOption)
    }
  }

  shouldComponentUpdate(nextProps) {
    return !_isEqual(this.props, nextProps)
  }
@前端进阶之旅: 代码已经复制到剪贴板

config 里的 usingComponents 是把 ec-canvas 作为原生自定义组件注册进来,这是 Taro 引用小程序原生组件的标准做法。

刷新策略这块是这个组件的关键。ECharts 的实例是自己管理画布的,它不参与 React 的渲染流程,所以图表的更新不能靠 render,只能靠手动调 setOption。这里用 componentWillReceiveProps 捕获新 props,用 lodash 的深比较判断是不是真的变了,变了才刷新。

配上 shouldComponentUpdate 里同样的深比较,父组件任何一次无关的重渲染都不会波及到图表。图表这类组件的重绘成本很高,尤其在小程序端还牵扯 canvas 重画,这层拦截是必要的。

接着是刷新和渲染部分:

fe
  • 一、小程序里画图表卡在哪
  • 二、接入 echarts-for-weixin
    • 2.1 把 ec-canvas 拷进项目
    • 2.2 排除 Taro 编译选项
    • 2.3 定制 echarts 减小包体积
  • 三、封装一个双端复用的图表组件
    • 3.1 依赖与运行时的 Taro 引用
    • 3.2 两端各自的初始化逻辑
    • 3.3 组件主体与刷新策略
    • 3.4 render 与 props 定义
  • 四、用起来长什么样
  • 五、几个绕不过去的问题
  • 六、这篇写于 2019 年,有几处要更新
  • 总结
  • 参考

← Git操作清单 从撤销到rebase的日常命令梳理小程序蓝牙开发实践 BLE 连接流程与踩坑记录 →