微信小程序的地图组件提供了基础的 marker 标注能力,但默认只支持通过 iconPath 设置一张静态图片。如果标注点需要展示动态统计数字、渐变背景、状态颜色或复杂组合图形,用静态图片就意味着要提前生成大量图片资源,后期修改成本高,还无法根据实时数据灵活变化。利用 Canvas 绘制复杂标注,再把画布导出为临时图片交给 marker 使用,是解决这一问题的常用方案。本文将拆解从创建离屏 Canvas、绘制图形到更新地图标注的完整流程,并给出可以直接运行的代码示例。

为什么用 Canvas 生成标注点更灵活
在微信小程序的 <map> 组件中,marker 标注点依靠 iconPath 加载图片。这个路径可以是网络图片、本地临时文件或项目内的静态资源。通常做法是让 UI 设计出几种不同状态的标注图,前端根据数据切换图片路径。但当地图业务复杂起来,比如标注需要显示数量、温度、设备状态、告警等级等信息时,图片资源数量会成倍增长,而且任何一个视觉调整都要重新出图。
Canvas 绘制方案把标注从静态资源变成动态生成。开发者可以在运行时根据数据绘制圆底、渐变、描边、文字、甚至小图标。绘制完成后调用 wx.canvasToTempFilePath 拿到临时图片路径,这个路径可以直接作为 marker 的 iconPath。由于 Canvas 是程序化绘制的,颜色、内容、尺寸都能灵活控制,也不需要维护庞大的图片资源目录。当实时数据更新时,重新绘制一次即可更新地图上的标注外观。
使用离屏 Canvas 的好处是不会影响页面本身的显示结构。通过 wx.createOffscreenCanvas 创建的 canvas 不挂载在 WXML 上,只存在于内存中,适合在工具函数或逻辑层中批量生成标注图片。其绘制接口与普通 Canvas 2D 基本一致,可以轻松完成复杂图形渲染。
离屏 Canvas 绘制复杂标注并导出
先明确整体流程:创建离屏 Canvas、设置物理像素尺寸、拿到 2D 上下文、执行绘制命令、导出临时文件、把临时文件地址写入 marker。这里的关键是物理像素尺寸必须考虑设备像素比(dpr),否则在高清屏上文字和图形会模糊。通常先获取系统的 pixelRatio,再将逻辑宽度和高度分别乘以 dpr 作为 canvas 的真实宽高,然后通过 ctx.scale(dpr, dpr) 把绘制坐标系还原为逻辑尺寸。
下面示例创建一个宽高均为 48 逻辑像素的标注,绘制一个蓝色渐变圆底、白色描边,并在中心写上数量文字。代码中使用了 wx.createOffscreenCanvas,该接口在基础库 2.16.1 及以上版本可用。如果是低版本,可以改用页面中隐藏的 <canvas> 元素配合 wx.canvasToTempFilePath 的旧版参数。
// 获取设备像素比
const dpr = wx.getSystemInfoSync().pixelRatio || 2;
const logicalSize = 48;
const canvas = wx.createOffscreenCanvas({
type: '2d',
width: logicalSize * dpr,
height: logicalSize * dpr
});
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
// 绘制渐变圆底
const gradient = ctx.createRadialGradient(24, 20, 5, 24, 20, 24);
gradient.addColorStop(0, '#6A8DFF');
gradient.addColorStop(1, '#3D5AFE');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(24, 20, 20, 0, Math.PI * 2);
ctx.fill();
// 绘制白色描边
ctx.lineWidth = 2;
ctx.strokeStyle = '#FFFFFF';
ctx.stroke();
// 绘制中心文字
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold 18px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('12', 24, 20);
// 导出临时图片
wx.canvasToTempFilePath({
canvas: canvas,
success: (res) => {
const tempFilePath = res.tempFilePath;
console.log(tempFilePath);
},
fail: (err) => {
console.error('导出标注图片失败', err);
}
});
导出拿到临时图片路径后,就可以把它写入 marker 的 iconPath 字段。marker 还需要设置 width 和 height,通常与 Canvas 的逻辑尺寸保持一致。例如上面的 48×48 标注,marker 就设置 width 为 48,height 为 48。如果标注图形带有底部尖角,anchor 可以设置为 y 接近 1,让底部尖角对准地图坐标点。
高清屏适配与多尺寸标注
高清屏适配是 Canvas 绘制标注时最容易忽视的问题。如果直接以 48×48 逻辑尺寸创建 canvas,不乘以 dpr,在 dpr 为 3 的设备上实际只有 144×144 物理像素用来显示 48×48 的标注。图片被放大后,文字边缘会出现锯齿,渐变过渡也会变得粗糙。因此推荐每次创建 canvas 时都先取 wx.getSystemInfoSync().pixelRatio,把宽高乘以该值,再用 ctx.scale(dpr, dpr) 保持绘制坐标为逻辑单位。这样文字和图形最终输出时足够清晰。
标注尺寸也不是一成不变的。如果标注里的数字从一位变成三位,固定 48×48 的底圆可能不够宽,文字会被压扁。此时可以根据文字内容动态计算需要的逻辑宽度。Canvas 2D 提供了 measureText 方法,可以先设置字体样式,再测量文字宽度,加上左右内边距得到合适的标注宽度。高度则可以保持固定,也可以按内容行数计算。绘制时把圆形改成圆角矩形或胶囊形,更符合多数地图标注的视觉风格。
对于不同状态的标注,例如正常、告警、离线,可以封装一个统一的绘制函数,传入状态、文字、尺寸等配置,函数内部根据状态切换渐变颜色或边框颜色。这样新增一种状态只需增加一个配置项,不需要重复写绘制逻辑。绘制完成后,同一套代码可以为多个 marker 生成不同外观的图标。
function drawMarkerBadge(options) {
const {
text = '',
bgColor = '#3D5AFE',
borderColor = '#FFFFFF',
width = 48,
height = 48
} = options;
const dpr = wx.getSystemInfoSync().pixelRatio || 2;
const canvas = wx.createOffscreenCanvas({
type: '2d',
width: width * dpr,
height: height * dpr
});
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
const cx = width / 2;
const cy = height / 2 - 4;
const radius = Math.min(width, height) / 2 - 4;
const gradient = ctx.createRadialGradient(cx - 4, cy - 4, 3, cx, cy, radius);
gradient.addColorStop(0, bgColor);
gradient.addColorStop(1, '#1E2F9E');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = borderColor;
ctx.stroke();
// 绘制尖角指示
ctx.fillStyle = bgColor;
ctx.beginPath();
ctx.moveTo(cx - 6, cy + radius - 2);
ctx.lineTo(cx + 6, cy + radius - 2);
ctx.lineTo(cx, cy + radius + 8);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold ' + Math.round(height * 0.38) + 'px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, cx, cy);
return canvas;
}
// 更新 marker 图标
function updateMarkerIcon(markerId, text) {
const canvas = drawMarkerBadge({ text, width: 56, height: 56 });
wx.canvasToTempFilePath({
canvas,
success: (res) => {
const markers = this.data.markers.map(item => {
if (item.id === markerId) {
return Object.assign({}, item, {
iconPath: res.tempFilePath,
width: 56,
height: 56
});
}
return item;
});
this.setData({ markers });
},
fail: (err) => {
console.error('更新标注失败', err);
}
});
}
上面封装了绘制函数 drawMarkerBadge,可以通过传入不同参数生成不同颜色和文字的标注。更新时调用 updateMarkerIcon 重新绘制并替换对应 marker 的 iconPath。需要注意的是,marker 的 width 和 height 应该与 Canvas 的逻辑尺寸保持一致,否则地图上显示的大小会和设计不一致。
性能优化与常见问题
多次频繁绘制标注会带来性能开销。如果地图上标注数量很多,并且数据实时刷新,每次都重新创建离屏 Canvas 并导出临时文件,可能会造成短暂的卡顿。优化方向有两个:一是对相同配置的标注做缓存,只绘制一次,后续复用同一个临时文件路径;二是将批量标注的绘制过程放在数据更新结束后统一执行,避免高频触发。另外,导出临时文件后应及时清理不再使用的文件,调用 wx.getFileSystemManager().unlink 即可释放存储空间。
另一个常见问题是临时文件路径失效。由于 wx.canvasToTempFilePath 生成的临时文件在小程序生命周期内有效,但如果用户长时间停留在页面,文件数量过多可能影响性能。因此建议在页面卸载时遍历已生成的临时文件并删除。对于从服务端获取的标注数据,可以先在本地缓存图片地址,数据不变时直接使用已有图片,减少重复绘制。
兼容性方面,wx.createOffscreenCanvas 需要基础库 2.16.1 及以上。低版本客户端可以使用页面中隐藏的 <canvas> 节点配合 wx.createSelectorQuery 获取上下文,再用旧版 wx.canvasToTempFilePath 导出。不过旧版接口的参数和用法略有不同,建议根据实际发布环境做降级处理。另外,Canvas 绘制的文字字体在不同系统上渲染效果可能有差异,最好在真机上验证。
总结
通过 Canvas 绘制微信小程序地图标注点,本质是把标注图片的生产过程从设计资源转移到运行时程序。这样既能灵活响应数据变化,又能减少静态图标的维护成本。实现时重点把握三个环节:创建离屏 Canvas 并设置正确的物理像素尺寸;封装绘制函数处理渐变、描边、文字和状态变化;导出临时图片并正确更新 marker 的 iconPath。在此基础上,结合缓存与文件清理策略,可以满足大多数个性化地图标注的需求。