移动端必须用 touchstart/touchmove 并在 touchmove 中调用 preventDefault(),否则 Safari 和 Android 浏览器会拦截事件导致签名中断;需结合 getBoundingClientRect() 坐标转换与 devicePixelRatio 适配高 DPI 屏幕,确保坐标精准、签名清晰。

移动端必须用 touchstart/touchmove,不能只靠 mousedown;否则 Safari 和 Android 浏览器会拦截事件来响应缩放或滚动,签名轨迹直接断开或无反应。
为什么 touchmove 必须加 event.preventDefault()
移动端浏览器默认把 touchmove 当作滚动/缩放手势,不阻止就会中断绘图路径。哪怕你只监听了 touchmove,只要没调用 preventDefault(),手指一滑画布就停笔。
- 只在
touchmove里加preventDefault()就够,touchstart和touchend不强制要求 - 但要注意:如果 canvas 外层有可滚动容器,
preventDefault()会让整个页面无法滚动,此时得用touch-action: noneCSS 配合(写在 canvas 样式上) - PC 端 fallback 时,
mousemove不需要preventDefault(),但要确保mousedown已触发并设置了isDrawing = true
getBoundingClientRect() 坐标转换为什么不能省
canvas 的 clientX/clientY 是相对于视口的,而 canvas 绘图坐标系是相对于 canvas 元素左上角的——两者偏移量不为零,尤其当 canvas 有 margin、padding 或被 flex/grid 容器约束时。
- 错误写法:
const x = e.touches[0].clientX(移动端)或e.clientX(PC),直接当画布坐标用 - 正确做法:先
const rect = canvas.getBoundingClientRect(),再算x = e.touches[0].clientX - rect.left(触控)或e.clientX - rect.left(鼠标) - 高 DPI 屏幕(如 Retina)下,还要乘以
window.devicePixelRatio才能对齐物理像素,否则签名模糊或偏移
ctx.lineCap = 'round' 和动态线宽怎么影响签名真实感
默认 lineCap: 'butt' 会让笔画两端硬切,像刀刻;真实手写有起笔轻、收笔淡、边缘微虚的特征,靠基础样式就能显著提升观感。
立即学习“前端免费学习笔记(深入)”;
-
ctx.lineCap = 'round'和ctx.lineJoin = 'round'必须同时设,否则拐角还是生硬直角 - 固定
ctx.lineWidth = 2显得机械;推荐按移动速度动态调整:ctx.lineWidth = Math.max(1, 3 - speed * 0.5)(speed 可用前后两点距离除以时间差估算) - 别每点都
beginPath()→moveTo()→lineTo()→stroke(),这样性能差且线条断续;应累积点数组,最后一次性stroke() - 抗锯齿建议关掉:
ctx.imageSmoothingEnabled = false(导出时防模糊),但可用ctx.shadowBlur = 0.5轻微柔化边缘,更接近墨迹晕染
toBlob() 比 toDataURL() 更适合生产环境
toDataURL() 返回的是 base64 字符串,长度动辄超 2MB,传给后端可能被 Nginx 截断,存 localStorage 也可能失败;toBlob() 直出二进制,体积小、可控性强。
- 优先用:
canvas.toBlob(callback, 'image/png', 0.8),第三个参数是压缩质量,0.8 是清晰度和体积的合理平衡点 - 上传时直接
formData.append('signature', blob, 'sig.png'),服务端收到的就是标准文件流 - 若必须用 base64,记得截掉前缀
data:image/png;base64,,只取后面部分;服务端解码前务必校验是否为合法 Base64 字符集,且长度是 4 的倍数 - 签名有效性不能只靠图片存在——前端应同步计算
crypto.subtle.digest('SHA-256', new Uint8Array(await blob.arrayBuffer()))并随请求发送哈希值,后端比对防篡改
高 DPI 适配和坐标对齐是上线前最容易漏的两个点,一模糊一偏移,用户第一眼就觉得“不像真签名”。别等 QA 提 bug 才补,初始化 canvas 时就该把 devicePixelRatio 和 getBoundingClientRect() 逻辑写全。



















