Canvas 绘制图像的核心方法是 drawImage,但如果时序控制不好,画布上最容易出现的结果就是一片空白。图片作为外部资源需要异步加载,而 Canvas 的绘图操作是同步执行的,两者配合一旦出现偏差,就会产生各种难以排查的问题。跨域限制、参数传错、缓存更新不及时也是实际项目中频繁遇到的坑。这篇文章围绕图片加载与绘制这条链路,把常见问题拆开说明,并给出可直接运行的代码示例。

图片加载完成后再绘制,别让画布空白
Image 对象在设置 src 属性后会立即开始异步加载,但 drawImage 并不会等待图片解码完成。很多新手习惯创建完 Image 后马上调用绘图方法,结果只能在画布上看到一片透明区域。正确的做法是把绘图逻辑放进 onload 回调,或者使用 load 事件监听器,确保图片像素数据已经可用。
下面这段代码展示了最稳妥的顺序。先绑定 onload,最后才设置 src,这样可以避免极端情况下图片从缓存命中导致 load 事件在绑定前触发。虽然现代浏览器对 Image 加载事件的处理已经比较健壮,但保持这个顺序仍然是一个好习惯。
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0);
};
img.onerror = function() {
console.error('图片加载失败,请检查地址是否正确');
};
img.src = 'photo.jpg';
除了 onload,还可以用 addEventListener('load', handler) 绑定多个回调,方便在复杂组件中解耦。若绘制的是用户上传的本地图片,同样需要等待 load 事件,而不是直接读取文件后就开始绘图。加载失败时最好补充 onerror 处理,避免程序静默失败。
drawImage 参数要分清源区域与目标区域
drawImage 有三种重载形式:两参数版本按图片原始尺寸绘制;四参数版本将整个图片缩放绘制到目标矩形;九参数版本则支持从源图片裁切一部分,再缩放绘制到画布指定位置。九参数中的前四个值 sx、sy、sw、sh 表示源图片上的裁切区域,后四个值 dx、dy、dw、dh 表示画布上的目标区域。实际开发里,这两个区域经常被搞反。
比如想从一张大图左上角切出 100x100 的区域,并缩放到 200x200 绘制,代码应该这样写:
const img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0, 100, 100, 50, 50, 200, 200);
};
img.src = 'large-photo.jpg';
这里源区域从 (0, 0) 开始,宽高为 100;目标区域从画布 (50, 50) 开始,宽高为 200。若源区域超过了图片的实际宽度或高度,部分浏览器会直接抛异常,有些则会静默绘制失败。因此裁切前最好通过 naturalWidth 和 naturalHeight 确认图片真实尺寸。目标宽高为 0 时不会绘制任何内容,缩放场景下还需要处理好宽高比,避免图片变形。
跨域图片与画布污染问题
如果你尝试把一张跨域图片画到 Canvas 上,再调用 toDataURL 导出数据,很可能会遇到 SecurityError。原因在于浏览器必须保护像素数据:一旦 Canvas 中的像素来自未授权跨域资源,攻击者就可能通过读取像素推断出图片内容。此时画布会被标记为污染状态,任何读取像素的操作都会被拒绝。
解决跨域绘制需要给 Image 对象设置 crossOrigin 属性,并且图片服务器必须返回允许跨域的响应头。设置方式如下:
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function() {
ctx.drawImage(img, 0, 0);
// 如果服务器未返回 Access-Control-Allow-Origin,这里仍会抛错
const dataUrl = canvas.toDataURL('image/png');
};
img.src = 'https://cdn.ipipp.com/photo.jpg';
需要特别注意的是,如果同一张图片之前已经在没有 crossOrigin 的情况下加载过,浏览器可能会复用缓存,导致带 crossOrigin 的请求仍然失败。常见的处理办法是在 URL 后追加一个随机参数来破坏缓存,例如 photo.jpg?_=' + Date.now()。另外,服务器返回的 Access-Control-Allow-Origin 必须与当前页面源匹配,不能使用通配符同时携带凭证。
CDN 和对象存储一般都可以配置 CORS 规则。画布被污染后,除非重新用合规图片绘制,否则无法恢复干净状态。因此最好在所有需要导出或像素操作的图片加载前统一设置 crossOrigin。
本地文件、SVG 和 Base64 图片的绘制
从 <input type='file'> 读取本地图片时,不能直接把 File 对象传给 drawImage。正确流程是先用 FileReader 把文件读取成 Data URL,再创建 Image 加载这个 Data URL,最后在 load 事件里绘制。这样做的原因是 drawImage 只接受图片元素、SVG 元素、视频帧或另一块画布等可绘制对象。
SVG 图片的绘制稍有不同。SVG 如果没有显式设置 width 和 height,某些浏览器可能无法正确计算其固有尺寸,导致画布上什么都不显示。建议在生成 SVG 字符串时明确写出宽高属性。Base64 图片则可以直接赋给 Image 的 src,与普通 URL 使用方式一致。
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(evt) {
const img = new Image();
img.onload = function() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
};
img.src = evt.target.result;
};
reader.readAsDataURL(file);
});
如果希望用户连续上传同一张图片,需要给 FileReader 或 Image 重新创建实例,部分浏览器会对同一个 File 对象缓存读取结果,导致第二次 change 事件无法触发完整流程。对缓存问题保持敏感,可以少走很多弯路。
几个容易忽略的补充点
图片解码会消耗内存和 CPU,频繁绘制同一张图时可以考虑把图片先绘制到一个离屏 Canvas 中,后续只复制这个离屏画布。尤其在做动画或游戏时,减少每帧对原始图片的解码操作能明显提升性能。
另外,在移动端 Safari 或低版本 Chrome 中,超大尺寸图片可能出现绘制空白或者崩溃。常见做法是先判断 naturalWidth 和 naturalHeight,如果超过 4096 或 8192 像素,就通过离屏 Canvas 进行降采样。还有一点,Canvas 的 drawImage 不会自动裁剪圆角,需要配合 clip() 或后续合成操作来实现。
最后,如果页面使用了 will-change 或硬件加速导致 Canvas 截图异常,可以尝试在绘制完成后调用一次 ctx.getImageData 来强制刷新,但要注意这会将画布置为可读状态,对性能有一定影响。
HTML Canvas图片加载drawImage修改时间:2026-10-02 17:31:01