导读:本期聚焦于Robin创作的《React中如何用Canvas实现电子签名板并保存为PNG图片?》,敬请观看详情。电子签名在合同签署、审批确认等场景中越来越常见,前端实现一个流畅的手写签名板是关键一步。本文围绕React环境,讲解如何利用HTML5 Canvas的鼠标事件与触摸事件采集书写轨迹,通过贝塞尔曲线平滑笔迹,支持设置笔触颜色与粗细,处理移动端自适应与高分辨率屏幕下的模糊问题,最后借助toDataURL方法将签名导出为透明背景的PNG图片并提交后端。文中还提供清空、撤销、空白检测等实用功能的完整代码示例,帮助你快速落地一个生产可用的签名组件。

在合同签署、表单确认、物流签收等业务场景中,电子签名已经成为标配功能。要在前端实现一个体验良好的签名板,核心方案就是HTML5的Canvas。本文将结合React,从事件监听、轨迹平滑、高分屏适配,到最终导出PNG图片,完整讲解一个可投入生产的签名组件应该如何编写。

React中如何用Canvas实现电子签名板并保存为PNG图片?

一、基础实现:监听鼠标与触摸事件绘制轨迹

签名板的核心逻辑其实并不复杂:监听用户在画布上的按下、移动、抬起三类事件,在移动过程中不断绘制线段,就形成了笔迹。在React中,我们通常在useEffect中获取canvas的2D上下文,并在useRef中保存引用,避免组件重渲染时丢失绘制状态。

需要注意的一点是,移动端浏览器支持触摸事件与Pointer事件两种方式。Pointer Events是更现代的方案,它统一了鼠标、触摸、触控笔三种输入,一套代码即可兼容所有设备。下面是一个基础版本的实现:

import { useEffect, useRef } from 'react';

export default function SignaturePad() {
  const canvasRef = useRef(null);
  const drawing = useRef(false);

  useEffect(() => {
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    ctx.lineWidth = 3;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.strokeStyle = '#000';

    const getPos = (e) => {
      const rect = canvas.getBoundingClientRect();
      return {
        x: e.clientX - rect.left,
        y: e.clientY - rect.top,
      };
    };

    const start = (e) => {
      drawing.current = true;
      const { x, y } = getPos(e);
      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.lineTo(x + 0.01, y); // 画一个点,处理单击落笔的情况
      ctx.stroke();
    };

    const move = (e) => {
      if (!drawing.current) return;
      const { x, y } = getPos(e);
      ctx.lineTo(x, y);
      ctx.stroke();
    };

    const end = () => {
      drawing.current = false;
    };

    canvas.addEventListener('pointerdown', start);
    canvas.addEventListener('pointermove', move);
    canvas.addEventListener('pointerup', end);
    canvas.addEventListener('pointerleave', end);

    return () => {
      canvas.removeEventListener('pointerdown', start);
      canvas.removeEventListener('pointermove', move);
      canvas.removeEventListener('pointerup', end);
      canvas.removeEventListener('pointerleave', end);
    };
  }, []);

  return <canvas ref={canvasRef} width={600} height={300} style={{ border: '1px solid #ccc' }} />;
}

这个版本已经可以正常书写,但直接用lineTo连点会出现明显的折线感,因为采集到的坐标点是离散的,快速书写时相邻两点距离很大,连线就显得生硬。另外还有一个隐患:手指在触摸设备上滑动会触发页面的滚动或缩放,需要在canvas上添加touch-action: none样式来阻止浏览器的默认手势行为,否则签名过程中页面会跟着乱跑。

二、笔迹平滑与高分辨率屏幕适配

用二次贝塞尔曲线平滑笔迹

改善笔迹观感的常用技巧是使用二次贝塞尔曲线。思路是:把相邻采集点的中点作为线段端点,采集点本身作为控制点,这样绘制出来的曲线过渡自然,非常接近真实笔迹。改造后的绘制逻辑如下:

const points = useRef([]);

const start = (e) => {
  drawing.current = true;
  points.current = [getPos(e)];
};

const move = (e) => {
  if (!drawing.current) return;
  const pts = points.current;
  pts.push(getPos(e));
  ctx.clearRect(0, 0, canvas.width, canvas.height); // 配合重绘时使用
  ctx.beginPath();
  ctx.moveTo(pts[0].x, pts[0].y);
  for (let i = 1; i < pts.length - 1; i++) {
    const midX = (pts[i].x + pts[i + 1].x) / 2;
    const midY = (pts[i].y + pts[i + 1].y) / 2;
    ctx.quadraticCurveTo(pts[i].x, pts[i].y, midX, midY);
  }
  ctx.stroke();
};

如果采用边画边累积的方式(即不clearRect,直接在旧笔迹上追加),则只需要对新进入的两个中点之间做一次quadraticCurveTo即可,性能更好,也是签名类库的主流做法。两种方式的选择取决于是否需要撤销功能:累积式绘制可以配合快照实现撤销,重绘式则更简单直接。

解决高分屏模糊问题

很多开发者发现签名在Retina屏幕上发虚,原因在于canvas的像素尺寸与CSS尺寸被当成了同一个东西。canvas的width和height属性定义的是绘图缓冲区的物理像素,而CSS样式控制的是显示尺寸。当设备像素比(devicePixelRatio)大于1时,如果两者相等,浏览器会把物理像素拉伸显示,自然就模糊了。解决办法是让缓冲区尺寸等于CSS尺寸乘以像素比,再对上下文做缩放:

useEffect(() => {
  const canvas = canvasRef.current;
  const dpr = window.devicePixelRatio || 1;
  const rect = canvas.getBoundingClientRect();
  canvas.width = rect.width * dpr;
  canvas.height = rect.height * dpr;
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr); // 之后所有坐标按CSS像素计算即可
}, []);

经过这样的处理,逻辑坐标仍然按照CSS像素书写,导出的图片却是高分辨率的,放大查看也不会出现锯齿。

三、导出PNG、撤销与空白检测

导出为PNG图片

Canvas原生提供了toDataURL方法,可以把画布内容序列化为Base64字符串。签名场景通常希望背景透明,只保留笔迹本身,这样后端可以把图片合成到合同的任意位置。只要不主动填充背景色,canvas默认就是透明的:

const handleSave = () => {
  const canvas = canvasRef.current;
  // 检查是否为空白画布
  if (isEmpty(canvas)) {
    alert('请先签名');
    return;
  }
  const dataUrl = canvas.toDataURL('image/png');
  // 方式一:直接提交给后端
  fetch('/api/signature', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ image: dataUrl }),
  });
  // 方式二:触发本地下载
  const a = document.createElement('a');
  a.href = dataUrl;
  a.download = 'signature.png';
  a.click();
};

需要注意的是,Base64编码会让数据体积膨胀约三分之一,如果签名图片较大,建议把dataUrl转成Blob后用FormData上传,可以减少传输体积。转换方法也很简单:atob解码后再借助Uint8Array构造Blob对象即可。

空白检测与撤销实现

空白检测有两种思路。一种是记录用户是否落过笔,用一个标志位判断,但这无法覆盖“点了一下又清空”的边界情况。更稳妥的方式是直接扫描像素数据:遍历getImageData返回的alpha通道,只要存在非零值就说明有笔迹。为了性能,可以按步长采样:

const isEmpty = (canvas) => {
  const ctx = canvas.getContext('2d');
  const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
  for (let i = 3; i < data.length; i += 4) {
    if (data[i] !== 0) return false; // alpha通道非零说明有内容
  }
  return true;
};

撤销功能建议采用快照方案:每次pointerup时调用ctx.getImageData保存当前画布的快照并压入数组,撤销时pop出上一张快照,用putImageData恢复。快照会占用内存,一般限制保存最近20步左右即可满足绝大多数用户的操作习惯。清空功能则直接clearRect整个画布并清空快照栈。

四、组件封装与产品化建议

把上述能力整合成一个完整的受控组件后,建议对外暴露这些Props:宽高、笔触颜色、笔触粗细、背景色、onChange回调(签名状态变化时通知父组件)。同时在内部处理好响应式尺寸变化——当容器宽度变化时,需要先把当前画布内容保存为图片,再重设尺寸,最后把图片绘制回去,避免窗口缩放导致签名丢失。

如果想进一步降低维护成本,社区中成熟的signature_pad库也是不错的选择,它内置了速度感应的笔锋模拟(书写越快线条越细,越慢越粗),手写观感更接近真实钢笔。但无论是否使用库,理解背后的Canvas原理都有助于你在遇到定制需求时游刃有余,比如在签名上叠加水印、限制签名区域形状、或者把多页签名合并导出等场景。

React Canvas签名板电子签名canvas手写签名保存PNG修改时间:2026-09-08 00:38:35

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260908/52524.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。