老板甩过来一个需求:要在微信小程序里加一个手动签字功能。听起来不就是画布上手写吗?真正落地的时候才发现,坑是一个接一个——真机画线发虚、手指拖动页面跟着滚、签名区域上下留白一大片、生成的图片上传后端还报错。这篇文章我把整个实现过程从头到尾拆开,包含完整的WXML/WXSS/JS代码、canvas 2D接口的踩坑记录、签名图片裁剪与上传方案,以及我在实际项目中遇到的高频问题和排查思路,直接照着抄就行。
1. 功能设计思路与方案选型
1.1 为什么要用canvas而不是view加背景图
手动签字的核心是捕获用户手指在屏幕上的轨迹,并且把轨迹渲染出来。很多第一次做这个功能的人会上来就写一堆view,每个touchmove事件里往页面里塞一个小圆点,用绝对定位控制位置。这个方案在小规模测试时看着没问题,但一旦签名时间稍长、轨迹点变多,页面节点会膨胀得非常厉害,iPhone 6这类老设备直接卡成PPT。而且view节点没法直接导出成图片,最后还是要借助canvas把整个区域重新画一遍,等于做了两遍工作。
正确做法是直接用canvas承载整个签名过程,手指滑动时实时绘制笔迹,松手后把canvas内容导出成图片。canvas是位图渲染,绘制性能远高于操作DOM节点,而且导出图片是天生的能力,一步到位。微信小程序从基础库2.9.0开始推荐使用Canvas 2D接口(也就是type="2d"),相比旧版的wx.createCanvasContext,新接口更贴近Web标准,性能更好,而且官方的维护重心已经完全转移到新接口上。我的建议是直接上新接口,虽然初期要多写几行初始化代码,但后面真机适配时能少踩很多坑。
1.2 整体功能拆解与流程规划
一个完整的签名功能,拆开来看其实就四个环节:签名画布展示、手写轨迹捕获与绘制、签名图片生成、图片上传与回显。业务上还要考虑用户签错了怎么重写、签名区域怎么校验是否为空、以及后续是否需要把签名叠加到某个合同模板上。
以下是我在实际项目中采用的功能清单:
| 功能点 | 实现方式 | 说明 |
|---|---|---|
| 手写轨迹捕获 | canvas触摸事件 | touchstart记录起点,touchmove持续绘制,touchend结束 |
| 笔迹渲染 | canvas 2D的stroke接口 | 设置线宽、线帽、颜色,实时绘制 |
| 清空重签 | 清空画布方法 | 保存初始状态,调用clearRect重置 |
| 签名图片导出 | canvasToTempFilePath | 生成临时文件路径,可指定导出尺寸 |
| 空白区域裁剪 | 像素扫描getImageData | 扫描非空白像素边界,裁剪四周留白 |
| 图片上传 | wx.uploadFile | 把临时文件提交到后端,兼容本地调试 |
| 空签名校验 | 记录hasDrawFlag | 通过是否真正绘制过笔迹来判断,不用扫描像素 |
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 WXML结构和WXSS布局的注意点
先看WXML结构。canvas组件本身是原生组件,层级最高,普通view盖不住它,所以在设计弹窗、按钮布局时要注意z-index没用,要么让按钮和canvas错开位置,要么用cover-view覆盖。我这里把底部按钮区放在canvas区域外面,既避开了原生组件层级问题,布局上也更自然。
html复制<!-- 签名弹窗区域 -->
<view class="sign-modal" wx:if="{{showSignModal}}">
<view class="sign-content">
<view class="sign-header">
<text class="sign-title">请签名</text>
<text class="sign-subtitle">请使用正楷书写</text>
</view>
<view class="sign-canvas-wrap">
<canvas
type="2d"
id="signCanvas"
class="sign-canvas"
disable-scroll="{{true}}"
bindtouchstart="onTouchStart"
bindtouchmove="onTouchMove"
bindtouchend="onTouchEnd"
></canvas>
</view>
<view class="sign-footer">
<button class="sign-btn reset-btn" bindtap="onResetSign">重签</button>
<button class="sign-btn confirm-btn" bindtap="onConfirmSign">确认</button>
</view>
</view>
</view>
注意canvas的id必须要给,后面初始化Canvas 2D实例时要靠这个id去查节点。disable-scroll这个属性很关键,告诉小程序在canvas区域内触摸时不要滚动页面,防止用户签个字页面跟着上下晃。
WXSS方面重点说尺寸问题。canvas的width和height样式要显式设置,不设置的话默认是300x150,而且开发者工具和真机的默认值还不一致。我习惯把签名区域做成固定高度,宽度铺满容器,配合rpx做响应式。
css复制.sign-canvas-wrap {
width: 100%;
height: 400rpx;
background: #fff;
border: 2rpx dashed #dcdfe6;
border-radius: 12rpx;
box-sizing: border-box;
}
.sign-canvas {
width: 100%;
height: 400rpx;
display: block;
}
有个很容易被忽视的坑:canvas样式上要加display: block。因为canvas默认是inline元素,底部会多出几像素的间隙,虽然不影响绘制本身,但会让canvas和容器边框之间出现一条细缝,在视觉上显得不严谨。
容器加个background: #fff也是有讲究的。如果容器背景是透明的,导出图片时透明区域在部分安卓机型上会变成黑色。白底一了百了,导出什么就是什么。
2.2 Canvas 2D初始化与坐标换算原理
Canvas 2D的初始化和旧版接口有本质区别。旧版直接wx.createCanvasContext('canvasId')就能用,新版必须先获取canvas节点,再调用canvas.getContext('2d')拿到绘图上下文。而且新版canvas的坐标系默认是CSS像素,但内部绘图精度是物理像素,所以需要在初始化时设置canvas的width和height为显示尺寸乘以设备像素比(dpr),否则画出来的线条会发虚。
javascript复制initCanvas() {
const query = this.createSelectorQuery();
query.select('#signCanvas')
.fields({ node: true, size: true })
.exec((res) => {
if (!res || !res[0] || !res[0].node) {
console.error('canvas节点获取失败');
return;
}
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
const dpr = wx.getSystemInfoSync().pixelRatio;
// 关键:canvas内部尺寸乘以dpr,保证清晰度
canvas.width = res[0].width * dpr;
canvas.height = res[0].height * dpr;
ctx.scale(dpr, dpr);
// 设置笔迹样式
ctx.lineWidth = 3;
