别再踩坑 手把手教你把ECharts图表集成到支付宝小程序 解决数据可视化不兼容问题 附完整代码示例
写这文的时候我揉了揉太阳穴,不是因为老了,是因为之前为了搞这个兼容问题熬了几个通宵。如果你现在正对着一个报错页面怀疑人生,别急,咱们从头捋顺。
一、先搞清楚为什么”处不来”
ECharts 是百度开源的纯 JavaScript 可视化库,它的设计哲学是”运行在浏览器里”。你打开浏览器,有 document、有 window、有标准的 Canvas 2D Context,ECharts 直接 new 一个实例,指哪画哪,丝滑得很。
但支付宝小程序是个啥环境?
浏览器环境(ECharts原生支持):
- 有 DOM(document.getElementById)
- 有 window 对象
- Canvas API 是标准 Web Canvas
支付宝小程序环境:
- 没有 DOM,没有 document
- 没有 window,只有 my API
- Canvas 用的是小程序自己的 my.createCanvasContext()
- 沙箱隔离,不能随便操作全局变量
这两套规则根本不兼容,就像让一个习惯用筷子的人突然去用刀叉吃拉面,不是不能吃,但得重新学。
如果你直接把 ECharts 的 CDN 脚本丢进 app.json 的 scripts 里,跑起来大概率会报:
ReferenceError: document is not defined
TypeError: Cannot read property 'getContext' of null
这时候别慌,问题出在”沟通方式”上,不是 ECharts 不行,是你用的姿势不对。
二、选对方案,比硬磕重要一百倍
目前业界主流的三条路:
方案 A:echarts-for-weixin(推荐)
这个项目最初是为微信小程序写的,但它底层用的是标准 Canvas API,支付宝小程序的 my.createCanvasContext 和微信的 wx.createCanvasContext 行为几乎一致,稍微适配一下就能跑。
优点:API 和 ECharts 原生几乎一模一样,迁移成本最低。
缺点:需要手动把 wx 替换成 my,少数细节要微调。
方案 B:小程序原生 Canvas + 自己封装
不依赖任何第三方库,用 my.createCanvasContext 一步步画。
优点:完全可控,没有依赖问题。 缺点:你得自己实现折线图、柱状图、饼图的绘制逻辑,相当于重新造轮子,不推荐。
方案 C:用 web-view 嵌入 H5 页面
把 ECharts 跑在网页里,小程序里用 web-view 组件展示。
优点:ECharts 原版直接用,零改造。
缺点:数据传递麻烦,页面加载慢,用户体验差,而且支付宝对 web-view 有域名白名单限制,审核可能不通过。不推荐生产环境用。
所以,我们走方案 A,把 echarts-for-weixin 改造成适配支付宝小程序的版本。
三、完整集成步骤(一步步跟着做)
第一步:下载 ECharts 核心库和适配层
去这里下载两个文件:
ec-canvas目录(适配层,来自 echarts-for-weixin)echarts.min.js(建议用 4.9.0 或 5.x 的兼容版本)
把 ec-canvas 整个文件夹复制到你的小程序项目根目录,echarts.min.js 也放一起。
第二步:配置页面
在你要放图表的页面的 .json 文件里声明自定义组件:
{
"usingComponents": {
"ec-canvas": "../../ec-canvas/ec-canvas"
}
}
第三步:编写页面结构
在 .wxml 里:
<view class="chart-container">
<ec-canvas
id="mychart-dom-line"
canvas-id="mychart-line"
ec="{{ ecLine }}"
></ec-canvas>
</view>
<view class="chart-container">
<ec-canvas
id="mychart-dom-bar"
canvas-id="mychart-bar"
ec="{{ ecBar }}"
></ec-canvas>
</view>
对应的 .wxss(支付宝小程序用 .wxss 或 .css 都行):
.chart-container {
width: 100%;
height: 400rpx;
}
第四步:最关键——编写 JS 逻辑
这是最容易出错的地方,我直接给你完整可用的代码:
// 页面的 .js 文件
import * as echarts from '../../ec-canvas/echarts';
Page({
data: {
ecLine: {
onInit: null // 初始化回调,后面会赋值
},
ecBar: {
onInit: null
}
},
// 页面加载完成后触发
onLoad() {
this.initLineChart();
this.initBarChart();
},
// 初始化折线图
initLineChart() {
this.setData({
ecLine: {
onInit: (canvas, width, height) => {
// 注意:支付宝小程序传入的 canvas 对象需要适配
const chart = echarts.init(canvas, null, {
width: width,
height: height
});
// 把 chart 实例绑定到 canvas,后续渲染需要用到
canvas.setChart(chart);
chart.setOption(this.getLineOption());
return chart;
}
}
});
},
// 初始化柱状图
initBarChart() {
this.setData({
ecBar: {
onInit: (canvas, width, height) => {
const chart = echarts.init(canvas, null, {
width: width,
height: height
});
canvas.setChart(chart);
chart.setOption(this.getBarOption());
return chart;
}
}
});
},
// 折线图配置
getLineOption() {
return {
title: {
text: '近7日访问量趋势',
left: 'center',
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['访问量', '用户数'],
bottom: 0,
textStyle: { fontSize: 10 }
},
grid: {
left: '10%',
right: '5%',
top: '15%',
bottom: '18%'
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
axisLabel: { fontSize: 10 }
},
yAxis: {
type: 'value',
axisLabel: { fontSize: 10 }
},
series: [
{
name: '访问量',
type: 'line',
smooth: true,
data: [120, 132, 101, 134, 90, 230, 210],
itemStyle: { color: '#1890ff' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(24,144,255,0.3)' },
{ offset: 1, color: 'rgba(24,144,255,0.05)' }
])
}
},
{
name: '用户数',
type: 'line',
smooth: true,
data: [220, 182, 191, 234, 290, 330, 310],
itemStyle: { color: '#52c41a' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(82,196,26,0.3)' },
{ offset: 1, color: 'rgba(82,196,26,0.05)' }
])
}
}
]
};
},
// 柱状图配置
getBarOption() {
return {
title: {
text: '各品类销售额',
left: 'center',
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
grid: {
left: '10%',
right: '5%',
top: '15%',
bottom: '18%'
},
xAxis: {
type: 'category',
data: ['服饰', '食品', '数码', '家居', '美妆', '运动'],
axisLabel: { fontSize: 10, rotate: 0 }
},
yAxis: {
type: 'value',
axisLabel: { fontSize: 10 }
},
series: [
{
name: '销售额(万元)',
type: 'bar',
data: [18, 36, 51, 24, 42, 31],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#722ed1' },
{ offset: 1, color: '#b37feb' }
]),
borderRadius: [4, 4, 0, 0]
},
barWidth: '50%'
}
]
};
},
// 页面显示时确保图表渲染
onShow() {
// 如果需要动态刷新数据,在这里调用
// this.refreshLineChart();
},
// 动态更新数据示例
updateLineData(newData) {
// 获取已存在的 chart 实例并更新
const canvas = this.selectComponent('#mychart-dom-line').canvas;
const chart = canvas.getChart();
if (chart) {
chart.setOption({
series: [{ data: newData.line1 }, { data: newData.line2 }]
});
}
}
});
第五步:ec-canvas 适配层的修改(重要!)
echarts-for-weixin 原始代码是为微信写的,支付宝小程序需要做以下两处关键修改:
文件:ec-canvas/ec-canvas.js
找到 init 方法,把 wx 全部替换为 my,这是最核心的改动:
// 原始微信版本(❌ 不能在支付宝跑)
wx.createCanvasContext(canvasId, this.data.context)
// 改为支付宝版本(✅)
my.createCanvasContext(canvasId, this.data.context)
另外,echarts-for-weixin 里有一个 touch 事件的处理,支付宝小程序的事件名称和微信一致(都是 touchstart、touchmove、touchend),这部分不用改。
但有一个坑:支付宝小程序的 canvas 组件默认不支持滚动穿透,如果图表在可滚动的页面里,需要给页面加上:
page {
height: 100%;
overflow: hidden;
}
或者在图表容器上加:
.chart-container {
touch-action: none;
}
四、最常见的三个坑,我帮你排除了
坑一:图表显示空白,不报错也不渲染
这是最让人抓狂的。90% 的原因是 canvas-id 没对上。
检查这三处必须一致:
<!-- wxml 里的 canvas-id -->
<ec-canvas canvas-id="mychart-line" ...></ec-canvas>
<!-- JS 里 init 用的 canvasId -->
init: (canvas, width, height) => { ... }
<!-- 如果在 ec-canvas.js 里手动创建 canvas context -->
my.createCanvasContext('mychart-line', this) // 必须和上面一致
三个地方写的不一样,图表就会静默失败,什么都不渲染,也不报错。
坑二:图表在真机上模糊/分辨率低
这是因为小程序 canvas 默认物理像素比是 1:1,而手机屏幕通常是 2x 或 3x。解决方法是在初始化时传入 devicePixelRatio:
const chart = echarts.init(canvas, null, {
width: width,
height: height,
devicePixelRatio: my.getSystemInfoSync().pixelRatio // 自动获取设备像素比
});
加这一行,图表在 iPhone 14 这种 Retina 屏上也会很清晰。
坑三:数据更新后图表不刷新
ECharts 图表实例是异步初始化的,如果你在 onLoad 里立刻调用 getChart(),可能拿到的是 null。正确做法是等初始化完成后再操作:
// ❌ 错误:onLoad 里直接获取,此时 canvas 还没渲染完
const chart = this.selectComponent('#mychart-dom-line').canvas.getChart();
// ✅ 正确:在 onInit 回调里拿到 chart 实例并保存
Page({
data: {
chartInstance: null // 用来保存 chart 实例
},
initLineChart() {
this.setData({
ecLine: {
onInit: (canvas, width, height) => {
const chart = echarts.init(canvas, null, {
width, height,
devicePixelRatio: my.getSystemInfoSync().pixelRatio
});
canvas.setChart(chart);
this.setData({ chartInstance: chart }); // 保存实例
chart.setOption(this.getLineOption());
return chart;
}
}
});
},
// 数据变了再更新
updateChart(newData) {
if (this.data.chartInstance) {
this.data.chartInstance.setOption({
series: [{ data: newData }]
});
}
}
});
五、一个完整的真实案例:销售数据仪表盘
下面给你一个可以跑起来的完整示例,模拟一个电商后台的仪表盘页面:
项目结构:
├── pages/
│ └── dashboard/
│ ├── dashboard.wxml
│ ├── dashboard.js
│ ├── dashboard.json
│ └── dashboard.wxss
├── ec-canvas/
│ ├── ec-canvas.js
│ ├── ec-canvas.wxml
│ ├── ec-canvas.json
│ ├── ec-canvas.wxss
│ └── echarts.js ← 把 echarts.min.js 重命名放这里
└── app.js / app.json
dashboard.json:
{
"usingComponents": {
"ec-canvas": "../../ec-canvas/ec-canvas"
},
"navigationBarTitleText": "数据仪表盘"
}
dashboard.wxml:
<view class="page">
<!-- 顶部卡片:核心指标 -->
<view class="card-row">
<view class="metric-card">
<text class="metric-value">¥{{totalSales}}</text>
<text class="metric-label">今日销售额</text>
</view>
<view class="metric-card">
<text class="metric-value">{{orderCount}}</text>
<text class="metric-label">今日订单</text>
</view>
<view class="metric-card">
<text class="metric-value">{{visitCount}}</text>
<text class="metric-label">今日访客</text>
</view>
</view>
<!-- 折线图:趋势 -->
<view class="chart-card">
<view class="chart-title">近30天销售趋势</view>
<ec-canvas
id="trend-chart"
canvas-id="trend-chart"
ec="{{ ecTrend }}"
></ec-canvas>
</view>
<!-- 饼图:品类分布 -->
<view class="chart-card">
<view class="chart-title">品类销售占比</view>
<ec-canvas
id="pie-chart"
canvas-id="pie-chart"
ec="{{ ecPie }}"
></ec-canvas>
</view>
<!-- 柱状图:TOP品类 -->
<view class="chart-card">
<view class="chart-title">品类销售额排行</view>
<ec-canvas
id="rank-chart"
canvas-id="rank-chart"
ec="{{ ecRank }}"
></ec-canvas>
</view>
</view>
dashboard.js:
import * as echarts from '../../ec-canvas/echarts';
// 模拟从服务器获取的数据
const mockData = {
sales: 128650,
orders: 342,
visitors: 5680,
trend: {
labels: Array.from({ length: 30 }, (_, i) => `${i + 1}日`),
sales: [
3200, 3500, 3100, 4200, 4800, 5100, 4900,
3800, 4100, 4600, 5200, 5800, 5400, 5100,
4700, 5300, 5900, 6200, 5800, 5400, 5100,
4800, 5200, 5700, 6100, 6500, 6200, 5800,
5400, 5900
],
orders: [
85, 92, 78, 105, 120, 132, 125,
95, 102, 115, 128, 142, 135, 128,
118, 130, 145, 155, 148, 138, 128,
120, 132, 145, 158, 168, 160, 150,
140, 155
]
},
pie: [
{ value: 335, name: '服饰' },
{ value: 310, name: '食品' },
{ value: 274, name: '数码' },
{ value: 235, name: '家居' },
{ value: 180, name: '其他' }
],
rank: {
labels: ['服饰', '食品', '数码', '家居', '美妆', '运动', '图书'],
values: [335, 310, 274, 235, 180, 156, 120]
}
};
Page({
data: {
totalSales: 0,
orderCount: 0,
visitCount: 0,
ecTrend: { onInit: null },
ecPie: { onInit: null },
ecRank: { onInit: null },
chartInstances: {}
},
onLoad() {
this.loadData();
},
onShow() {
// 每次页面显示时刷新数据
this.refreshCharts();
},
loadData() {
// 实际项目里这里是 my.request 请求接口
// 这里用模拟数据
const data = mockData;
this.setData({
totalSales: data.sales,
orderCount: data.orders,
visitCount: data.visitors
});
},
refreshCharts() {
const data = mockData;
this.initTrendChart(data);
this.initPieChart(data);
this.initRankChart(data);
},
initTrendChart(data) {
this.setData({
ecTrend: {
onInit: (canvas, width, height) => {
const chart = echarts.init(canvas, null, {
width,
height,
devicePixelRatio: my.getSystemInfoSync().pixelRatio
});
canvas.setChart(chart);
this.data.chartInstances.trend = chart;
chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: ['销售额', '订单量'], bottom: 0, textStyle: { fontSize: 10 } },
grid: { left: '8%', right: '5%', top: '10%', bottom: '18%' },
xAxis: {
type: 'category',
data: data.trend.labels,
axisLabel: { fontSize: 9, rotate: 0, interval: 4 },
axisTick: { show: false }
},
yAxis: [
{ type: 'value', name: '销售额', fontSize: 9, position: 'left' },
{ type: 'value', name: '订单', fontSize: 9, position: 'right' }
],
series: [
{
name: '销售额',
type: 'line',
yAxisIndex: 0,
data: data.trend.sales,
smooth: true,
symbol: 'none',
lineStyle: { width: 2, color: '#1890ff' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(24,144,255,0.25)' },
{ offset: 1, color: 'rgba(24,144,255,0.02)' }
])
}
},
{
name: '订单量',
type: 'line',
yAxisIndex: 1,
data: data.trend.orders,
smooth: true,
symbol: 'none',
lineStyle: { width: 2, color: '#722ed1' }
}
]
});
return chart;
}
}
});
},
initPieChart(data) {
this.setData({
ecPie: {
onInit: (canvas, width, height) => {
const chart = echarts.init(canvas, null, {
width,
height,
devicePixelRatio: my.getSystemInfoSync().pixelRatio
});
canvas.setChart(chart);
this.data.chartInstances.pie = chart;
chart.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: {
orient: 'vertical',
right: '5%',
top: 'center',
textStyle: { fontSize: 10 }
},
series: [{
type: 'pie',
radius: ['40%', '70%'],
center: ['38%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 6,
borderColor: '#fff',
borderWidth: 2
},
label: { show: false },
emphasis: {
label: { show: true, fontSize: 12, fontWeight: 'bold' }
},
data: data.pie
}]
});
return chart;
}
}
});
},
initRankChart(data) {
this.setData({
ecRank: {
onInit: (canvas, width, height) => {
const chart = echarts.init(canvas, null, {
width,
height,
devicePixelRatio: my.getSystemInfoSync().pixelRatio
});
canvas.setChart(chart);
this.data.chartInstances.rank = chart;
chart.setOption({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: '12%', right: '8%', top: '5%', bottom: '8%' },
xAxis: { type: 'value', axisLabel: { fontSize: 9 } },
yAxis: {
type: 'category',
data: data.rank.labels.reverse(),
axisLabel: { fontSize: 10 }
},
series: [{
type: 'bar',
data: data.rank.values.reverse(),
itemStyle: {
color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: '#1890ff' },
{ offset: 1, color: '#91d5ff' }
]),
borderRadius: [0, 4, 4, 0]
},
barWidth: '55%',
label: {
show: true,
position: 'right',
fontSize: 9,
formatter: '{c}'
}
}]
});
return chart;
}
}
});
},
// 供外部调用的刷新方法
refreshAll() {
this.loadData();
this.refreshCharts();
}
});
dashboard.wxss:
.page {
padding: 20rpx;
background: #f5f7fa;
min-height: 100vh;
}
.card-row {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
}
.metric-card {
flex: 1;
margin: 0 8rpx;
background: #fff;
border-radius: 16rpx;
padding: 28rpx 20rpx;
text-align: center;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
}
.metric-value {
display: block;
font-size: 36rpx;
font-weight: 700;
color: #1a1a1a;
margin-bottom: 8rpx;
}
.metric-label {
font-size: 22rpx;
color: #8c8c8c;
}
.chart-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
}
.chart-title {
font-size: 28rpx;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16rpx;
}
六、如果还是有问题,用这招排查
把下面这段代码加到你的 ec-canvas.js 的 init 方法最前面,开启调试模式:
init(canvas, width, height) {
console.log('🔍 ec-canvas init 被调用');
console.log('canvas 对象:', canvas);
console.log('width:', width, 'height:', height);
console.log('当前小程序环境:', my.getSystemInfoSync().platform);
// ... 原有逻辑
}
然后在开发者工具里打开”调试器 → Console”,如果看到 ec-canvas init 被调用 但没有后续,说明 onInit 回调没执行,检查 canvas-id 是否一致。
如果打印出来 canvas 是 null,那说明 ec-canvas 组件没渲染出来,检查 .json 里 usingComponents 的路径是否正确。
七、总结一句话
把 ECharts 搬到支付宝小程序,本质就做三件事:换个适配层、把 wx 换成 my、保证 canvas-id 三处一致。剩下的配置和浏览器里写 ECharts 完全一样,不用重新学习。
照着上面这个仪表盘示例跑一遍,遇到哪个具体的报错把错误信息发出来,我能帮你定位。可视化这事,一旦打通了就是真香,前期踩的坑都是值得的。
