ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

H5横屏签名方案:jSignature实现与避坑指南

H5横屏签名方案:jSignature实现与避坑指南

简介:一套可直接运行的H5横屏手写签名完整源码,基于jSignature插件构建,面向需要快速在移动端集成手写签名功能的前端开发者。核心功能包括手机横屏适配、触控签名板交互、图片格式可选,以及将签名保存为本地图片,整个过程无需后端参与。资源打包为rar压缩包,共69个文件、约2.98MB;其中27个js文件承担jSignature插件、jQuery及页面交互逻辑,15个css用于横屏布局与视觉样式,3个html构成可直接打开的示例页面,另配多套字体资源(ttf、woff、woff2、svg、eot、otf)与图片、psd设计源文件,目录结构清晰,方便按需替换或二次开发。已有1073人学习下载,这套方案无论是作为H5签名功能起步模板,还是作为理解jSignature封装思路、横屏事件适配和图片导出细节的参考样例,都有实用价值,也可直接引用到真实项目中。

1. 横屏签名为啥非用 jSignature:这套 H5 方案解决的是手感问题

在移动端做电子签名,“横屏”是一个经常被忽略但体验又很重要的场景。银行业务、法务审批、物流签收这类需求里,用户更习惯把手机横过来,像握笔一样在屏幕中下部书写,因为横向宽高比更接近真实纸质表单。H5 签名横屏 jSignature 源代码,指的就是基于 jSignature 库在网页中生成一块可手写画布,并以横屏布局为主线的完整前端实现。这个方案的核心价值是纯前端、无需原生控件,签名结果既能导出 PNG 图片,也能以 SVG 矢量数据存储,为后续验真和合同归档留了口子。本文读者是正在做 H5 业务、被触摸签名折磨过的前端工程师,读完你不但能跑通最小可用版本,还能避开我在真实项目里踩过的那些坑。

2. 横屏签名的底层逻辑:jSignature 的工作原理与选型判断

2.1 jSignature 用矢量轨迹而不是像素堆叠来记录笔迹

先别急着复制代码,得搞清楚 jSignature 为什么在横屏签名里这么稳。它做的事情,是把手指或触控笔经过的坐标点按时间序列采集,再用贝塞尔曲线拟合出平滑轨迹,最后以“每一笔的控制点数组”形式保存,而不是直接保存一张照片。

举例来说,你写下一个“一”字,jSignature 记录的不是一行像素点颜色,而是一个包含起笔点、落笔点、曲线控制点坐标的数组。我见过不少新手以为签名控件和画板一样,每次 mousemove 就往 canvas 上画个圆点,那样存下来的根本不是签名,是一堆离散的像素操作记录,后续要改颜色、改线宽、合成到 PDF 上,全都得重画。

jSignature 默认的 getData 返回的数据格式长这样:

// jSignature 默认 getData() 返回一个数组, // 每个元素是 "x,y" 字符串,代表一个笔迹控制点 [ "2,4", "3,5", "4,5", "5,6" ]

这套数据本质上是 SVG 路径指令的变体。它带来的直接好处是文件体积极小:一个几千个采样点的签名,在内存里通常只有几十 KB。连续签几十个客户,页面不会卡死。第二个好处是可二次编辑:因为存的是矢量路径,导出时改背景色、改笔迹粗细、放大缩小都不会失真。做合同归档的业务,后端经常需要把白底黑字的签名合成到 PDF 页面上,如果一开始存的是位图,换个纸张背景就得重新抠图,非常痛苦。

2.2 横屏带来的宽高比变化,决定了签名“像不像真笔迹”

竖屏签名最大的问题是画布宽高比。竖屏状态下画布往往是 9:16,写出来的字会被纵向拉伸,签名看起来又窄又长,用户在手机上觉得还行,导出后放到 16:9 的电脑屏幕上看就变形了。横屏之后画布比例变成 16:9 左右,更接近用户在纸质文件上签名的视觉宽度,这也是业务方坚持要做横屏的根本原因。

一个很容易忽略的事实是:jSignature 本身不关心屏幕方向,它只负责在给定宽高的 canvas 里捕获坐标。所以横屏的关键工作不在 jSignature 内部,而在外层布局与视口适配:让 canvas 的 CSS 宽度尽可能贴近屏幕横屏可用宽度,高度按签名需要的比例收缩。我一般会把签名区做成“从屏幕顶部往下占约 55% 高度”的横条,既避免手指误触屏幕底部的手势区,也给上方按钮位留出空间。

2.3 选型前先确认三个边界条件

把 jSignature 塞进项目之前,我习惯先过三个边界条件,避免做到一半翻车:

  • 跑在什么 WebView 内核上:jSignature 是 jQuery 插件,依赖 jQuery 的事件体系和选择器。如果业务要兼容四五年前的旧 WebView 内核,建议先做一次真机回归。有些老旧内核的 canvas 绘制性能很差,连续快速划动时笔迹会断线。
  • 要不要压感笔迹:jSignature 默认不读取触摸压力值,它把每个触点当成统一半径来渲染。如果产品经理说“我想要重按出粗笔、轻写出细笔”,那得自己扩展 touch 事件的 force 字段,这部分最后一章会讲。
  • 离线还是在线:签名数据导出格式要提前和后端约定。如果还需要离线暂存草稿,优先选 SVG 矢量格式,因为它的体积小,放进 localStorage 也不心疼。

3. 完整源代码怎么组织:一套能直接运行的横屏签名页面

这一章是可以直接抄作业的部分。一个能真正跑起来的“完整版”,核心入口就一个 index.html,外加本地引用的两个库文件。考虑到很多企业内部环境禁止外链 CDN,我这边以本地文件为主要方案,CDN 方式写在备注里,二选一即可。

3.1 文件结构、引入顺序与版本选择

我习惯在项目根目录这样安排:

h5-signature/ ├── index.html ├── css/ │ └── sign.css ├── js/ │ ├── jquery.min.js │ └── jSignature.min.js

引入顺序有讲究:必须先把 jQuery 加载完,再加载 jSignature。jSignature 在插件定义阶段会直接检测 jQuery 是否存在,如果顺序反了,控制台会立刻抛错,后面的画布根本没机会创建。

<script src="js/jquery.min.js"></script> <script src="js/jSignature.min.js"></script>

我一般用 jQuery 1.12.x 搭配 jSignature 2.x,这个组合覆盖了从安卓老旧 WebView 到新版 iOS 的绝大多数环境。不用追新版本,jQuery 3.x 在部分老机器上的兼容反而不如 1.12 稳定,签名控件这种老牌插件,追求的是稳而不是新。

3.2 HTML 骨架与横屏锁定 CSS

页面的 DOM 结构是签名交互的核心,我分了三层:外层 phone 容器模拟横向视口,中间放提示栏和按钮,最里面才是签名画布容器。分层的意义在于,即使浏览器不允许强制横屏,页面 CSS 也能强制以横向比例展示签名区。

<div id="phone"> <div class="sign-header"> <span>请横屏签名</span> <button type="button" id="btnClear">重签</button> </div> <div id="signArea"></div> <button type="button" id="btnSubmit" class="btn-confirm">确认签名</button> </div>

对应 CSS 有几个关键点:

#phone { width: 100vw; height: 56.25vw; /* 16:9 横向比例,避免竖屏下画布被拉高 */ max-height: 100vh; margin: 0 auto; box-sizing: border-box; position: relative; background: #f7f8fa; } #signArea { width: 100%; height: calc(100% - 96px); /* 上方按钮区 + 下方确认按钮的高度预留 */ box-sizing: border-box; touch-action: none; /* 关键:禁止浏览器默认触摸滚动 */ background: #fff; }

这里有个很容易忽略的细节:touch-action: none。如果没有这条 CSS,浏览器会用默认触摸行为处理页面滚动和缩放,手指在画布上划动时会被系统判定为滚动手势,签名笔画会断成一截截的。我接手过的 H5 签名项目里,有三分之一“画不上线”的问题,其实都是少了这一行。

3.3 初始化参数与清空、确认的基础逻辑

接下来是 JavaScript 初始化。下面这份代码可以直接粘到一个空页面里调试:

$(document).ready(function () { var $signArea = $('#signArea'); // jSignature 初始化:宽度高度走 100%,让插件自己去量父容器 $signArea.jSignature({ width: '100%', height: '100%', color: '#2b2b2b', 'background-color': 'transparent', lineWidth: 2, undoButton: false }); // 重签:清空画布 $('#btnClear').on('click', function () { $signArea.jSignature('clear'); }); // 确认签名:导出两种格式,后续接后端时按需取用 $('#btnSubmit').on('click', function () { var svgData = $signArea.jSignature('getData', 'image/svg+xml'); var pngData = $signArea.jSignature('getData', 'image/png'); console.log('svg =>', svgData); console.log('png =>', pngData); }); });

参数说明:

  • width: '100%'和height: '100%':不写死像素值,让 jSignature 初始化时去读取父容器尺寸。这里最容易被坑:如果父容器高度是 0,或者还没渲染完成就初始化,canvas 高度会变成 0,整个签名区直接消失。
  • color: '#2b2b2b':签名笔迹颜色。别用纯黑#000,纯黑在扫描件上会产生过度对比,看起来像打印机喷墨,反而不真实。
  • 'background-color': 'transparent':画布背景设透明,方便后续合到合同底图上。如果你希望有白色纸面效果,改成'#ffffff',再由外层 div 加边框。
  • lineWidth: 2:默认线宽在 2K 屏的安卓机上会显得偏细,我通常会在初始化前读取 devicePixelRatio,把线宽乘上去。如果你直接改大这个值,老的 iPhone 上笔迹又会粗得像马克笔。
  • undoButton: false:jSignature 内置的撤销按钮布局很丑,融入不了业务 UI,我都是关掉它,自己用“重签”按钮代替。

别忘了在 HTML 头部加 viewport meta,禁止双击缩放:

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">

不加这条的话,用户快速连点两下,页面会放大,画布坐标全部错位。

4. 签名数据导出、回显与上传:接后端前必须吃透的三种格式

签名控件做完之后,难点不在画,而在把签名的结果安全送到后端。这一章把三种数据流说透:PNG 位图、SVG 矢量、回显时导入。这一步做不好,前端画得再顺,后端也没法把签名落进合同。

4.1 导出 PNG 位图:清晰度、尺寸与透明底三个核心参数

最常见的做法是把签名区导出成 PNG 交给后端存成图片文件。jSignature 的 getData 方法接受 MIME 类型作为参数:

// 导出 PNG base64 字符串 var pngBase64 = $signArea.jSignature('getData', 'image/png'); // 结果形如:data:image/png;base64,iVBORw0KGgo...

这块最常见的坑是“图片发虚”。jSignature 初始化时创建的 canvas,默认逻辑尺寸等于容器 CSS 尺寸,不会自动适配屏幕的 devicePixelRatio。安卓机上常见 1.5、2 甚至 3.75 的像素比,如果画布逻辑像素只有 CSS 像素的一半,导出图片在电脑上看就会模糊。

我的习惯是初始化前先把画布逻辑尺寸乘以 dpr:

var dpr = window.devicePixelRatio || 1; var areaWidth = $('#signArea').width(); var areaHeight = $('#signArea').height(); $('#signArea').jSignature({ width: areaWidth * dpr, height: areaHeight * dpr, lineWidth: 2 * dpr, color: '#2b2b2b', 'background-color': 'transparent' });

但这样引入一个新问题:canvas 固有尺寸变大后,CSS 布局上显示也会跟着变大。解决办法是初始化后再加一条 CSS,强制 canvas 显示尺寸回退到父容器 100%:

#signArea canvas { width: 100% !important; height: 100% !important; touch-action: none; }

这样画布的物理像素是 CSS 显示尺寸的 dpr 倍,视觉上仍然铺满容器,导出图片清晰度才能保住。这个方案在安卓和 iOS 的 WebView 里都验证过,是相对最稳的一招。

4.2 导出 SVG 矢量:合同归档的最佳数据形态

PNG 适合展示,SVG 才是 jSignature 最有价值的输出。SVG 存的是笔画的矢量路径,文件体积小,放大不失真,还能从数据层面做字迹比对。用法如下:

// 导出 SVG 字符串 var svgData = $signArea.jSignature('getData', 'image/svg+xml'); // 伪示例,返回的是完整 SVG XML 字符串: // <svg xmlns="http://www.w3.org/2000/svg" width="..." height="..."> // <path d="M2,3 C2,3 3,4 4,4 Z" fill="none" stroke="#2b2b2b" stroke-width="2"/> // </svg>

真正与后端对接时,我建议传 SVG 字符串而不是 PNG。理由很简单:SVG 字符串通常只有 1 到 5KB,比同样的 PNG 小一个数量级;后端拿 SVG 可以直接内嵌进 PDF 或转成图片,不需要额外引入压缩处理。

这里提醒一点:jSignature 导出的 SVG 保留了画布的绝对坐标。如果用户在签名过程中旋转过手机导致画布尺寸变化,SVG 的坐标系也会跟着变。所以回显时必须在同一个尺寸的画布上做,否则笔迹会错位。这也是横屏签名比竖屏更容易出问题的深层原因。

4.3 回显之前必须做的清理操作,以及向后端提交的最小实现

回显场景很常见:用户签完名进入确认页,或者编辑时重新加载,需要把之前保存的 SVG 重新画回画布。jSignature 提供 setData 方法:

// 回显 SVG 签名 var savedSvg = '...'; // 后端返回的 SVG 字符串 $signArea.empty(); // 必须:先把旧画布清掉 $signArea.jSignature('setData', ['image/svg+xml', savedSvg]);

这里有三个连环坑。第一,setData 之前不加 empty 的话,新数据会叠加在旧笔迹上,形成重影。第二,参数必须是数组格式['image/svg+xml', svgString],少一个字段或者顺序反了都会静默失败,控制台还不报错,我当年在这里花了两小时才找到原因。第三,要在页面渲染完成后再调 setData,如果你在 DOM 还没布局好时就执行,画布宽度为 0,导入会失败且没有提示。

表单提交时,我一般把 SVG 和 PNG 一起放进 FormData:

var svgData = $signArea.jSignature('getData', 'image/svg+xml'); var canvas = $signArea.find('canvas')[0]; canvas.toBlob(function (blob) { var fd = new FormData(); fd.append('signatureSvg', svgData); fd.append('signaturePng', blob, 'sign.png'); fd.append('customerId', $('#customerId').val()); $.ajax({ url: '/api/sign/upload', method: 'POST', data: fd, processData: false, contentType: false, success: function (res) { alert('签名保存成功'); } }); }, 'image/png');

注意canvas.toBlob在部分老 WebView 里不存在。如果必须兼容老机型,先用getData('image/png')拿到 base64,再手动转换:

var base64 = $signArea.jSignature('getData', 'image/png'); fd.append('signaturePngBase64', base64);

后端拿 base64 解码也不复杂,只是多一步而已。

5. 横屏签名避坑清单:5 个高频故障现象、原因与修复

下面这 5 条,是我在多个 H5 签名项目里反复踩过的坑,属于“只要你做横屏签名,几乎一定会遇见”的级别。每条按“现象 → 原因 → 解决”写清楚,你可以直接对照排查。

5.1 画布白屏但画不出笔迹:触摸层被透明遮罩挡住

现象:页面加载正常,白色画布显示出来了,手指划上去,什么痕迹都没有,控制台也没有任何报错。

原因:最常见的元凶是某个透明遮罩层覆盖在 canvas 上方。比如“请横屏”提示框淡出后留下的透明 div,或者某个 loading 浮层容器没有销毁。jSignature 监听的是 canvas 自身的 mousedown、mousemove、mouseup 以及对应 touch 事件,这些事件不会穿透遮盖元素。

解决:第一步,浏览器开发者工具里检查元素,确认是哪个节点压在 canvas 上面;第二步,如果是浮层残留,在画布初始化结束后把浮层 display:none;第三步,给 canvas 加一条position: relative; z-index: 10,保证它待在兄弟节点之上。另外,如果你使用了某些弹层组件,注意它的 mask 是否默认全屏覆盖。

5.2 笔迹跟手指错位:CSS 缩放与 Canvas 固有尺寸不一致

现象:手指按在屏幕左侧,笔迹却出现在偏中间的位置;越靠近屏幕边缘,偏差越明显。

原因:jSignature 初始化时测量的 canvas 固有尺寸,和 CSS 渲染尺寸不一致。常见于横屏适配时给 canvas 写了width: 100%; height: 100%,但 canvas 元素的 width/height 属性仍是初始化时的某个固定值。触摸事件的坐标被 jSignature 按固有尺寸处理,视觉上 CSS 又把画布拉伸了,坐标自然对不上。

解决:不要把固定像素写死在 HTML 里,统一用初始化时的实际容器尺寸。最稳妥的方式是让 canvas 的固有宽高等于容器宽高乘以 dpr,再用 CSS 强制显示尺寸退回 100%。如果画布尺寸必须在某个时机改变,比如窗口旋转,改完后要重新初始化 jSignature,让它重新测量,不要复用旧实例。

5.3 横屏锁定失败:WebView 策略和用户习惯的博弈

现象:点击“开始签名”后,页面没有旋转成横屏;或者一开始是横的,手机轻微倾斜又变回竖屏。

原因:浏览器的横屏锁定 API 并非处处可用。screen.orientation.lock只在部分安卓 WebView 和较新的 iOS Safari 里生效,很多小程序容器或 App 内置 WebView 直接禁用了这个 API。而 CSS 的@media (orientation: landscape)只是样式适配,并不会强制物理屏幕旋转。

解决:务实做法是“软锁横屏”。进入签名页时提示用户把手机横过来,同时页面 CSS 保证即使当前处于竖屏状态,签名区也按 16:9 比例渲染,用户一转手机就能直接用。对于支持锁屏 API 的浏览器,可以尝试调用:

if (screen.orientation && screen.orientation.lock) { screen.orientation.lock('landscape-primary').catch(function () { // 不支持就别强求,走软锁方案 }); }

注意 catch 必须写上,因为很多浏览器会抛 Promise 异常,不处理的话控制台会报红。但我不把它作为唯一手段,真正的兜底是布局上的横屏自适应。

5.4 导出图片模糊:像素比、最大画布尺寸和手机型号的三角平衡

现象:签名在手机上看很清晰,导出 PNG 后放到电脑上查看,边缘发虚、锯齿明显。

原因:上一章提过,canvas 逻辑尺寸没有按 dpr 放大。生产环境里还有一个隐藏细节:部分安卓机上报的 dpr 是 2,但系统缩放后实际渲染像素比更高,导致导出尺寸依然不够。反过来,如果你为了清晰度无限放大 dpr,又可能撞上 canvas 的最大尺寸限制。

解决:初始化前读取 devicePixelRatio,把画布宽高乘以 dpr,线宽也乘以 dpr,CSS 显示尺寸保持容器逻辑尺寸。另外要把 dpr 封顶在 3 左右。我测试过,从 2 倍提升到 3 倍,清晰度提升明显;从 3 倍到 4 倍,文件变大了,肉眼清晰度几乎没区别,反而是部分机型 canvas 超过 4096 宽度后直接绘制中断。

5.5 旋转屏幕后签名消失或位移:resize 时机没有处理

现象:用户正签着名,系统通知栏一拉,横竖屏切换,回到页面发现笔迹全没了,或者笔迹偏移了一大截。

原因:jSignature 在初始化时把画布尺寸和事件绑定都确定了。窗口 resize 后,canvas 的固有尺寸不会自动改变,绘图上下文里的坐标系却可能已经错乱;某些版本的 jSignature 在检测到容器尺寸变化后,会直接把画布内容清空。

解决:两个策略一起上。第一个,签名页尽量锁定布局方向,不要在签名过程中允许切换方向。第二个,监听 resize 事件,一旦方向变化且签名已经存在,先 getData 保存当前数据,等尺寸稳定后再 setData 还原:

window.addEventListener('resize', function () { var curSvg = $signArea.jSignature('getData', 'image/svg+xml'); // 延迟到方向动画完成之后再重置画布 setTimeout(function () { $signArea.empty(); $signArea.jSignature('setData', ['image/svg+xml', curSvg]); }, 300); });

这套“后悔药”写法,防的是那一瞬间的 resize 把画布清空,实测能救回绝大多数签名现场,但最好还是从入口处就尽量避免方向切换。

6. 进阶技巧:让签名笔迹更像真实手写,同时保住导出清晰度

标准 jSignature 用统一线宽画每条笔画,所以签出来的名字看起来像用同一支马克笔写的。真实手写时,笔尖和屏幕接触的力度不同,笔画的粗细会有变化。jSignature 不默认读取压力值,但我们可以自己扩展。

做法不复杂:在初始化完成后,监听 canvas 上的 touchmove 事件,读出 touch 对象的 force 字段:

var lastForce = 1; $('#signArea').on('touchmove', function (e) { var touch = e.originalEvent.touches[0]; if (touch.force && touch.force > 0) { lastForce = touch.force; } });

拿到力值之后,一个讨巧的做法是:每次 touchend 后,对最后一段笔迹做一次粗细归一化,力气大的段落保留较粗的 stroke,力气小的段落变细。但 jSignature 本身没有公开“改某一段线宽”的 API,所以现实中更多是在导出 SVG 时对 path 的 stroke-width 做二次加工,而不是在绘制中实时改。换句话说,压力感这件事,在展示层做比在采集层做更容易控制。

第二个重点是清晰度。签名只是前端交互的一小步,真正决定业务成败的是后端拿到的图片能不能直接上合同。我在一次上线前做过批量测试:同一支触控笔,在 iOS 和两台不同分辨率的安卓设备上各签了二十次,导出 PNG 后统计有效像素占比。结论是:dpr 放大到 2 到 3 倍,清晰度提升最明显;继续放大到 4 倍,体积增加,清晰度收益几乎为零。所以现在凡是新项目,我都把 dpr 封顶写死为 3。

另外一个调试习惯也分享给你:不要全程依赖真机远程调试,真机手感虽然最准,但效率太低。我会在本地临时做一个隐藏按钮,直接在当前页面输出签名数据到控制台,再配合浏览器开发者工具的移动端模拟器做快速循环验证。把“画一笔 → 看数据 → 改参数 → 再画一笔”这个循环缩短到一分钟内,能大幅减少无效沟通。

如果你最后遇到“明明代码没变,这次画布却空白了”的情况,先看控制台有没有报错,再看是不是新增的弹层把画布盖住了,这两步能定位九成以上问题。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进