你如果是以前端面试 + 公司实际开发为目标学 Canvas,我建议不要只学“画矩形、画圆”,而是按:
基础 API → 图片处理 → 交互 → 动画 → 性能 → 实际业务场景 → 工程化 → 面试高频问题
这条路线学。
Canvas 2D 本质上就是拿到CanvasRenderingContext2D,然后通过它绘制形状、文字、图片等内容。(MDN Web Docs)
一、Canvas 到底是什么
最简单:
<canvas id="canvas" width="500" height="300"></canvas>JS:
const canvas = document.getElementById('canvas') const ctx = canvas.getContext('2d') ctx.fillStyle = 'red' ctx.fillRect(100, 50, 200, 100)可以把 Canvas 理解成:
Canvas ↓ 一块“位图画布” ↓ JS 操作 ctx ↓ 往里面画东西和普通 DOM 最大区别:
DOM: <div> <span>hello</span> </div> 浏览器维护: 元素 → 属性 → 样式 → 布局 → 渲染Canvas:
<canvas> ↓ 像素 ↓ 你自己画Canvas 画完以后,浏览器并不知道你画了一个“按钮”还是“圆形”。
所以:
Canvas 更像一个“画板”,而不是 DOM 元素树。
二、Canvas 基础
1. 创建 Canvas
<canvas id="canvas"></canvas>const canvas = document.querySelector('#canvas') const ctx = canvas.getContext('2d')常见 context:
canvas.getContext('2d') canvas.getContext('webgl')公司前端业务中,2D Canvas 最常见。
三、Canvas 宽高非常重要
这是一个特别容易踩坑的地方。
推荐:
<canvas width="800" height="600"></canvas>而不是:
canvas { width: 800px; height: 600px; }两者不是一回事。
Canvas 有两套尺寸
Canvas 实际像素尺寸 ↓ width / height Canvas CSS 显示尺寸 ↓ style width / height例如:
<canvas width="800" height="600" style="width:400px;height:300px"> </canvas>实际绘制:
800 × 600显示:
400 × 300相当于浏览器进行了缩放。
四、画矩形
最常用的几个 API。
fillRect
ctx.fillStyle = 'red' ctx.fillRect( 100, 50, 200, 100 )参数:
x y width heightstrokeRect
只画边框:
ctx.strokeStyle = 'blue' ctx.lineWidth = 5 ctx.strokeRect(100, 50, 200, 100)clearRect
清除区域:
ctx.clearRect( 0, 0, canvas.width, canvas.height )动画里面非常常见。
五、画路径
Canvas 真正强大的地方是 Path。
ctx.beginPath() ctx.moveTo(100, 100) ctx.lineTo(200, 100) ctx.lineTo(150, 200) ctx.closePath() ctx.stroke()画的是:
100,100 -------- 200,100 \ / \ / \ / 150,200六、beginPath 为什么重要
例如:
ctx.beginPath() ctx.moveTo(100, 100) ctx.lineTo(200, 100) ctx.stroke()再画一条:
ctx.moveTo(100, 200) ctx.lineTo(200, 200) ctx.stroke()- 第一次
stroke():绘制当前路径(线 1),路径还留在上下文中,不清除 moveTo/lineTo:继续往同一个路径对象追加,现在路径 = 线 1 + 线 2- 第二次
stroke():把全部路径再次描边 →线 1 被重复绘制 2 次
后果:
- 如果线条有透明度,第一条线会变深;
- 如果换了
strokeStyle颜色,第一条线也会变成新颜色!(最经典坑)
推荐始终明确:
ctx.beginPath() ... ctx.stroke() ctx.beginPath() ... ctx.stroke()beginPath()的作用:新建一条独立路径,切断和之前旧路径的关联。
stroke()/fill()只是把「当前所有累积的路径」画出来,不会清空路径。
否则路径可能不断累积。
七、画线
ctx.beginPath() ctx.moveTo(50, 50) ctx.lineTo(200, 100) ctx.lineTo(300, 50) ctx.stroke()常用:
ctx.lineWidth = 2 ctx.strokeStyle = '#333' ctx.lineCap = 'round' ctx.lineJoin = 'round'| 属性 | 控制什么 | 常用值 |
|---|---|---|
lineCap | 线段两端 | butt / round / square |
lineJoin | 折线拐角 | miter / round / bevel |
lineWidth | 线宽 | 数字 |
八、画圆
ctx.beginPath() ctx.arc( 200, 150, 50, 0, Math.PI * 2 ) ctx.fill()参数:
x y radius startAngle //起始弧度 endAngle //结束弧度完整:
ctx.beginPath() //0度画到2Π ctx.arc( 200, 150, 50, 0, Math.PI * 2 ) ctx.fillStyle = 'red' ctx.fill()九、Canvas 颜色
fillStyle
填充颜色:
ctx.fillStyle = '#ff0000'strokeStyle
边框颜色:
ctx.strokeStyle = '#000'透明度
ctx.globalAlpha = 0.5或者:
ctx.fillStyle = 'rgba(255,0,0,0.5)'十、渐变
实际项目里也经常用。
线性渐变
//起点(0,0) → 终点(300,0) const gradient = ctx.createLinearGradient( 0, 0, 300, 0 ) gradient.addColorStop(0, 'red') gradient.addColorStop(1, 'blue') // 把填充样式设置成这个渐变 ctx.fillStyle = gradient // 画矩形,使用上面的渐变填充,在渐变范围内的形状都是渐变色的 ctx.fillRect(0, 0, 300, 100)径向渐变
const gradient = ctx.createRadialGradient( 150, 150, 20, 150, 150, 100 ) gradient.addColorStop(0, 'white') gradient.addColorStop(1, 'blue') ctx.fillStyle = gradient ctx.fillRect(0, 0, 300, 300)十一、Canvas 文字
ctx.font = '30px Arial' ctx.fillStyle = 'black' ctx.fillText( 'Hello Canvas', 100, 100 )strokeText
ctx.strokeText( 'Hello', 100, 100 )文字对齐
ctx.textAlign = 'center' ctx.textBaseline = 'middle'例如:
ctx.textAlign = 'center' // 水平对齐:文字中心点对准 x坐标 ctx.textBaseline = 'middle' // 垂直对齐:文字中心点对准 y坐标 ctx.fillText( 'Hello', canvas.width / 2, canvas.height / 2 )这个在:
验证码
海报
图片水印
图表
里面都非常常见。
十二、保存和恢复状态
这是 Canvas 很重要的 API。
ctx.save() ctx.fillStyle = 'red' ctx.translate(100, 100) ctx.rotate(Math.PI / 4) ctx.fillRect(0, 0, 100, 100) ctx.restore()可以理解:
save() ↓ 保存当前绘图状态 修改各种属性 ↓ 绘制 restore() ↓ 恢复之前状态save()把当前画布所有绘图状态压栈保存;restore()弹出,恢复到上次 save 时的状态。
⚠️ 重点:保存的是【绘图状态】,不是画布上已经画出来的像素!已经画在画布的图形,restore 不会擦掉。就能从之前的状态继续画
实际开发非常推荐:
ctx.save() // 修改状态 // 绘制 ctx.restore()避免污染后面的绘制。
十三、坐标变换
Canvas 的高级开发经常会用到。
translate
移动坐标系:
ctx.translate(100, 50)之后:
ctx.fillRect(0, 0, 100, 100)实际上相当于:
从 100,50 开始画rotate
旋转:
ctx.rotate(Math.PI / 4)注意:
Math.PI等于:
180°所以:
Math.PI / 2就是:
90°scale
ctx.scale(2, 2)相当于放大 2 倍。
十四、图片绘制——公司最重要的一部分
Canvas 很多实际项目不是“画图形”,而是:
处理图片。
核心 API:
ctx.drawImage()它支持多种图片源,包括HTMLImageElement、HTMLVideoElement、HTMLCanvasElement、ImageBitmap等。(MDN Web Docs)
最简单
const img = new Image() img.onload = () => { ctx.drawImage(img, 0, 0) } img.src = '/test.jpg'注意一定要等:
img.onload否则图片可能还没加载完成。
十五、图片缩放
ctx.drawImage( img, 0, 0, 300, 200 )把图片画成:
300 × 200十六、图片裁剪
drawImage最强大的地方之一。
ctx.drawImage( img, sx, sy, sw, sh, dx, dy, dw, dh )前 4 个:源裁切(从原图挖一块)
sx:源图片x 起点(原图坐标系)sy:源图片y 起点sw:源图片裁切宽度(要挖多宽)sh:源图片裁切高度(要挖多高)
后 4 个:绘制到画布(把挖出来的块画到 canvas)
5.dx:画布上x 起点
6.dy:画布上y 起点
7.dw:画布上绘制的宽度(拉伸 / 压缩)
8.dh:画布上绘制的高度(拉伸 / 压缩)
意思:
原图: ┌──────────────────┐ │ │ │ ┌──────┐ │ │ │裁剪区│ │ │ └──────┘ │ │ │ └──────────────────┘ ↓ Canvas ┌──────────────┐ │ │ │ 裁剪内容 │ │ │ └──────────────┘例如:
ctx.drawImage( img, 100, 100, 300, 300, 0, 0, 300, 300 )十七、公司实际场景 ①:图片压缩
这个非常重要。
你之前关注过的:
用户上传原图 → 前端压缩 → WebP → OSS
Canvas 就是核心技术之一。
流程:
用户选择图片 ↓ File ↓ Image ↓ Canvas ↓ 限制最大宽高 ↓ drawImage ↓ toBlob ↓ 压缩后的 Blob/File ↓ OSS ↓ 拿到 URL一个实际的图片压缩函数
function compressImage(file, maxWidth = 2048, quality = 0.8) { return new Promise((resolve, reject) => { const img = new Image() const url = URL.createObjectURL(file) img.onload = () => { URL.revokeObjectURL(url) let { width, height } = img let scale = 1 // 调整图片缩放,限制图片宽高,像素太高的图片会被压像素,会有个像素封顶值 if (width > maxWidth) scale = Math.min(scale, maxWidth / width) if (height > maxHeight) scale = Math.min(scale, maxHeight / height) width = width * scale height = height * scale const canvas = document.createElement('canvas') canvas.width = width canvas.height = height const ctx = canvas.getContext('2d') ctx.drawImage( img, 0, 0, width, height ) canvas.toBlob( blob => { if (!blob) { reject(new Error('压缩失败')) return } resolve(blob) }, 'image/webp', quality ) } img.onerror = reject img.src = url }) }最大像素:2048*2048 ≈ 400万像素,内存固定上限≈16MB
使用:
const blob = await compressImage(file) const compressedFile = new File( [blob], 'compressed.webp', { type: 'image/webp' } )十八、toBlob vs toDataURL
这是面试高频。
toDataURL
const url = canvas.toDataURL( 'image/jpeg', 0.8 )得到:
data:image/jpeg;base64,...缺点:
Canvas ↓ Base64 ↓ 字符串 ↓ 内存占用较大所以实际上传文件一般不推荐。
toBlob
canvas.toBlob(blob => { console.log(blob) }, 'image/jpeg', 0.8)得到:
Blob然后:
const file = new File( [blob], 'image.jpg', { type: 'image/jpeg' } )实际开发:
上传图片优先
toBlob。
MDN 也明确提供了toBlob()用于创建 Canvas 内容对应的 Blob。(GitHub)
十九、公司实际场景 ②:图片裁剪
例如头像上传。
用户选择图片 ↓ Canvas 显示图片 ↓ 用户拖动裁剪框 ↓ 计算裁剪区域 ↓ drawImage ↓ 生成 Blob ↓ 上传核心:
ctx.drawImage( img, cropX, cropY, cropWidth, cropHeight, 0, 0, outputWidth, outputHeight )这就是:
Canvas 图片裁剪器
很多头像裁剪、证件照裁剪、商品图裁剪,本质都是这个。
二十、公司实际场景 ③:水印
例如:
用户上传图片 ↓ Canvas ↓ drawImage ↓ fillText("xxx公司") ↓ toBlob ↓ 上传代码:
ctx.drawImage( img, 0, 0, width, height ) ctx.font = '30px Arial' ctx.fillStyle = 'rgba(255,255,255,0.5)' ctx.fillText( 'Company', 30, height - 30 )二十一、公司实际场景 ④:图片拼接
例如把:
图片 A 图片 B 图片 C拼成:
┌─────────┐ │ 图片 A │ ├─────────┤ │ 图片 B │ ├─────────┤ │ 图片 C │ └─────────┘Canvas:
ctx.drawImage(img1, 0, 0) ctx.drawImage(img2, 0, height1) ctx.drawImage(img3, 0, height1 + height2)最后:
canvas.toBlob(...)可以生成一张新图片。
二十二、公司实际场景 ⑤:截图
这个很常见,但要注意:
Canvas 本身不能直接把任意 DOM “截图”。
通常是:
DOM ↓ html2canvas 等库 ↓ Canvas ↓ Blob / Image或者:
浏览器 Screenshot API所以面试不要说:
Canvas 可以直接截网页。
更准确:
Canvas 可以作为截图结果的绘制载体,DOM 截图通常需要额外的 DOM → Canvas 方案。
二十三、公司实际场景 ⑥:验证码
例如:
┌─────────────┐ │ A 7 K 2 │ │ \ /\/\/ │ │ 噪声 │ └─────────────┘Canvas:
ctx.font = '30px Arial' ctx.fillText('A7K2', 20, 40)然后:
ctx.beginPath() ctx.moveTo(...) ctx.lineTo(...) ctx.stroke()加入:
随机颜色
随机字体
随机旋转
干扰线
噪点
二十四、公司实际场景 ⑦:签名板
这个非常典型。
比如:
用户在手机上签名 ┌───────────────────────┐ │ │ │ 手写签名 │ │ │ └───────────────────────┘核心:
canvas.addEventListener( 'pointerdown', e => { ctx.beginPath() ctx.moveTo(e.offsetX, e.offsetY) } ) canvas.addEventListener( 'pointermove', e => { ctx.lineTo(e.offsetX, e.offsetY) ctx.stroke() } )实际开发建议使用:
PointerEvent而不是分别处理:
mousedown mousemove touchstart touchmove因为 Pointer Events 可以统一鼠标、触摸、笔。
二十五、公司实际场景 ⑧:画板
类似:
Photoshop 白板 在线涂鸦 流程图 电子签名基本结构:
Canvas ↓ PointerDown ↓ 开始路径 PointerMove ↓ 不断 lineTo PointerUp ↓ 结束例如:
let drawing = false canvas.addEventListener('pointerdown', e => { drawing = true ctx.beginPath() ctx.moveTo(e.offsetX, e.offsetY) }) canvas.addEventListener('pointermove', e => { if (!drawing) return ctx.lineTo(e.offsetX, e.offsetY) ctx.stroke() }) canvas.addEventListener('pointerup', () => { drawing = false })二十六、公司实际场景 ⑨:图表
例如:
销量 │ │ ● │ ● │ ● │ ● └────────────────可以自己使用 Canvas 实现:
折线图
柱状图
饼图
雷达图
热力图
不过公司实际开发通常:
业务图表 ↓ ECharts / AntV 等 ↓ 底层可能使用 Canvas/SVG所以你不一定需要手写完整图表库。
但是理解 Canvas 有助于理解:
为什么 ECharts Canvas 模式能够渲染大量数据。
二十七、公司实际场景 ⑩:动画
Canvas 非常适合动画。
核心:
requestAnimationFrame()例如小球:
let x = 0 function render() { ctx.clearRect( 0, 0, canvas.width, canvas.height ) ctx.beginPath() ctx.arc( x, 100, 20, 0, Math.PI * 2 ) ctx.fill() x += 2 requestAnimationFrame(render) } render()动画流程:
requestAnimationFrame ↓ 清空 Canvas ↓ 更新状态 ↓ 重新绘制 ↓ requestAnimationFrame ↓ ...二十八、为什么动画不用 setInterval?
不推荐:
setInterval(() => { render() }, 16)推荐:
requestAnimationFrame(render)因为它更适合浏览器渲染节奏。
基本思想:
JS 更新 ↓ 浏览器下一帧 ↓ 绘制二十九、Canvas 动画性能
这是公司面试非常喜欢问的。
假设:
10000 个粒子每一帧都:
clearRect() for (...) { draw() }主线程压力可能很大。
优化方向:
1. 减少绘制数量
10000 → 30002. 减少状态切换
不要:
fillStyle = 'red' draw() fillStyle = 'blue' draw() fillStyle = 'red' draw()尽量:
fillStyle = 'red' for (...) { draw() }三十、批量绘制
例如:
ctx.beginPath() for (const point of points) { ctx.moveTo(point.x, point.y) ctx.arc( point.x, point.y, 2, 0, Math.PI * 2 ) } ctx.fill()相比大量:
beginPath() fill() beginPath() fill()通常更合理。
三十一、Canvas 高 DPI / 高清屏
这是实际开发非常重要的一块。
例如:
普通屏幕 devicePixelRatio = 1 Retina devicePixelRatio = 2如果直接:
<canvas width="500" height="300">可能出现:
文字模糊 线条模糊 图片模糊常见解决方案:
const dpr = window.devicePixelRatio || 1 const rect = canvas.getBoundingClientRect() canvas.width = rect.width * dpr canvas.height = rect.height * dpr ctx.scale(dpr, dpr)这样:
CSS: 500 × 300 实际 Canvas: 1000 × 600Retina 屏幕就能得到更好的清晰度。
三十二、但是这里有一个坑
如果你:
canvas.width = 1000 canvas.height = 600然后:
canvas { width: 500px; height: 300px; }那么你必须明确:
逻辑坐标 500 × 300 实际像素 1000 × 600所以项目里通常要把:
CSS 尺寸 逻辑尺寸 Canvas bitmap 尺寸区分开。
三十三、Canvas 和 DOM 事件
Canvas 本身是一个元素:
<canvas></canvas>所以可以监听:
canvas.addEventListener('click', ...)但是 Canvas 内部:
你画的矩形 你画的圆 你画的文字都不是 DOM。
所以:
document.querySelector('.circle')找不到你画的圆。
三十四、Canvas 如何实现点击某个图形?
需要自己做:
鼠标坐标 ↓ 判断 ↓ 是否落在图形区域例如圆:
const dx = mouseX - circle.x const dy = mouseY - circle.y const distance = Math.sqrt( dx * dx + dy * dy ) if (distance <= circle.radius) { console.log('点击了圆') }这叫:
Hit Testing / 命中检测
Canvas 编辑器、白板、图形编辑器都会大量使用。
三十五、矩形命中检测
function isPointInRect( x, y, rect ) { return ( x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height ) }三十六、一个非常实用的技巧:isPointInPath
Canvas 自己提供了路径命中检测:
const path = new Path2D() path.rect(100, 100, 200, 100) ctx.fill(path) canvas.addEventListener('click', e => { const rect = canvas.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top if (ctx.isPointInPath(path, x, y)) { console.log('点击到了') } })这个在 Canvas 图形编辑器里很好用。
三十七、Canvas 图片跨域问题
这是面试必问坑点之一。
假设:
const img = new Image() img.src = 'https://xxx.com/a.jpg' img.onload = () => { ctx.drawImage(img, 0, 0) canvas.toBlob(...) }可能报:
SecurityError原因:
跨域图片没有正确 CORS 授权,Canvas 被污染(tainted)。
一旦 Canvas 被污染:
canvas.toBlob() canvas.toDataURL() ctx.getImageData()都可能抛出SecurityError。(MDN Web Docs)
三十八、怎么解决?
前端:
const img = new Image() img.crossOrigin = 'anonymous' img.src = url但是光前端这样写不够。
服务端还需要:
Access-Control-Allow-Origin: https://your-site.com或者合适的允许来源配置。
所以:
Canvas 跨域图片问题是“前端 + 服务端 CORS”共同解决的。
MDN 也明确说明了crossorigin属性需要和服务端 CORS 响应头配合。(MDN Web Docs)
三十九、Canvas 清空的几个方式
clearRect
ctx.clearRect( 0, 0, canvas.width, canvas.height )最常用。
重置 width
canvas.width = canvas.width也能清空。
但是会:
重置 Canvas 的绘图状态。
所以一般不推荐作为普通清屏手段。
四十、Canvas 撤销 / 重做怎么实现?
Canvas 不自带:
undo redo公司项目一般自己做。
方案一:
保存图片:
const history = [] history.push( canvas.toDataURL() )撤销:
history.pop() ↓ 重新 drawImage但是:
数据量大时非常占内存。
更好的方式:
保存操作:
[ { type: 'line', points: [...] }, { type: 'circle', x: 100, y: 100 } ]然后:
state ↓ 重新 render ↓ Canvas这其实就是:
Canvas + 状态模型
更适合复杂编辑器。
四十一、Canvas 和 Web Worker
如果 Canvas 任务很重:
大量粒子 复杂图片处理 大图处理 复杂绘制可以考虑:
主线程 ↓ Worker ↓ OffscreenCanvasOffscreenCanvas可以让 Canvas 渲染脱离 DOM,并支持在 Worker 中进行渲染,从而把一部分重任务从主线程移走。(MDN Web Docs)
基本:
const offscreen = canvas.transferControlToOffscreen() const worker = new Worker('./worker.js') worker.postMessage( { canvas: offscreen }, [offscreen] )Worker:
self.onmessage = e => { const canvas = e.data.canvas const ctx = canvas.getContext('2d') // 绘制 }这个属于:
Canvas 性能进阶。
不是普通业务一开始就需要。
四十二、Canvas 性能优化总结
公司实际开发可以记这一套:
Canvas 性能 │ ├── 减少绘制次数 │ ├── 减少状态切换 │ ├── 批量绘制 │ ├── requestAnimationFrame │ ├── 避免频繁创建对象 │ ├── 控制 Canvas 分辨率 │ ├── 大量计算放 Worker │ ├── OffscreenCanvas │ └── 必要时使用 WebGL四十三、Canvas 和 SVG 怎么选?
这个也是面试高频。
| Canvas | SVG | |
|---|---|---|
| 本质 | 位图 | 矢量 DOM |
| 元素数量 | 多时更适合 | 太多可能压力大 |
| 单个元素操作 | 不方便 | 很方便 |
| 事件 | 自己做命中检测 | DOM 事件 |
| 放大 | 可能失真 | 不失真 |
| 图片处理 | 很强 | 一般 |
| 游戏 | 很适合 | 一般 |
| 图片编辑 | 很适合 | 一般 |
| 图标 | 一般 | 很适合 |
| 流程图 | 可以 | 很适合 |
| 数据可视化 | 很适合 | 也很适合 |
简单记:
大量像素 大量绘制 动画 图片处理 游戏 ↓ Canvas大量独立图形 需要点击单个元素 需要 DOM 结构 需要无损缩放 ↓ SVG四十四、Canvas 和 WebGL 怎么选?
进一步:
Canvas 2D ↓ CPU / 浏览器 2D 绘制模型 WebGL ↓ GPU ↓ 大量图形 / 3D / 高性能例如:
普通图片处理 签名 海报 验证码 简单动画 → Canvas 2D3D 大量粒子 复杂游戏 地图 GPU 大规模计算 → WebGL / WebGPU四十五、公司项目最常见的 Canvas 场景
你可以直接记这张表:
| 场景 | Canvas 使用 |
|---|---|
| 图片压缩 | ⭐⭐⭐⭐⭐ |
| 图片裁剪 | ⭐⭐⭐⭐⭐ |
| 图片格式转换 | ⭐⭐⭐⭐⭐ |
| 图片水印 | ⭐⭐⭐⭐⭐ |
| 图片拼接 | ⭐⭐⭐⭐ |
| 签名板 | ⭐⭐⭐⭐⭐ |
| 画板 | ⭐⭐⭐⭐⭐ |
| 验证码 | ⭐⭐⭐⭐ |
| 海报生成 | ⭐⭐⭐⭐⭐ |
| 截图 | ⭐⭐⭐⭐ |
| Canvas 图表 | ⭐⭐⭐⭐ |
| 粒子动画 | ⭐⭐⭐⭐⭐ |
| 游戏 | ⭐⭐⭐⭐⭐ |
| 视频帧处理 | ⭐⭐⭐⭐ |
| PDF/文档预览中的图形处理 | ⭐⭐⭐ |
| OCR 前图片预处理 | ⭐⭐⭐⭐ |
| 大数据可视化 | ⭐⭐⭐⭐ |
四十六、如果你是做 Vue / React
实际项目不要把 Canvas 所有逻辑直接塞组件里。
不推荐:
function App() { // 500 行 Canvas 代码 }建议:
components/ CanvasEditor.jsx hooks/ useCanvas.js utils/ canvas/ image.js draw.js coordinate.js hitTest.js export.js例如:
function useCanvas() { const canvasRef = useRef(null) function draw() { const canvas = canvasRef.current const ctx = canvas.getContext('2d') // ... } return { canvasRef, draw } }Vue:
const canvasRef = ref(null) onMounted(() => { const canvas = canvasRef.value const ctx = canvas.getContext('2d') })四十七、公司实际开发推荐架构
如果做一个 Canvas 编辑器:
React/Vue │ ↓ CanvasEditor │ ┌─────────┴─────────┐ ↓ ↓ State Canvas │ │ ↓ ↓ shapes / images render() │ │ └─────────┬─────────┘ ↓ Interaction │ ┌───────────┼───────────┐ ↓ ↓ ↓ Pointer HitTest Keyboard核心思想:
不要把 Canvas 当成状态本身。
而应该:
业务状态 ↓ render ↓ Canvas例如:
const shapes = [ { type: 'rect', x: 100, y: 100, width: 200, height: 100 }, { type: 'circle', x: 400, y: 200, radius: 50 } ]然后:
function render(ctx, shapes) { ctx.clearRect( 0, 0, canvas.width, canvas.height ) for (const shape of shapes) { drawShape(ctx, shape) } }这个思路非常重要。
四十八、一个真正比较像公司的 Canvas 项目
建议你练:
「图片编辑器」
功能:
┌─────────────────────────────┐ │ 图片编辑器 │ ├─────────────────────────────┤ │ │ │ Canvas │ │ │ │ ┌──────────┐ │ │ │ 图片 │ │ │ └──────────┘ │ │ │ ├─────────────────────────────┤ │ 上传 │ 裁剪 │ 旋转 │ 水印 │ │ 压缩 │ 撤销 │ 重做 │ 导出 │ └─────────────────────────────┘你会一次性学会:
Canvas 基础 ↓ drawImage ↓ 图片缩放 ↓ 图片裁剪 ↓ 旋转 ↓ 坐标转换 ↓ 鼠标/触摸事件 ↓ 命中检测 ↓ 撤销/重做 ↓ toBlob ↓ 文件上传 ↓ 高 DPI ↓ 性能优化这比单纯练:
画圆 画矩形 画线有价值得多。
四十九、Canvas 面试重点
如果你准备前端面试,我建议重点掌握这15 个问题:
基础
Canvas 和 SVG 区别?
getContext('2d')是什么?Canvas width/height 和 CSS width/height 有什么区别?
fillRect、strokeRect、clearRect?beginPath()有什么作用?save()/restore()?translate/rotate/scale?
图片
drawImage()三种参数形式?如何用 Canvas 压缩图片?
toBlob()和toDataURL()区别?Canvas 图片跨域为什么会污染?
Canvas 如何做图片裁剪?
性能
Canvas 如何实现动画?
为什么用
requestAnimationFrame?Canvas 卡顿怎么优化?
再往上:
Canvas ↓ OffscreenCanvas ↓ Web Worker ↓ WebGL ↓ WebGPU属于进阶路线。
五十、你可以按这个顺序学
如果你的目标是公司实际开发 + 面试,我建议不要平均用力:
第一阶段:基础 │ ├── Canvas 创建 ├── 坐标系 ├── 矩形 ├── Path ├── 圆 ├── 文字 ├── 渐变 └── save / restore ↓ 第二阶段:图片 │ ├── Image ├── drawImage ├── 图片缩放 ├── 图片裁剪 ├── toBlob ├── toDataURL ├── 图片压缩 ├── WebP └── 水印 ↓ 第三阶段:交互 │ ├── PointerEvent ├── 坐标转换 ├── 拖动 ├── 缩放 ├── 旋转 ├── HitTest └── isPointInPath ↓ 第四阶段:动画 │ ├── requestAnimationFrame ├── 动画状态 ├── 粒子 └── 性能优化 ↓ 第五阶段:工程化 │ ├── React/Vue ├── 高 DPI ├── 撤销/重做 ├── 状态管理 ├── Worker └── OffscreenCanvas其中对普通前端岗位来说,最值得你重点掌握的是:
drawImage+ 图片压缩/裁剪 +toBlob+ PointerEvent + 坐标转换 + 高 DPI + RAF + 跨域 + Canvas 性能。
尤其是你之前做的前端图片预处理 → 压缩 → WebP → OSS → AI这条链路,Canvas 正好是其中非常核心的一环。实际生产里要特别注意超大图片导致的内存/主线程阻塞、CORS、EXIF 方向、输出质量和文件大小这些问题;其中跨域图片如果没有 CORS 授权,会直接影响toBlob()、toDataURL()等读取操作。(MDN Web Docs)
如果按“真正能拿去面试”的方式继续,我建议下一步直接做一套《Canvas 10 个公司实战案例》:图片压缩 → 裁剪器 → 图片旋转 → 水印 → 签名板 → 橡皮擦 → 撤销/重做 → 海报生成器 → Canvas 粒子动画 → React/Vue Canvas 编辑器,每个案例都带完整代码和面试考点。