ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序 ECharts 图表:ec-canvas 接入与性能优化

微信小程序 ECharts 图表:ec-canvas 接入与性能优化 小程序里画图表这件事我前后折腾过不少项目。早年用原生 canvas 手撸柱状图简单场景还行一旦客户要求堆叠柱、双 Y 轴、缩放拖拽代码就变成一坨没人敢碰的乱麻。后来转投 ECharts满心以为把网页端那套 option 原样搬过来就能跑结果真机一测白屏、模糊、包体超限轮番上演。这篇东西就按我实际项目的推进顺序从小程序加 ECharts 的技术选型、组件接入、图表实操一路讲到真机踩坑和性能优化中间所有配置尽量给到你能直接抄的程度。不管你是刚接触微信小程序开发的新手还是做过网页端 ECharts、第一次往小程序里搬的老手都能在这里找到对得上号的东西。图表这件事说难不难说简单也真不简单关键是把几个容易翻车的点提前搞清楚后面就顺了。1. 先想明白小程序容器里跑 ECharts 到底难在哪1.1 小程序的渲染环境不是浏览器这是所有问题的根源。网页端的 ECharts 底层依赖 DOM它要 document 去创建 canvas 节点要 window 去监听 resize要 getComputedStyle 去算尺寸要 addEventListener 去接鼠标事件。小程序这套环境里这些东西统统没有它用的是自己的渲染层和逻辑层双线程模型canvas 得靠wx.createCanvasContext或者新版 canvas 2d 接口拿到事件走的是bindtouchstart这类绑定。你把官方 echarts.js 直接 import 进来第一步初始化就给你报错因为里面document.createElement找不到宿主。那 ECharts 是怎么在小程序里跑起来的靠的是官方维护的ec-canvas组件。它做的事情本质上是给 ECharts 造了一套假 DOM——用wx-canvas.js这个适配层模拟出 canvas 对象应有的getContext、setAttribute、addEventListener等方法让 ECharts 以为自己还在浏览器里画图。理解这一点特别重要因为后面遇到的绝大多数诡异问题根源都在这是个模拟环境不是真浏览器上。比如 CSS 里给 canvas 写的宽度样式在旧版 canvas 上根本不生效比如 tooltip 的浮层定位在小程序里压根不是 HTML 元素而是直接画在 canvas 上的。所以我的第一条经验是别拿网页端 ECharts 的调试直觉去套小程序。网页上 F12 一看就知道了小程序里很多问题只能靠打日志、真机预览一点点试出来。心态上先接受这个前提后面踩坑会淡定很多。1.2 三种落地方案的取舍往小程序里塞图表路子其实不止一条。我把常见的三种方案摆出来对比你按项目情况挑。方案实现方式开发成本性能包体积交互能力适用场景ec-canvas 组件官方适配层 ECharts低中高需定制压缩完整触摸、缩放绝大多数常规图表需求原生 canvas 手绘自己用 canvas 接口画极高高极小需自己实现极简单的固定图形WebView 内嵌 H5web-view 加载网页跑 ECharts中低小受通信限制复杂大屏、独立页面先说我为什么不推荐 WebView 方案。虽然它最省事网页端怎么写小程序就怎么写但 web-view 是独立渲染的和小程序原生页面的通信要靠 postMessage延迟明显样式上还有导航栏、滚动等一堆别扭的地方用户体验割裂感很强。除非你要做的是一个完整独立的可视化大屏页面不跟小程序其他页面频繁交互否则别走这条路。原生 canvas 手绘只适合那些图形极其固定、永远不变的场景比如一个静态的进度环。一旦需求变成我想切个数据源你就得重写绘图逻辑维护成本高得吓人。所以绝大多数情况下ec-canvas是那个正确答案。它把 ECharts 的强大能力和小程序的运行环境连起来配置语法和网页端几乎一致网上搜到的 option 写法大部分能直接用。代价就是包体积和性能上要做点优化但这是可解的后面细说。1.3 包体积这条红线得先算清楚微信小程序主包上限是 2M整个小程序主包 分包上限 20M。这个限制卡死了很多人。ECharts 完整版的echarts.min.js压缩后大概 700KB 到 1MB 上下你在主包塞这么一个东西再加上图片、字体、业务代码很容易就顶到 2M 的天花板上传的时候直接给你报主包体积超限。我见过太多人是先写完了功能最后卡在体积上传不上去回头再折腾定制非常痛苦。正确做法是一开始就把 ECharts 放进分包或者用按需定制砍掉不用的图表类型。比如你只做折线图和柱状图就没必要带上桑基图、关系图、树图这些几十 KB 的模块。定制之后一个项目里只用了折线、柱状、饼图的 echarts 包往往能压到 300KB 以内这个差别在 2M 红线下是生死级别的。具体怎么定制下一章手把手讲。2. 环境准备把 ec-canvas 正确装进项目2.1 获取组件与目录结构规划先拿组件。官方仓库在 GitHub 上搜索echarts-for-weixin就能找到里面最核心的就是ec-canvas这个目录。把它整个拷贝到你小程序项目的根目录下或者放到你规划好的组件目录里大致结构是这样项目根目录/ ├── ec-canvas/ │ ├── ec-canvas.js │ ├── ec-canvas.json │ ├── ec-canvas.wxml │ ├── ec-canvas.wxss │ ├── echarts.js │ └── wx-canvas.js ├── pages/ │ └── chart/ │ ├── chart.js │ ├── chart.json │ ├── chart.wxml │ └── chart.wxss ── app.json这里有几个坑我要提前说。第一ec-canvas这个目录名别随便改虽然理论上能改但组件内部有些相对路径引用改名字容易出幺蛾子除非你清楚每个文件的引用关系。第二不同版本的ec-canvas对基础库要求不一样新版默认走 canvas 2d 接口要求基础库 2.9.0 以上。如果你的小程序要兼容很老的版本可以用旧版 canvas但性能和体验会差一些这个后面讲。拷贝完之后在要用图表的页面page.json里注册组件{ usingComponents: { ec-canvas: ../../ec-canvas/ec-canvas } }注意路径是相对路径../../的层级要和你实际目录对应上注册错了编译时会直接报组件找不到。我建议把ec-canvas放在和pages同级的根目录路径引用最省心。2.2 按需定制 echarts把包体积压下来默认的echarts.js是完整版直接上很可能撑爆主包。定制流程我走的是官方提供的在线定制工具思路就是勾选你要用的图表类型和组件生成一个只包含这些功能的压缩包。具体操作打开官方 ECharts 的在线构建页面找到定制入口左侧勾选你需要的模块。以最常见的业务场景为例折线图、柱状图、饼图基本跑不掉对应的需要勾选line、bar、pie这几个图表类型坐标轴组件grid、提示框tooltip、图例legend、标题title、数据缩放dataZoom、直角坐标系cartesian都要勾上如果要画地图还得勾geo和map。右侧会实时显示预计的体积大小勾完下载压缩包里面有个echarts.min.js。拿到这个文件后直接把它重命名为echarts.js覆盖掉ec-canvas目录里原来那个。注意覆盖前先备份一下原始文件万一后面发现少了某个功能还能对照回去重新定制。提示定制时宁可多勾一个用得上但暂时没用到的组件也别漏勾因为漏了要到运行时才会暴露问题那时候你看到的往往是一句莫名其妙的报错排查起来很费劲。定制完之后你的 echarts 包大小能砍掉一半甚至更多。我手上一个电商后台的小程序只用了折线、柱状、饼图加地图定制后 echarts 包压到了 280KB 左右主包体积压力一下就缓过来了。2.3 页面骨架与基础样式组件注册好就可以搭页面了。wxml 里加上ec-canvas标签view classchart-box ec-canvas idline-chart canvas-idline-canvas ec{{ ec }}/ec-canvas /view对应的 wxss 一定要给容器明确的尺寸这是新手最容易翻车的地方.chart-box { width: 100%; height: 500rpx; } ec-canvas { width: 100%; height: 100%; }为什么强调尺寸因为 ECharts 初始化时要读取 canvas 的实际宽高如果容器高度是 0 或者 autochart 就会算出一个 0 尺寸结果是白屏。这个问题在网页端也常见但在小程序里更隐蔽因为真机上一看就是空白你还以为是组件没加载。另外ec-canvas本身也要给 100% 宽高让它撑满外层容器。我习惯用一个固定高度的 view 包住高度用 rpx 而不是百分比因为百分比在一些嵌套布局里会失效。页面 js 里先准备好ec对象这个对象决定了图表怎么初始化Page({ data: { ec: { onInit: initChart } } });但这里有个细节initChart函数得定义在 Page 外面因为ec.onInit需要在组件创建时被调用作用域和 Page 的 this 不是一回事。下一章详细讲两种初始化方式的区别。3. 跑通第一个折线图初始化与数据更新3.1 两种初始化方式onInit 与 lazyLoadec-canvas支持两种初始化时机选哪种取决于你的数据是不是页面一加载就万事俱备。第一种是onInit页面渲染时组件立刻初始化并调用你给的函数画图。适合图表数据在打开页面时就已经确定的情况。写法是ec: { onInit: initChart }其中initChart长这样import * as echarts from ../../ec-canvas/echarts; function initChart(canvas, width, height, dpr) { const chart echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr }); canvas.setChart(chart); const option { xAxis: { type: category, data: [周一, 周二, 周三, 周四, 周五] }, yAxis: { type: value }, series: [{ data: [120, 200, 150, 80, 170], type: line, smooth: true }] }; chart.setOption(option); return chart; }第二种是lazyLoad先不初始化等数据从接口回来之后再手动触发。这个方式在实际项目里用得更多因为数据基本都要请求。写法是ec: { lazyLoad: true }然后在拿到数据的回调里这样调onReady() { this.chartComp this.selectComponent(#line-chart); this.chartComp.init((canvas, width, height, dpr) { const chart echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr }); chart.setOption(this.buildOption()); this.chart chart; return chart; }); }我个人的选择标准很直接如果数据是同步的用 onInit只要涉及网络请求一律 lazyLoad。因为 onInit 是页面渲染阶段触发的那时候你的onLoad里发的请求十有八九还没回来你只能先画个空图然后还要拿到 chart 实例再 setOption绕一圈反而麻烦。注意不管用哪种方式echarts.init返回的 chart 实例一定要自己存起来比如挂到this.chart上。因为后面更新数据、resize、dispose 都要用丢了实例你就只能重建浪费性能。3.2 setOption 的正确姿势与增量更新拿到 chart 实例后更新数据就靠setOption。这里有个参数很多人不注意就是第二个参数notMerge。// 合并更新保留原有配置 this.chart.setOption({ series: [{ data: newData }] }); // 全量替换清掉旧的所有配置 this.chart.setOption(fullOption, true);默认是合并模式也就是把新的 option 和旧的深度合并。这在只改数据、不动结构的时候很方便比如定时刷新折线图的数据点。但有个坑如果你的 series 数量变了比如从 2 条线变成 3 条线合并模式下旧的系列可能残留图表就变成一坨乱的。这种时候一定要传true做全量替换。我吃过这个亏切换部门数据时图表上多出来一条已经不该存在的线查了半天才反应过来是 merge 的锅。还有一种情况是切换图表类型比如从折线切到柱状这种彻底变更配置的场景也用notMerge: true最稳。代价是每次都会走完整的重绘流程性能上不如合并但对于交互触发的切换来说用户感知不到这点开销。数据更新的另一个要点是别在onShow里无脑 setOption。页面切走再切回来会触发 onShow如果你每次都在这里重新 setOption图表会闪一下。除非你的数据确实需要刷新否则 onShow 里只做chart.resize()就够了因为页面尺寸可能变了需要重新计算。3.3 尺寸、dpr 与响应式适配设备像素比 dpr 是个容易被忽略但影响很大的参数。小程序的 canvas 在物理像素和逻辑像素之间有缩放关系如果你 init 的时候不传devicePixelRatio在部分高分辨率机型上图表会糊成一片文字和细线都能看出锯齿。const dpr wx.getSystemInfoSync().pixelRatio;推荐的做法就是 init 时把devicePixelRatio: dpr传进去ec-canvas 在 onInit 里已经把 dpr 作为第四个参数给你了直接用即可。响应式这块小程序的页面尺寸变化主要发生在横竖屏切换或者某些特殊容器里。网页端 ECharts 会自动监听 window.resize小程序里没这回事得手动调// 横竖屏切换时 onResize() { if (this.chart) { this.chart.resize({ width: this.data.canvasWidth, height: this.data.canvasHeight }); } }如果你用的是百分比布局图表容器宽度随屏幕变化那么在某些机型上就得主动 resize 一下。我一般会在onReady后再补一次 resize确保尺寸稳定。实测在 iPhone 系列上不补这一下偶尔会出现右侧留白的情况补了就正常了。4. 几类高频图表的实操细节4.1 饼图labelLine 末端小圆点偏移怎么解饼图是做后台最常用的图表之一而labelLine末端的小圆点偏移是很多人问过我的问题。现象是这样的饼图每个扇区的标签引线末端本该精准落在一个位置结果真机上跑偏了要么和文字重叠要么飘到了扇区外面。这个问题根源在于标签和引线的布局算法对空间的计算。ECharts 从 5.x 之后对饼图标签引入了alignTo机制你可以通过配置来控制对齐方式。我常用的稳定配方是这样series: [{ type: pie, radius: [35%, 60%], label: { alignTo: edge, edgeDistance: 10, formatter: {b}\n{d}% }, labelLine: { length: 15, length2: 12, smooth: 0.2, minTurnAngle: 90 } }]alignTo: edge让标签尽量贴到容器边缘对齐edgeDistance控制离边的距离这样小圆点的落点就稳定了。labelLine里的length是第一段引线长度length2是第二段也就是带小圆点那一截的长度。真机偏得很厉害的时候我会把length2适当调小或者把minTurnAngle设成 90 度限制折角让引线不要出现太急的弯。还有一点如果标签文字特别长会互相挤压这时候可以用labelLayout里的hideOverlap: true隐藏重叠的标签或者干脆把长名称截断用formatter自己拼一个短名字。别指望 ECharts 自动帮你把长文本排得漂漂亮亮它做不到得你手动控制。提示饼图标签密集时把radius设成环形第一项不为 0中间留白能让视觉重心更稳比实心饼图好看不少也更容易放中心汇总数据。4.2 中国地图registerMap 与 geoJSON 加载地图类的需求主要是区域数据可视化比如各省销售额分布。要画地图第一步是把地理数据注册进去因为 ECharts 本身不携带地理边界数据得靠 geoJSON。import * as echarts from ../../ec-canvas/echarts; import chinaJson from ./china.json; echarts.registerMap(china, chinaJson);geoJSON 文件可以找公开的地理数据源下载注意数据要精简别用那种带几十万个坐标点的超精细版本那会让包体积爆炸、渲染卡死。一般做省市级别的可视化用简化的 geoJSON 就够了。注册完之后配置地理坐标系const option { geo: { map: china, roam: true, itemStyle: { areaColor: #f3f6ff, borderColor: #c8d3e8 }, emphasis: { itemStyle: { areaColor: #dbe4ff } } }, series: [{ type: map, geoIndex: 0, data: [ { name: 广东, value: 1200 }, { name: 江苏, value: 900 } ] }] };这里有个容易踩的点series的data里name必须和 geoJSON 里的地区名称严格一致差一个字就渲染不上。比如 geoJSON 里写的是北京市你这里写成北京那就没数据。我一般会先把 geoJSON 里的名称列表打出来对一遍省得真机上一个个试。roam: true开启缩放和平移地图可以拖动放大。在小程序里这个交互是默认支持的因为 ec-canvas 把触摸事件透传给了 ECharts。不过如果地图嵌在 scroll-view 里拖动地图会和外层滚动冲突这个后面踩坑那块讲怎么破。4.3 折线图 x 轴刻度与 tooltip 换行折线图 x 轴刻度标签太多、互相重叠是另一个高频问题。解决办法无非几种旋转、间隔显示、或者换行。xAxis: { type: category, data: dates, axisLabel: { interval: 0, rotate: 45, formatter: (value) { // 按需截断或换行 return value.length 6 ? value.slice(0, 6) … : value; } } }interval: 0表示强制显示所有标签不自动抽稀rotate: 45让标签斜着排给横向空间腾地方。如果你的业务不允许斜排有些设计规范要求水平那就用formatter把长标签截断或者用换行formatter返回带\n的字符串就行。但换行会让 x 轴区域变高别忘了给 grid 底部留出空间否则标签会被裁掉。tooltip 自动换行也是搜索量很高的点。小程序的 tooltip 是画在 canvas 上的不像网页是 HTML 浮层所以没法用 CSS 的 white-space 来换行。要在 formatter 里手动处理tooltip: { trigger: axis, confine: true, formatter: (params) { let result params[0].axisValue \n; params.forEach(item { result item.seriesName item.value \n; }); return result; } }关键点是 formatter 返回的字符串里带上\ncanvas 绘制时就会换行。再配合confine: true让 tooltip 不要超出图表边界在小屏幕上这一点尤其重要否则提示框会被截掉一半。5. 真机踩坑与性能优化实录5.1 常见报错与排查速查表下面这张表是我这些年攒下来的高频问题清单遇到对应现象直接查。现象可能原因解决方向图表完全不显示一片空白容器高度为 0 或未给尺寸给外层 view 和 ec-canvas 明确宽高图表模糊、有锯齿未传 devicePixelRatioinit 时传 dpr 参数报错找不到 canvas 上下文基础库版本过低升级基础库或改用旧版 canvas主包体积超限上传失败echarts 用了完整版在线定制放进分包图表能显示但 tooltip 不弹未开启或 formatter 出错检查 tooltip 配置与 formatter 返回值多图表只有第一个显示id 或 canvas-id 重复每个图表用唯一 id切页面回来图表消失未保存 chart 实例实例挂到 this必要时重新 init这张表里我想单独说切页面回来图表消失这个坑因为它很典型。你从 A 页面跳到 B 页面再返回 Acanvas 有可能被回收图表就没了。解决思路是保存 chart 实例在onShow里判断实例是否还有效失效了就重新初始化。ec-canvas 的新版对这个场景做了处理但旧版不一定稳所以实例管理这件事你最好自己心里有数。5.2 canvas 层级、滚动穿透与真机差异小程序的 canvas 在旧版里是原生组件层级永远最高会盖住普通 viewz-index 都压不住它。如果你在图表上想放个下拉菜单或者弹层旧的 canvas 会直接把它盖住。这个问题的解法有两个一是用cover-view来承载浮层二是换成新版 canvas 2d新版 canvas 支持同层渲染层级问题基本没了。!-- 强制使用旧版 canvas兼容极老基础库时用 -- ec-canvas idchart canvas-idchart ec{{ ec }} force-use-old-canvas{{ true }}/ec-canvas新版 canvas 需要基础库 2.9.0现在绝大多数用户都满足除非你明确知道有一批老设备要支持否则优先用新版性能和层级都更好。滚动穿透是另一个烦人的点。图表放在scroll-view里你在地图上拖动想缩放结果整个页面在滚动。解决思路是给 ec-canvas 的触摸事件做拦截或者用catchtouchmove阻止事件冒泡。ec-canvas 组件本身提供了disableTouch配置但它会把图表的所有触摸交互都禁掉包括拖拽和缩放所以不能一刀切。我的做法是只在需要交互的图表上开启 roam不放 scroll-view 里或者把地图单独放到一个占满屏幕的容器里避免和外层滚动争抢。真机差异这件事只能靠多测。iOS 和安卓在 canvas 渲染上确实有细微差别比如文字基线、抗锯齿效果。我遇到过一次安卓上文字显示不全的问题最后发现是字号和容器比例的问题调了一下就好了。经验就是能在开发者工具里看到的不一定真机就那样真机上出现的问题先在开发者工具里对着日志复现复现不了就加日志到真机上看。别凭猜测改代码效率太低。5.3 大数据量与多图联动的性能处理数据量大到几千上万个点的时候折线图会明显卡。这时候有几个优化方向。第一是数据降采样。ECharts 有内置的sampling配置对付超长折线很有效series: [{ type: line, sampling: lttb, data: hugeData }]lttb是一种保形降采样算法能在大幅减少点数的同时保留曲线走势视觉效果几乎看不出差别。几千个点降到几百个点渲染一下就顺畅了。第二是关掉动画。animation: false在数据频繁刷新的场景下能省不少性能图表虽然少了一点动效但流畅度优先级更高。第三是把重绘频率降下来。如果数据是实时推送的别每来一条就 setOption 一次用节流攒一批再更新比如 500ms 更新一次。这样既保证视觉上的实时感又不至于把渲染线程压垮。多图联动是另一个常见需求比如点击一个图表的某个扇区另一个图表联动过滤。实现上就是监听第一个图表的点击事件拿到参数后更新第二个图表的数据this.chart.on(click, (params) { const selected params.name; this.secondChart.setOption({ series: [{ data: filterBy(selected) }] }); });注意在小程序里事件监听要绑在真实的 chart 实例上别绑在 canvas 元素上因为事件最终都会被 ec-canvas 转成 ECharts 的事件模型。绑对了之后联动响应是很跟手的。6. 多图表页面协作与动态标题设置6.1 同页面多个图表实例管理一个页面放好几个图表是很常见的比如仪表盘页面同时有折线、柱状、饼图。这时候要特别注意两个事id 唯一和实例分别保存。每个ec-canvas的id和canvas-id都必须唯一否则组件会互相串出现第二个图表画的是第一个图表的数据这种诡异现象。我见过有人复制粘贴忘了改 id调试了一下午。ec-canvas idchart-line canvas-idcanvas-line ec{{ ecLine }}/ec-canvas ec-canvas idchart-bar canvas-idcanvas-bar ec{{ ecBar }}/ec-canvas ec-canvas idchart-pie canvas-idcanvas-pie ec{{ ecPie }}/ec-canvas实例也要分别挂到不同变量上this.lineChart null; this.barChart null; this.pieChart null;如果这几个图表用同一份数据的不同维度可以在接口回来后统一刷新但每个实例各调各的 setOption。别想着复用一个 chart 实例去画多个图那是不可能的一个实例对应一个 canvas。另外页面里图表多了要注意初始化时机。用 lazyLoad 的话数据回来后一口气把几个图都 init 了可能会有一个短暂的卡顿因为每个 init 都要创建 canvas 上下文。优化办法是按需初始化比如用wx.nextTick或者分帧处理先初始化用户第一眼看到的那个图表。6.2 结合 wx.setNavigationBarTitle 动态更新标题页面标题跟着图表状态变是提升体验的一个小细节。比如切换图表维度时把标题从月度销售趋势改成季度销售趋势。小程序里用wx.setNavigationBarTitle就能改switchDimension(e) { const dim e.currentTarget.dataset.dim; const titleMap { month: 月度销售趋势, quarter: 季度销售趋势, year: 年度销售趋势 }; wx.setNavigationBarTitle({ title: titleMap[dim] }); this.refreshChart(dim); }这样用户切维度时标题同步更新图表标题和页面标题一致认知负担小。注意wx.setNavigationBarTitle是异步的不用等回调直接调就行。如果你用的是自定义导航栏那这个 API 就不生效了得自己通过 data 控制自定义导航栏的文字这个要注意区分。顺带提一句图表的title配置和页面导航栏标题最好别重复太多。页面导航栏已经写了销售分析图表里再写一遍销售分析就冗余了。我一般会让图表内部标题承担更具体的说明比如带时间范围或数据口径页面标题承担模块定位各司其职。图表在小程序里的落地说到底就是理解模拟环境 用好 ec-canvas 提前压体积 真机多验这四件事。我见过太多人在第一步就被白屏劝退其实大部分白屏就是容器没给高度改一行样式的事。把上面这些配置和坑点过一遍一个稳定能用的图表页面基本就成型了。剩下那些高级交互和视觉细节都是在这个基础上慢慢调出来的先跑起来比什么都重要。
返回列表