说实话,第一次在支付宝小程序里跑 ECharts 的时候,我差点把键盘砸了。
你以为只要 npm install 然后 import 就完事了?天真。
支付宝小程序的环境跟微信小程序、H5 真的不太一样。canvas 的实现机制、JS 的执行沙箱、还有那个让人头秃的 easychart 兼容问题……每一个坑都能让你排查到凌晨三点。
我花了整整两天时间,翻遍了 GitHub 的 issues、支付宝官方文档的角落,甚至去翻了 ECharts 源码里关于小程序适配的部分,才终于把那个该死的折线图渲染出来。
今天就把这其中的血泪史,掰开了、揉碎了,讲给你听。希望能帮你省下至少一天的时间。
第一步:为什么你不能直接“裸用” ECharts?
很多新手(包括当初的我)拿到 ECharts 的文档,一看:“哦,小程序支持,有 ec-canvas 组件。” 然后兴冲冲地把微信小程序的代码复制到支付宝小程序项目里。
结果报错如下:
Error: Component "ec-canvas" is not found in path "components/ec-canvas/ec-canvas"
或者更离谱的:
VM1922:1 ReferenceError: wx is not defined
核心原因只有一个:ECharts 官方的小程序适配层,是基于微信小程序的 API 封装的。
支付宝小程序的 wx.* 系列 API 全部换成了 my.*,比如:
wx.createCanvasContext->my.createSelectorQuery+my.createCanvasContextwx.getSystemInfoSync->my.getSystemInfoSync
而且,支付宝小程序的 canvas 是自定义组件 canvas,渲染流程跟微信略有不同,特别是在 2.0 版本以上的支付宝小程序基础库,对 canvas 的操作有严格的沙箱限制。
所以,你必须替换底层依赖,或者使用专门为支付宝小程序适配过的 ECharts 包。
第二步:选型——用哪个包?
目前社区里比较主流的几套方案:
| 方案 | 优点 | 缺点 | 推荐指数 |
|---|---|---|---|
| 原生 ec-canvas 修改版 | 社区资源多,原理清晰 | 需要自己改 wx 为 my,坑多 | ⭐⭐⭐ |
| echarts-for-weixin (支付宝适配版) | 有人维护,相对完整 | 更新滞后,部分新版图表支持差 | ⭐⭐⭐⭐ |
| antv/f2 (蚂蚁官方) | 专为阿里系设计,支付宝亲儿子 | 图表类型不如 ECharts 丰富 | ⭐⭐⭐⭐⭐(如果是中后台项目) |
| easy-chart | 专门解决支付宝兼容问题 | 文档较少,API 不一致 | ⭐⭐⭐ |
我的建议:
- 如果你只是要展示常见的折线、柱状、饼图,强烈建议直接用
@antv/f2。这是蚂蚁集团自己的数据可视化库,跟支付宝小程序是亲生父子关系,兼容性最好,性能也最稳。 - 如果你必须用 ECharts(比如公司规范、复杂定制图表),那就往下看,我用的是 基于 echarts-for-weixin 深度修改的支付宝兼容版本。
第三步:实战——从零搭建 ECharts 支付宝版(避坑全过程)
假设你决定硬刚 ECharts,以下是完整、可运行的避坑指南。
3.1 初始化项目并安装依赖
# 进入你的支付宝小程序项目根目录
npm init -y
npm install echarts-for-weixin --save
注意:不要直接 npm install echarts,因为你需要的是带小程序适配层的包装包,而不是纯净的 ECharts 核心。
3.2 关键修改:把 wx 换成 my
这是最关键、也最容易出错的一步。
echarts-for-weixin 这个包内部大量使用了 wx 对象。你需要对它的源码进行“手术”。
方法一:手动替换(适合小改动)
找到 node_modules/echarts-for-weixin 目录下的核心文件,比如 ec-canvas.js 和 wx-canvas.js,全局搜索 wx.,替换为 my.。
但这里有个大坑:有些方法在支付宝里名字不一样!
例如:
wx.createCanvasContext(canvasId)在支付宝里是my.createCanvasContext(canvasId)—— 这个还好,直接替换即可。wx.createSelectorQuery()在支付宝里也是my.createSelectorQuery()。- 但是,
wx.getSystemInfoSync()在支付宝里也是my.getSystemInfoSync()。
然而! 支付宝小程序的 canvas 初始化方式不同。微信是通过 <canvas type="2d"></canvas> 然后调用 wx.createSelectorQuery().select('#canvas').fields({node:true,size:true}).exec() 获取节点,再 wx.createCanvasContext(node)。
支付宝的 2.0 基础库也支持这种方式,但要注意 异步问题。
方法二:使用已适配好的 Fork 版本(推荐)
GitHub 上有人专门做了这个工作,比如 ec-canvas-alipay。你可以直接 clone 下来放到 components 目录,而不是依赖 npm 包。这样更可控。
我推荐的方式是:自己维护一个 ec-canvas 组件,把源码拷出来,然后进行以下修改。
3.3 创建自定义组件 ec-canvas
在你的项目里新建 components/ec-canvas/ec-canvas 目录,包含以下文件:
ec-canvas.jsec-canvas.jsonec-canvas.wxmlec-canvas.wxss(支付宝用 .css 后缀)
ec-canvas.wxml
<view class="ec-canvas" style="width:{{canvasWidth}};height:{{canvasHeight}};">
<canvas
type="2d"
id="myChart"
class="ec-canvas__canvas"
style="width:100%;height:100%;"
canvas-id="myChart"
></canvas>
</view>
注意点:
- 必须加
type="2d",这是支付宝小程序高性能 canvas 的关键。 canvas-id和id最好保持一致,避免后续查询混淆。
ec-canvas.js(核心逻辑,重点看注释)
// ec-canvas.js
const app = getApp()
// 引入 echarts 主文件(注意路径)
import * as echarts from 'echarts/dist/echarts.esm.min.js'
Component({
properties: {
canvasId: {
type: String,
value: 'ec-canvas'
},
lazyLoad: {
type: Boolean,
value: false
},
// 强制指定宽度高度,避免自适应问题
forceUseOldCanvas: {
type: Boolean,
value: false
}
},
data: {
canvasWidth: 0,
canvasHeight: 0,
// 其他数据...
},
ready: function () {
if (this.data.lazyLoad) {
this.init()
} else {
this.init()
}
},
methods: {
init: function () {
const query = my.createSelectorQuery().in(this)
// 关键:支付宝小程序获取 canvas 节点必须用 node 方式
query.select('#myChart')
.fields({ node: true, size: true })
.exec((res) => {
if (!res[0]) {
console.error('Canvas node not found')
return
}
const canvas = res[0].node
const ctx = canvas.getContext('2d')
// 这里有个大坑:支付宝的 canvas 像素比需要手动获取
const dpr = my.getSystemInfoSync().pixelRatio
// 设置 canvas 的实际渲染尺寸(解决高清屏模糊问题)
const rect = my.getSystemInfoSync()
const width = this.data.canvasWidth || res[0].width
const height = this.data.canvasHeight || res[0].height
canvas.width = width * dpr
canvas.height = height * dpr
ctx.scale(dpr, dpr)
// 初始化 echarts
const instance = echarts.init(canvas, null, {
width: width,
height: height,
devicePixelRatio: dpr
})
this.instance = instance
this.canvas = canvas
// 暴露给父组件调用
this.triggerEvent('init', {
echarts: instance,
canvas: canvas
})
})
},
// 父组件调用此方法来设置配置项
setChartOption: function (option) {
if (this.instance) {
this.instance.setOption(option, true) // true 表示不合并,完全替换
}
},
// 获取图表实例
getInstance: function () {
return this.instance
},
// 销毁实例(防止内存泄漏)
dispose: function () {
if (this.instance) {
this.instance.dispose()
this.instance = null
}
},
// 处理触摸事件,转发给 echarts
touchStart: function (e) {
if (this.instance && e.touches.length > 0) {
const touch = e.touches[0]
const chartTouch = {
x: touch.clientX,
y: touch.clientY
}
this.instance.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: this.getDataIndex(chartTouch)
})
}
},
touchMove: function (e) {
if (this.instance) {
this.instance.dispatchAction({ type: 'showTip', x: e.touches[0].clientX, y: e.touches[0].clientY })
}
},
touchEnd: function (e) {
if (this.instance) {
this.instance.dispatchAction({ type: 'hideTip' })
}
},
// 简单的 dataIndex 计算(实际项目建议用 echarts 自带的 on 事件)
getDataIndex: function (touch) {
// 这里需要根据你的具体图表类型实现
return -1
}
}
})
3.4 ec-canvas.json(组件配置)
{
"component": true,
"usingComponents": {}
}
3.5 ec-canvas.css
.ec-canvas {
width: 100%;
height: 100%;
position: relative;
}
.ec-canvas__canvas {
width: 100%;
height: 100%;
display: block;
}
第四步:页面集成与数据绑定
现在组件准备好了,我们来看怎么在页面里用它。
4.1 page.json
{
"navigationBarTitleText": "数据看板",
"usingComponents": {
"ec-canvas": "../../components/ec-canvas/ec-canvas"
}
}
4.2 page.axml
<view class="container">
<view class="chart-card">
<ec-canvas
id="myChart"
canvas-id="myChart"
force-use-old-canvas="{{false}}"
onInit="onChartInit"
onTouchStart="onTouchStart"
onTouchMove="onTouchMove"
onTouchEnd="onTouchEnd"
></ec-canvas>
</view>
</view>
4.3 page.js(核心逻辑)
// page.js
Page({
data: {
ec: {
lazyLoad: true // 可选,延迟加载
}
},
onReady() {
// 页面渲染完成后,手动触发一次更新(如果 lazyLoad 没设)
this.updateChart()
},
onChartInit(e) {
const { ec } = e.detail
this.ecInstance = ec
// 初始渲染
this.setChartOption(ec)
},
// 设置图表配置
setChartOption(instance) {
const option = {
title: {
text: '月度销售趋势',
left: 'center',
textStyle: {
fontSize: 16,
color: '#333'
}
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255,255,255,0.9)',
borderColor: '#ddd',
borderWidth: 1,
textStyle: {
color: '#333'
}
},
legend: {
data: ['销售额', '利润'],
bottom: 10
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
axisLine: {
lineStyle: {
color: '#ccc'
}
},
axisLabel: {
color: '#666'
}
},
yAxis: {
type: 'value',
axisLine: {
show: false
},
splitLine: {
lineStyle: {
color: '#f0f0f0'
}
},
axisLabel: {
color: '#666'
}
},
series: [
{
name: '销售额',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
itemStyle: {
color: '#1890ff'
},
areaStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(24,144,255,0.3)' },
{ offset: 1, color: 'rgba(24,144,255,0.05)' }
]
}
},
data: [12000, 13200, 10100, 13400, 9000, 23000]
},
{
name: '利润',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
itemStyle: {
color: '#52c41a'
},
data: [2200, 2820, 2010, 2940, 1900, 4300]
}
]
}
instance.setOption(option)
},
// 动态更新数据(比如从接口获取后)
updateChart() {
if (this.ecInstance) {
// 模拟异步数据
const newData = [15000, 16200, 14100, 17400, 13000, 25000]
const profitData = [3200, 3500, 3100, 3800, 2900, 5300]
this.ecInstance.setOption({
series: [
{ data: newData },
{ data: profitData }
]
}, true)
}
},
// 触摸事件透传(可选,用于交互)
onTouchStart(e) {
// 如需复杂交互,可在这里处理
},
onTouchMove(e) {
if (this.ecInstance) {
const touch = e.touches[0]
this.ecInstance.dispatchAction({
type: 'showTip',
x: touch.clientX,
y: touch.clientY
})
}
},
onTouchEnd(e) {
if (this.ecInstance) {
this.ecInstance.dispatchAction({ type: 'hideTip' })
}
}
})
4.4 page.css
.container {
padding: 20rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.chart-card {
background-color: #ffffff;
border-radius: 16rpx;
padding: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05);
margin-bottom: 20rpx;
}
第五步:常见坑点与解决方案汇总
坑 1:图表渲染模糊
现象:在 iPhone 等高清屏上,图表线条发虚。
原因:没有正确处理 devicePixelRatio。
解决:
在 ec-canvas.js 中,务必执行:
const dpr = my.getSystemInfoSync().pixelRatio
canvas.width = width * dpr
canvas.height = height * dpr
ctx.scale(dpr, dpr)
并且在 echarts.init 时传入 `{ devicePixelRatio
