基于jSignature的H5横屏电子签名完整方案与踩坑实录

基于jSignature的H5横屏电子签名完整方案与踩坑实录 简介面向移动端H5签名场景的横屏jSignature源码包适合需要快速在手机网页中实现电子签名、并将签名保存为可选格式图片的开发者或项目团队。压缩包内共69个文件约2.98MB以27个JavaScript逻辑文件、15个CSS样式文件及3个HTML页面为核心同时包含ttf、woff、eot、svg等字体资源与jpg、png图片素材可支撑界面渲染与图标展示。源码已封装可直接运行的完整实例横屏签名后一键保存图片图片格式可选覆盖jSignature插件接入、jQuery及Vue辅助处理、Swiper与AUI组件整合、页面样式和签名成功页等模块便于对照修改或嵌入现有项目。已有1069人学习下载适合前端初学者借完整示例理解签名插件调用流程也适合需快速落地的中级开发者直接复用。 最近在做移动端审批流程的H5页面领导提了个需求在手机横屏状态下完成电子签名。我第一反应是直接用jSignature插件结果真正落地时才发现坑不少——横屏适配、Canvas坐标偏移、图片导出格式、iOS兼容性每个环节都能卡你半天。折腾了一整天我把完整可运行的方案整理出来了这篇文章里的代码直接拷贝就能用适合正在做H5签名功能、又恰好被横屏适配折磨的前端同学参考。先说清楚这套代码解决什么问题它基于jSignature这个老牌手写签名库实现了在手机浏览器横屏状态下流畅签名、一键清空、保存签名图片、回显签名这4个核心功能。不需要引入框架原生JavaScript就能跑放在任何静态页面里都能直接用。1. 整体设计思路拆解为什么是jSignature而非其他方案1.1 横屏签名的真实业务场景移动端需要横屏签名最典型的场景是电子合同签署和医疗确认单。竖屏状态下签名区域太窄写出来的名字被压缩变形识别率低法务那边也不认。横屏后签名区域宽度能达到700像素以上书写体验接近纸质签名。但问题来了浏览器本身不提供强制横屏的APIscreen.orientation.lock()这个接口在iOS Safari上完全不支持Android Chrome也只支持部分锁定方向。所以业界的通用做法是“软横屏”——通过CSS旋转把整个页面横过来配合用户手动旋转手机达到横屏签名的效果。我在实践中最常用的方案是检测到用户手机处于竖屏时弹层提示“请将手机旋转至横屏”同时用CSS把签名区域旋转90度作为兜底。这种组合策略既照顾了用户体验又确保了功能可用性。1.2 为什么选jSignature而不是Canvas手写代码市面上做H5签名的方式大概分三种第一种是纯Canvas手写几十行代码就能实现基础效果但线条平滑度、笔迹压感、触摸事件兼容性都要自己调至少得花一天时间打磨第二种是signature_pad插件功能比jSignature强但包体积大对老旧Android WebView的兼容性不如jSignature第三种就是jSignature它基于Canvas和jQuery体积小、依赖少、API简洁专门为移动端触摸优化过在老设备和现代浏览器上表现稳定。jSignature最吸引我的一点是它支持矢量签名导出getData(image/svgxml)能直接输出SVG格式而SVG是法律上认可的电子签名格式之一。如果用透明背景的PNG导出后端做电子合同存档时就方便多了。综合对比下来jSignature在“简单项目快速落地”这个场景下是最优选。1.3 整套方案的架构分层我习惯把整套签名功能拆成三层每层职责清晰出了bug也好定位横屏适配层负责检测屏幕方向、提示用户旋转、CSS旋转兜底。这一层是H5签名横屏方案的地基。jSignature核心层负责签名画布的初始化、触摸绘制、清空重置、数据导出。这一层是功能主体。业务交互层负责页面上的按钮事件、签名数据回显、与后端接口对接。这一层根据你的具体业务调整。下面我按这三个层次逐一展开每一层都会给出可直接运行的代码和踩坑经验。2. 核心细节解析与实操要点2.1 jSignature初始化的关键参数jSignature的初始化方式很简洁核心代码就一行$(#signature).jSignature({ width: 800, height: 400, color: #333333, background: transparent, lineWidth: 2, decoration: false, undo: false });这里有几个参数值得细说。width和height决定签名区域的大小注意这是Canvas的逻辑像素尺寸不是CSS尺寸。横屏状态下我建议宽度设800、高度设350到400这是经过实测的比例签名书写体验最接近纸质。color是笔迹颜色默认黑色即可签名不需要花哨颜色。background透明背景很关键导出PNG时能保留透明通道方便叠加到合同模板上。如果把background设成白色导出图片会带白色底后台上传合同的时候就会出现一块突兀的白方块。lineWidth控制笔迹粗细移动端触屏一般设2到3像素比较合适。undo参数我觉得没必要开启签名场景中用户签错了直接清空重签更高效撤销功能反而增加操作复杂度。2.2 触摸事件兼容性处理jSignature源码里对触摸事件做了封装但它依赖jQuery的事件绑定机制。在实际测试中我发现部分Android WebView对touchstart、touchmove、touchend事件存在兼容问题表现为手指滑动时没有笔迹或者笔迹断断续续。解决办法是在初始化前强制给签名区域添加触摸事件处理并禁用浏览器的默认滚动行为var canvas document.getElementById(signature); canvas.addEventListener(touchstart, function(e) { e.preventDefault(); }, { passive: false }); canvas.addEventListener(touchmove, function(e) { e.preventDefault(); }, { passive: false });这里特别提醒{ passive: false }这个参数。移动端浏览器默认把touchmove当成被动事件处理即不阻止默认行为如果你在事件回调里调用preventDefault()却不声明passive: false浏览器会直接忽略你的阻止操作页面就会在签名时跟着滚动笔迹全部错乱。这个坑我踩过一次排查了半小时才定位到是passive listener的问题。2.3 横屏适配的核心CSS旋转与坐标映射这一节是整个方案的重头戏。横屏适配有两种实现路径我必须都讲清楚因为你不知道你会遇到哪种设备环境。第一种路径提示用户旋转页面不旋转。这种方式最保守页面保持竖屏布局检测到屏幕方向变化时调整签名区域尺寸。代码里监听orientationchange事件window.addEventListener(orientationchange, function() { setTimeout(function() { var angle window.orientation || screen.orientation.angle; if (angle 90 || angle -90) { // 横屏状态调整签名区域宽高 $(#signature).width(screen.width - 80); $(#signature).height(screen.height * 0.4); $(#signature).jSignature(reset); } }, 300); });第二种路径强制CSS旋转。这个方案不依赖用户旋转手机直接把整个容器旋转90度用CSS transform实现“页面横过来”的效果。核心代码.signature-container-landscape { position: fixed; top: 0; left: 0; width: 100vh; height: 100vw; transform: rotate(90deg); transform-origin: 0 0; overflow: hidden; }这段CSS的原理是把容器的宽高对调宽占满整个视口高度高占满视口宽度然后以左上角为原点顺时针旋转90度视觉上就是横屏效果。但这里有个隐患CSS旋转后Canvas绘图坐标不会自动跟着变。你看起来页面是横的但touch事件的坐标还是基于竖屏坐标系的导致签名笔迹偏移。解决方法是给触摸坐标做一次旋转映射canvas.addEventListener(touchstart, function(e) { var touch e.touches[0]; var x touch.clientY; var y window.innerHeight - touch.clientX; // 把映射后的坐标传给jSignature的绘图事件 // 通过重新触发mousedown事件模拟绘图 }, { passive: false });说实话CSS旋转方案在坐标映射这块比较折腾只适合“用户就是不旋转手机但业务方又强制要横屏”的极端场景。我的建议是优先用第一种路径配合友好的提示文案引导用户旋转手机体验最好代码也最少。2.4 签名数据的保存与回显签名最重要的功能就是把用户签的字保存下来提交给后端。jSignature提供两个方法getData(image/png)返回base64编码的PNG图片数据适合直接传给后端存储。getData(image/svgxml)返回SVG矢量数据适合需要无损缩放的场景。我一般把PNG和SVG同时导出PNG用于页面展示SVG用于后端存档。导出代码var pngData $(#signature).jSignature(getData, image/png); var svgData $(#signature).jSignature(getData, image/svgxml); // 提交给后端 $.ajax({ url: /api/signature/save, method: POST, data: { signatureSvg: svgData[1], signaturePng: pngData[1] } });注意getData返回的格式是一个数组第一个元素是MIME类型如data:image/png;base64第二个元素才是真正的数据。我在第一次对接后端时直接传了数组结果后端解析失败排查了很久才发现这个问题。回显签名的代码也很简单$(#signature).jSignature(setData, data:image/png;base64,xxxxxxxxxx);把之前保存的base64字符串传进去jSignature就能原样绘制回画布。注意回显时画布的大小和Signature初始化时的宽高要一致否则图片会被拉伸变形。3. 完整可运行源码与部署说明3.1 基础环境准备这套源码不需要npm、不需要webpack一个HTML文件加一个jQuery文件就能跑。建议下载jQuery 3.x版本我实测2.x和3.x都能与jSignature的1.9.x版本配合但3.x兼容性更好。jSignature 1.9.x是最后一个稳定版本GitHub上已经不再维护但这个库非常成熟不需要担心稳定性问题。国内CDN可以直接用bootcdn或jsdelivr加速。3.2 完整HTML源码把下面代码保存为signature-landscape-demo.html同一目录下放好jquery和jSignature的js/css文件直接浏览器打开就能运行!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0, maximum-scale1.0, user-scalableno titleH5横屏签名Demo/title link relstylesheet typetext/css hrefjquery.jSignature.css style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; background: #f5f6fa; padding: 20px; } .container { max-width: 900px; margin: 0 auto; background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); } .header { text-align: center; padding: 20px 0 10px; } .header h1 { font-size: 20px; color: #333; } .header p { color: #888; font-size: 14px; margin-top: 6px; } .rotate-tip { display: none; background: #fff3cd; color: #856404; padding: 10px 14px; border-radius: 8px; font-size: 14px; margin-bottom: 16px; text-align: center; } .signature-wrap { border: 2px dashed #ccc; border-radius: 8px; overflow: hidden; background: #fff; margin: 20px 0; } .signature-wrap canvas { width: 100%; display: block; } .btn-group { display: flex; gap: 12px; margin: 16px 0; } .btn-group button { flex: 1; padding: 12px 20px; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; transition: opacity 0.2s; } .btn-group button:active { opacity: 0.8; } .btn-clear { background: #f0f0f0; color: #333; } .btn-save { background: #4CAF50; color: #fff; } .btn-reset { background: #ff9800; color: #fff; } .preview { margin-top: 20px; } .preview h3 { font-size: 16px; color: #555; margin-bottom: 10px; } .preview img { max-width: 100%; border: 1px solid #eee; border-radius: 8px; background: #fff; } media (orientation: landscape) { .rotate-tip { display: none !important; } .container { max-width: 100%; } } media (orientation: portrait) { .rotate-tip { display: block; } .signature-wrap { min-height: 240px; } } /style /head body div classcontainer div classheader h1H5手写签名横屏适配版/h1 p支持触摸签名 一键清空 图片保存 签名回显/p /div div classrotate-tip 横向签字体验更佳建议将手机旋转至横屏模式 /div div classsignature-wrap div idsignature/div /div div classbtn-group button classbtn-clear onclickclearSignature()清空重签/button button classbtn-save onclicksaveSignature()保存签名/button button classbtn-reset onclickresetSignature()重新初始化/button /div div classpreview h3签名预览保存后显示/h3 img idpreviewImg src alt签名预览 /div /div script srcjquery-3.6.0.min.js/script script srcjquery.jSignature.min.js/script script var signatureWidth 800; var signatureHeight 350; function initSignature() { $(#signature).jSignature({ width: signatureWidth, height: signatureHeight, color: #2c3e50, background: transparent, lineWidth: 2.5, decoration: false }); } function clearSignature() { $(#signature).jSignature(clear); } function resetSignature() { $(#signature).jSignature(reset); } function saveSignature() { var pngData $(#signature).jSignature(getData, image/png); var svgData $(#signature).jSignature(getData, image/svgxml); var pngBase64 pngData[1]; var svgStr svgData[1]; document.getElementById(previewImg).src data:image/png;base64, pngBase64; console.log(PNG数据长度, pngBase64.length); console.log(SVG数据长度, svgStr.length); // 这里对接你的后端保存接口 // $.post(/api/signature/save, { // signatureSvg: svgStr, // signaturePng: pngBase64, // timestamp: Date.now() // }, function(res) { // alert(签名保存成功); // }); } function setExistingSignature(base64Data) { $(#signature).jSignature(setData, data:image/png;base64, base64Data); } window.addEventListener(orientationchange, function() { setTimeout(function() { var angle window.orientation || screen.orientation.angle; if (angle 90 || angle -90) { signatureWidth window.screen.width - 40; signatureHeight Math.floor(signatureWidth * 0.42); $(#signature).jSignature(reset); } }, 300); }); initSignature(); /script /body /html3.3 横屏尺寸动态适配说明注意代码里orientationchange事件的处理逻辑。当检测到用户旋转到横屏时我重新计算了签名区域的宽高宽度用屏幕宽度减去40像素留白高度按宽度的42%计算。这个比例来自多点实测签名区域太高会超出视野太低则写字太局促。还要注意在setTimeout延迟300毫秒后再重置画布因为orientationchange事件触发时浏览器的视口尺寸还没完全更新立即获取的screen.width可能是旧值。这个延迟是最佳实践经验不是随便写的数字。3.4 签名回显的使用场景setExistingSignature函数在编辑已有合同场景中非常实用。比如用户之前已签过名现在需要查看或重新签署时后端返回历史签名数据前端调用这个函数就能在画布上把旧签名画回来。回显后用户如果不想改直接保存即可如果想重签点一下“清空重签”就行。我见过有些项目在这里设计了“历史签名确认”按钮逻辑是回显后用户确认无误则锁定画布禁止修改需要修改则先清空再重签。这种交互在金融和法律场景中是硬需求你可以按需扩展。4. 常见问题与排查技巧实录4.1 问题速查表问题现象可能原因解决方案手指滑动无笔迹触摸事件未绑定或被动事件阻止了preventDefault给canvas添加touch监听设置{passive: false}并调用preventDefault笔迹与手指位置偏移CSS旋转后未做坐标映射按2.3节的映射公式转换坐标或用提示旋转的软横屏方案保存的图片背景是黑色jSignature初始化background参数未设置或设置无效显式设置background: transparent并在CSS中确保canvas无背景色横屏后签名区域变形旋转后未重新初始化画布在orientationchange回调中延迟300ms后重置jSignature画布iOS上Safari不支持横屏APIscreen.orientation.lock在iOS Safari不可用使用提示用户旋转的策略不要尝试lock接口签名图片拉伸变形画布初始化尺寸和setData回显尺寸不一致确保回显前画布尺寸与初始尺寸保持一致微信浏览器内页面缩放viewport未设置user-scalablenometa标签中添加maximum-scale1.0, user-scalableno笔迹断断续续canvas区域被遮挡或事件未防抖给canvas加高z-index并在touchmove中做坐标连续性处理4.2 微信内置浏览器和App WebView的差异处理如果你的H5页面需要跑在微信内置浏览器或企业App的WebView里一定要做针对性测试。微信浏览器的iOS版本对Canvas支持很好但Android版本在不同机型上差异巨大特别是华为和小米的老款机型。我踩过最典型的一个坑是在小米某款机型上jSignature初始化后有个2像素的模糊白边看起来像签名区域脏了一块。排查后发现是Canvas被CSS的width: 100%缩放导致的亚像素模糊。解决方案是给Canvas一个宽度略小于容器的固定像素值或者用image-rendering: pixelated样式。企业App的WebView通常会注入自己的JSBridge这可能与jQuery的事件机制产生冲突。建议在初始化jSignature前检查window.JSBridge是否存在存在则延迟150毫秒再初始化确保JSBridge先加载完成。4.3 签名图片体积优化jSignature生成的PNG base64字符串经常超过100KB如果合同页面上有多个签名甲方、乙方、日期整体数据量会很大。这里有两个优化技巧一是用SVG数据做后端存档SVG是矢量格式文件很小通常只有几KB但法律效力更高二是前端展示时把PNG压缩到合适尺寸用Canvas的toDataURL(image/jpeg, 0.8)转成JPEG体积能降60%以上。还有一点很重要微信内置浏览器的定位感知不同导致页面刷新后签名丢失。如果业务要求刷新后签名不丢你需要把签名数据暂存到sessionStorage页面重新加载时自动取回并回显。代码很简单window.addEventListener(beforeunload, function() { var data $(#signature).jSignature(getData, image/png); sessionStorage.setItem(tempSignature, data[1]); }); window.addEventListener(load, function() { var data sessionStorage.getItem(tempSignature); if (data) { $(#signature).jSignature(setData, data:image/png;base64, data); } });4.4 关于“未签名”状态的前端校验很多业务场景要求用户必须完成签名才能提交。但jSignature没有暴露“是否有笔迹”的API只能自己判断。我的做法是导出PNG数据后检查数据长度是否超过一个阈值function hasSignature() { var data $(#signature).jSignature(getData, image/png); // 空签名的PNG base64长度通常小于2000有笔迹的数据远大于这个值 return data[1].length 2000; }这个阈值在大多数设备上是可靠的但如果你发现某些设备上签名很淡比如用了浅色笔迹阈值可以适当调到1500。注意这个方法只能做初步校验如果业务要求严格的“是否签名”最好还是让后端通过检测SVG中的路径节点数来判断。5. 一套代码跑通“后台管理端回显签名”的扩展思路项目上线后一定会遇到一个需求管理后台需要查看用户在手机端签署的合同和签名。这里的签名数据回显方式和手机端不同后台是PC浏览器没有触摸事件jSignature默认的鼠标绘制在PC端也能用它封装了mousedown/mousemove/mouseup所以PC端也能正常显示签名。但如果你不希望PC端用户能修改签名比如管理员只有查看权限可以初始化时设置只读模式$(#signature).jSignature({ width: 800, height: 350, readonly: true, lineWidth: 2 });实测下来只读模式下jSignature会把Canvas设为只读层鼠标和触摸都不会触发绘制签名区域变成一个静态图片展示区。这是非常实用的隐藏功能文档里容易忽略但后台项目里几乎必用。再扩展一下如果后台需要把签名和合同正文合成一张图比如生成签署完成的合同PDF前端可以先把签名SVG嵌入到合同HTML中再用html2canvas或puppeteer生成图片。这个流程我在项目里跑通过但要注意html2canvas对SVG的支持不完美部分字体和样式会丢失正式环境建议用后端Node.js配合puppeteer生成更稳定。回到手机端如果你觉得签完名后还要让用户确认一下再提交可以再加一个“预览确认弹窗”。核心思路是把签名区域隐藏展示生成的签名图片用户点“确认”再提交点“重签”则返回签名界面。这三步交互在正式项目中非常常见能有效避免用户误触或者签名不满意但已经提交的问题。这个方案从最初的竖屏签名Demo到最终的横屏多端适配我用了一整天时间打磨核心收获其实就一句话移动端H5签名这件事30%的工程量在签名本身70%的工程量在适配各种浏览器和WebView。只要把横屏适配、触摸事件兼容、图片导出这几个关键节点踩实了后面就能顺风顺水。本文还有配套的精品资源点击获取