在微信小程序中使用canvas绘制分享海报,最让人头疼的往往不是布局,而是drawImage把网络图片画上去之后要么完全不显示,要么看起来像被蒙了一层雾。这两个问题背后其实分别对应着小程序运行环境的资源加载限制和渲染分辨率机制,只要摸清规则就能稳定解决。

网络图片不显示的根因与本地化方案
很多开发者第一次在微信小程序里写海报绘制逻辑时,会直接把后端返回的图片地址传给drawImage,结果画布上什么都没有。这并不是canvas本身有bug,而是小程序出于安全与性能考虑,禁止canvas直接读取未经过校验的网络资源。所有用于canvas绘制的网络图片,都必须先通过wx.getImageInfo接口下载并转换为本地临时路径,同时该图片域名还要在后台配置为合法downloadFile域名。
如果域名没有配置白名单,getImageInfo会直接失败,自然也就没有本地路径可供绘制。即便域名合规,若你在图片回调之前就执行了draw或者drawImage,同样会因为图片还未就绪而画出空白。因此正确的做法是在getImageInfo的success回调中再调用绘制方法,并且把多张网络图都加载完后再统一出图,避免时序错乱。
下面是一段典型的本地化后再绘制的代码,注意其中对特殊字符的转义以及路径的使用方式:
wx.getImageInfo({
src: 'https://ipipp.com/avatar.png',
success: function (res) {
const ctx = wx.createCanvasContext('poster');
// res.path 为本地临时文件路径,可直接用于 drawImage
ctx.drawImage(res.path, 0, 0, 300, 300);
ctx.draw();
},
fail: function (err) {
console.log('图片下载失败,请检查域名配置');
}
});
除了上述流程,还需要注意小程序基础库版本差异。较新的版本推荐使用Canvas 2d接口而非旧版的wx.createCanvasContext,但无论哪种方式,网络图片本地化这一步都不能省略。如果海报包含多张图,可以用Promise.all包裹多个getImageInfo调用,等所有图片就位后再绘制,这样能有效规避个别图片慢导致整体空白的问题。
图像模糊的物理像素与逻辑像素错位
解决了不显示,接下来就是模糊。小程序canvas默认按照逻辑像素(css像素)来设定尺寸,而手机屏幕通常是二倍或三倍的物理像素密度。如果你在wxml里写了一个宽高300px的<canvas>,它的背后实际像素可能只有300x300,但手机要用600x600甚至900x900的物理点去显示它,浏览器或小程序渲染层只能做插值放大,图片自然变虚。
正确的高清绘制思路是先获取设备像素比dpr,然后把canvas的width和height属性设为逻辑尺寸乘以dpr,同时通过css或style把canvas显示尺寸固定为逻辑尺寸,最后在绘图时用ctx.scale(dpr, dpr)把坐标系放大。这样你代码里写的300x300,实际落盘的是900x900像素,清晰度立刻提升。对于2d新接口,还需要在获取canvas节点后手动设置canvas.width与canvas.height。
以下示例展示了基于旧版api的dpr适配写法:
const dpr = wx.getSystemInfoSync().pixelRatio;
const ctx = wx.createCanvasContext('poster');
// 假设逻辑尺寸为 300x300
ctx.scale(dpr, dpr);
ctx.drawImage(localPath, 0, 0, 300, 300);
ctx.draw();
另外一个常被忽略的点是图片本身的分辨率。即使canvas适配了dpr,如果网络图片原始尺寸只有100x100,却被拉大到300x300逻辑像素(实际900x900物理像素),一样会糊。因此在设计海报时,应要求后端提供至少两倍于显示区域的图片,或者在前端对图片做合适的裁剪而非暴力拉伸。结合本地化加载与dpr缩放,基本可以彻底解决模糊。
综合实践与常见误区排查
把前面两点合并,一个健壮的海报绘制流程应该是:检查域名白名单,用getImageInfo把全部网络图本地化,拿到dpr并配置canvas真实像素尺寸,在回调中按缩放后的坐标系绘制,最后延迟一点时间再调用wx.canvasToTempFilePath导出。不少人导出图片时发现又是空白,其实是draw是异步的,还没渲染完就导出,应把导出写在draw的回调里或使用setTimeout留出帧时间。
还有一个误区是以为真机不显示但开发者工具显示,就说明代码没问题。实际上开发者工具有时会放宽域名校验,真机才暴露白名单缺失。此外,若使用2d接口,drawImage接受的是Image对象而非路径字符串,需要通过canvas.createImage创建并赋值src,等onload后再画,这和旧版路径方式不同,混用也会导致不显示。
下面给出一个2d接口下较完整的示意结构:
const query = wx.createSelectorQuery();
query.select('#poster')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
const dpr = wx.getSystemInfoSync().pixelRatio;
canvas.width = 300 * dpr;
canvas.height = 300 * dpr;
ctx.scale(dpr, dpr);
const img = canvas.createImage();
img.src = localPath;
img.onload = () => {
ctx.drawImage(img, 0, 0, 300, 300);
};
});
整体来看,小程序canvas画海报并不复杂,核心就是尊重它的资源加载规则和屏幕密度规则。把网络图变成本地路径、把绘制动作推迟到资源就绪、把画布像素按dpr放大,这三个动作做到位,不显示和模糊都会消失。剩下的只是版式与文字排布的细节打磨。