原来 ECharts 和飞机大战是同一个东西 —— 我终于搞懂了 Canvas 的本质
我一直觉得 ECharts 和网页游戏是两个完全不搭边的技术领域,一个是做数据报表的,一个是写游戏玩的,直到这周我同时开了两个项目,一个用 ECharts 画销售柱状图,一个用 Canvas 写飞机大战,才突然拍大腿发现:它们居然是同一个妈生的 —— 都是在一块画布上一笔一笔画出来的。
帅哥美女们帮我的掘金点点赞呗😘👍 完整文章地址👉:https://juejin.cn/post/7651524453219647534
说出来有点丢人,从来没打开过控制台看过 Echarts 到底是怎么渲染的。直到这次写完柱状图,随手点了一下审查元素,看到一个大大的 canvas 标签占满了整个图表区域,当时我就懵了。原来那些带渐变、带阴影、带 hover 效果的柱子,不是用 div 拼出来的,也不是 svg,居然是用 js 一行一行画在画布上的。
先从那个龙哥集团的柱状图说起
这周帮朋友做了个简单的销售数据看板,需求很简单,就是把龙哥集团 2025 年奶龙玩具的月度销售额做成一个柱状图。我想都没想就直接用 Vite 初始化了一个原生 JS 项目,安装了 ECharts。
项目结构特别简单,就是 Vite 的标准结构。我一开始把所有代码都写在 main.js 里,包括数据和图表配置,后来觉得太乱了,就把数据单独抽成了一个 data.js 文件。这个好习惯真的要养成,后来改数据的时候不用在一堆配置里翻来翻去。
▼javascript复制代码// data.js /** * 龙哥集团 - 奶龙玩具 2025 年月度销售数据 * 单位:百万元 */ export const company = '龙哥集团' export const months = [ '1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月' ] // 奶龙玩具月销售额(百万元),模拟一年的涨跌趋势 export const salesData = [8.5, 7.2, 9.8, 11.3, 10.6, 13.5, 14.2, 15.8, 13.1, 12.4, 16.7, 18.2]
然后在 main.js 里引入这些数据,初始化 ECharts。说真的,ECharts 的配置项虽然多,但是写多了就会发现套路都差不多:先拿一个 DOM 元素当容器,然后 init 一个实例,再传一个 option 进去就完事了。
▼javascript复制代码// main.js import './style.css' import * as echarts from 'echarts' import { company, months, salesData } from './data.js' document.querySelector('#app').innerHTML = ` <div class="chart-header"> <h1>${company}</h1> <p class="chart-subtitle">奶龙玩具 · 2025 年月度销售额</p> </div> <div id="chart"></div> ` // 初始化 ECharts 柱状图 const chartDom = document.getElementById('chart') const chart = echarts.init(chartDom) const option = { tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, valueFormatter: (value) => value + ' 百万元' }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', data: months, axisLabel: { fontSize: 13 } }, yAxis: { type: 'value', name: '销售额(百万元)', min: 0, axisLabel: { formatter: '{value}' } }, series: [ { name: '销售额', type: 'bar', data: salesData, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#c084fc' }, { offset: 1, color: '#7c3aed' } ]), borderRadius: [4, 4, 0, 0] }, emphasis: { itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#a855f7' }, { offset: 1, color: '#5b21b6' } ]) } } } ] } chart.setOption(option) // 响应式缩放 window.addEventListener('resize', () => chart.resize())
这里有个小坑,我一开始写渐变的时候,把 LinearGradient 的坐标搞反了,写成了 (0, 1, 0, 0),结果渐变变成了从上到下浅到深,特别丑。查了半天文档才发现,前两个参数是渐变起点的 x 和 y 坐标,后两个是终点的坐标。(0,0) 是左上角,(0,1) 是左下角,所以从上到下的渐变应该是 (0,0,0,1)。
最终效果就是这样的,看起来还挺专业的对吧?

好奇心害死猫,我想知道 ECharts 到底在干嘛
看着这个 canvas 标签,我突然很好奇,如果不用 ECharts,我自己能不能用纯 Canvas 画出一个类似的柱状图?于是我开了第二个项目,准备从零开始学 Canvas。
我先写了一个最简单的 demo,画两个矩形,一个填充的,一个描边的。
▼html复制代码<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Canvas 入门</title> </head> <body> <canvas id="myCanvas" width="600" height="400" style="border: 1px solid #333;"> 你的浏览器不支持Canvas </canvas> <script> // 拿到画布元素 const canvas = document.querySelector('#myCanvas'); // 拿到绘画的上下文对象,这就是我们的画笔 const ctx = canvas.getContext('2d'); // 设置填充颜色,画一个蓝色的实心矩形 ctx.fillStyle = '#4299e1'; ctx.fillRect(20, 20, 100, 100); // 设置边框颜色和宽度,画一个红色的空心矩形 ctx.strokeStyle = '#f56565'; ctx.lineWidth = 5; ctx.strokeRect(150, 20, 100, 100); // 清除一个矩形区域,这里我故意清掉了一点红色边框 ctx.clearRect(140, 10, 100, 100); </script> </body> </html>
跑起来一看,我突然明白了什么。原来 Canvas 就是一张白纸,getContext ('2d') 就是给你一支画笔。你告诉画笔什么颜色,它就画什么颜色;你告诉它画个矩形,它就画个矩形;你告诉它擦掉哪里,它就擦掉哪里。就这么简单。
注意这个 clearRect,我当时把它写在了 strokeRect 后面,结果红色边框的左上角被清掉了一块。这个细节后来帮了我大忙,写动画的时候全靠它。
动画的本质,就是擦了再画
会画静态的矩形了,那怎么让它动起来?我第一反应就是用 setInterval,每隔 16 毫秒(大概 60 帧)移动一下矩形的位置。结果跑起来卡得要死,还有严重的残影。
后来查了才知道,setInterval 根本就不是用来做动画的。它的时间不准,而且和屏幕的刷新率不同步,很容易出现掉帧和卡顿。浏览器专门给我们提供了一个做动画的 API,叫 requestAnimationFrame。
▼html复制代码<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Canvas 动画</title> </head> <body> <canvas id="myCanvas" width="600" height="400" style="border: 1px solid #333;"> 你的浏览器不支持Canvas </canvas> <script> const canvas = document.querySelector('#myCanvas'); const ctx = canvas.getContext('2d'); let x = 20; const y = 20; const width = 100; const height = 80; const speed = 3; function animate() { // 每一帧先把整个画布擦干净!这是最重要的一步 ctx.clearRect(0, 0, canvas.width, canvas.height); // 画矩形 ctx.fillStyle = '#4299e1'; ctx.fillRect(x, y, width, height); // 更新位置 x += speed; // 碰到右边界就回到左边 if(x + width > canvas.width) { x = 0; } // 递归调用自己,请求下一帧 requestAnimationFrame(animate); } // 启动动画 animate(); </script> </body> </html>
跑起来的那一刻,我真的被惊艳到了,丝滑得一批。原来动画这么简单,就是三步:
- 把上一帧的东西全部擦掉
- 在新的位置画上新的东西
- 重复上面两步
只要速度够快,每秒重复 60 次,人眼就会觉得这是一个连续的动画。这个原理太重要了,不管是简单的矩形移动,还是复杂的 3D 游戏,本质上都是这个逻辑。

那飞机大战呢?不就是多画几个东西吗
既然一个矩形能动,那我画个飞机,再画几个敌机,再画几颗子弹,不就是飞机大战了吗?说干就干,我又开了一个项目。
项目结构和 ECharts 那个几乎一模一样,都是 Vite 初始化的,src 里就一个 main.js。
其实整个游戏的核心逻辑还是那个 animate 函数,只不过现在每一帧要做的事情变多了:
- 擦干净画布
- 画玩家飞机
- 画所有子弹
- 画所有敌机
- 更新飞机的位置(根据键盘输入)
- 更新所有子弹的位置(向上飞)
- 更新所有敌机的位置(向下飞)
- 检测子弹和敌机的碰撞
- 检测敌机和玩家的碰撞
- 计算分数
- 请求下一帧
我当时写的时候,先让飞机能上下左右移动,然后加了边界限制,不让它飞出屏幕。然后加了空格键发射子弹,再然后加了随机生成敌机。最后加了碰撞检测和游戏结束逻辑。一步一步来,其实没那么难。
最终做出来的效果是这样的:

虽然画面很简陋,但是该有的功能都有了。而且整个游戏没有用任何第三方引擎,纯原生 Canvas 写的,加起来不到 300 行代码。
绕了一圈,回到 ECharts
现在再回头看 ECharts,我突然觉得它一点都不神秘了。它其实就是把我们手动画矩形、画文字、画动画的过程,全部封装成了一个个配置项。你告诉它 x 轴是什么数据,y 轴是什么数据,柱子是什么颜色,它就帮你在 Canvas 上一笔一笔画出整个图表。
你看 ECharts 的 series 里的 type 是 'bar',它就循环遍历你的数据,给每个数据点算好位置和宽度,然后调用 fillRect 画一个个矩形。你加了 tooltip,它就在你鼠标移动的时候,计算你鼠标在哪个柱子上,然后画一个浮层出来。你加了渐变,它就帮你创建一个 LinearGradient 对象,然后设置给 fillStyle。
原来我之前用了那么久的 ECharts,其实一直在用别人写好的 Canvas 绘图库。
这周踩过的几个坑,你们别再踩了
第一个坑,Canvas 的宽高一定要写在标签属性里,不能只写 CSS。如果你只在 CSS 里设置 width 和 height,Canvas 会把它默认的 300x150 的画布拉伸到你设置的大小,画面会变得特别模糊。正确的做法是:
▼html复制代码<!-- 正确 --> <canvas id="myCanvas" width="600" height="400"></canvas> <!-- 错误 --> <canvas id="myCanvas" style="width: 600px; height: 400px;"></canvas>
第二个坑,clearRect 一定要写在每一帧的最开头。我一开始把它写在了 animate 函数的最后,结果整个屏幕都被画满了,全是残影。因为你先画了新的东西,再擦掉上一帧的,等于什么都没擦掉。
第三个坑,requestAnimationFrame 是递归调用的,一定要在 animate 函数里再调用一次自己。我一开始忘了写,结果动画只跑了一帧就停了,调试了半天才发现。
最后说几句
这周最大的收获,不是学会了怎么用 Canvas 写飞机大战,也不是更熟练地使用 ECharts,而是明白了一个道理:很多看起来复杂的技术,底层都是由非常简单的原理组成的。
ECharts 再强大,底层也是 Canvas 的 fillRect、strokeRect 这些基础 API。飞机大战再好玩,本质上也是 “擦了再画” 这个简单的循环。
当然,Canvas 也不是万能的。如果只是做普通的网页布局,用 DOM 肯定更方便,也更容易维护。但是如果你需要做高性能的动画、游戏、或者高度自定义的数据可视化,Canvas 绝对是你的不二之选。
如果你也踩过 Canvas 的这些坑,或者有更好的理解,欢迎在评论区留言,我也想看看大家都是怎么玩的。
