简介:移动端网页签名场景中,横屏适配常遇到绘制区域错位、触摸事件不精准等问题,这份H5签名横屏jSignature源代码正好提供了成熟解法,适合需要快速集成手写签名、图片导出及横屏适配的开发者参考。资源包共69个文件,以JavaScript逻辑脚本、CSS样式表和HTML页面为主,同时包含少量字体图标、图片及配置文件,整体体积仅2.98MB,目录结构清晰,开箱即可运行。除核心jSignature插件外,还整合了jQuery、Vue、swiper等常用前端依赖,兼顾不同技术栈的阅读习惯;配合横屏布局可实现流畅的触控签名采集,并支持按需选择图片格式导出,满足合同签署、确认登记等业务场景。已有1073人学习下载。通过该源码可清晰了解签名绘制、画布坐标处理、横屏布局适配及导出保存的完整实现路径,并可直接复用核心代码,无论是教学演示还是项目改造,都能节省从零开发的调试时间。
1. H5签名横屏jSignature源代码:直接能跑的签名页,到底省了哪些事
某移动端审批项目要加一页电子签名,PC上鼠标签还好,手机一竖屏,签名区只有半屏宽,用户签出来的名字被压得不像字。项目排期紧,不可能从头写 canvas 的触摸笔画和撤销栈,网上竖屏示例很多,横屏版本要么依赖装不起来的原生插件,要么旋转之后笔迹和手指对不上。我要的是一份能直接打开运行的 H5签名横屏jSignature源代码:用 jSignature 这个轻量签名库,把横屏适配、坐标对齐、导出格式封装好,放到浏览器里就能跑通签名、清空、提交全流程。
它解决的是移动端合同确认、流程审批、实名签署这些场景里最现实的签名采集问题。适合正在做 H5 应用的前端工程师,也适合要快速交付签名能力、又不想从零研究触摸绘制的团队。全文按“原理、直接运行、参数调整、踩坑、对接后端”的顺序展开,后面每一段代码都是从这份可直接运行的源码里拆出来的。
需要说明的是,网上能找到的 jSignature 示例大多只给竖屏初始化,而横屏版本的核心难点不在签名库本身,而在横屏容器和初始化时机的配合。理解了这个,后面照着改参数就不会翻车。
2. jSignature 横屏签名原理:旋转方案、坐标系与初始化时机
2.1 为什么选 jSignature:轻量、触摸友好、导出格式全
jSignature 是个老牌的 canvas 签名库,依赖 jQuery。我第一次接触它的时候也犹豫过,毕竟现在新项目很少再引 jQuery,但真去对比一轮就会发现,它在签名这个垂直场景里依然是性价比最高的选择。
自己从零写签名板的完整工作量大概是这样的:监听 pointerdown、pointermove、pointerup,把触点坐标换算成 canvas 坐标;对快速移动产生的折线做二次贝塞尔平滑,不然签出来的名字全是折角;维护一个笔画栈实现撤销;最后再从 canvas 导出 base64 图片。这套东西写下来至少几百行,而且真机上的触摸事件差异还要单独调。jSignature 把这四件事全部收敛成了$('#sig_area').jSignature(options)一行调用,底层自动处理 touch 和 mouse 兼容,这就是我选择它的理由。
从源代码管理的角度来看,jSignature 也很适合做模板:它本身就是一个独立的 min.js,不依赖 Node 环境,没有构建步骤,和 jQuery 一起放进目录直接<script>引入就行。对团队来说,这种交付方式比引入一个 npm 构建链更可控,至少不会遇到“上周还能打包,这周依赖升级后跑不起来”的问题。
导出格式也是它的一大优势。jSignature 支持 image/svg+xml、image/png、image/jpeg 等格式,导出的数据以数组形式返回,第二项直接是带 data URI 前缀的字符串。这一点对移动端尤其重要:签名数据既能转成文件上传,也能拆掉前缀变成纯 base64 字符串提交给后端接口,后面第 6 章会具体写。
2.2 横屏化两种路线:CSS旋转与屏幕方向API怎么选
要做 H5 横屏签名,首先得把“横屏”这件事想清楚。移动端 H5 有两条路:
路线 A 是 CSS transform 旋转。给最外层容器加rotate(90deg),把竖屏页面视觉上压成横屏。优点是代码少、兼容性好,几乎所有浏览器都能跑;缺点是事件坐标系会乱,canvas 虽然能画出内容,但笔迹会跟手指错位,fixed 定位的元素也会跟着歪掉。更麻烦的是,旋转后的 canvas 内部尺寸其实还是竖屏时读到的那一组值,导出图片的宽高比例和用户签名体验都对不上。
路线 B 是让页面真正运行在横屏视口下。检测window.innerWidth > window.innerHeight,再配合屏幕方向 API,把签名区做成横屏尺寸。这种方案下 jSignature 收到的触摸坐标直接对应 canvas 坐标,笔迹跟手,导出图片比例正常,是我在项目里优先采用的做法。
真实浏览器环境里有个限制:部分内置浏览器或 iOS 系统锁定了竖排方向,脚本调用屏幕方向 API 并不总能强制旋转,所以工程上的成熟做法是“真实横屏为主,CSS 旋转兜底”。检测到横屏就直接展示,检测到竖屏就把最外层容器旋转 90 度,等旋转稳定后再初始化签名板。
有人可能会问,为什么会用 CSS 旋转做兜底,这不是路线 A 吗?区别在于兜底场景里签名板是旋转之后才初始化的,jSignature 读取的是旋转后容器的实际宽高,坐标系仍然是新的 canvas 本地坐标,并没有走错误的那条路。这也是为什么初始化时机特别关键。
2.3 坐标系:为什么“先转容器、后初始化”的顺序一定不能反
jSignature 画在 canvas 上的所有坐标,都是相对于 canvas 元素本身的坐标,带不带 CSS transform 都不影响这个前提。真正影响的是 canvas 的物理尺寸:假设竖屏时 canvas 宽 375、高 600,旋转后视觉宽高变成了 600、375,但 canvas 内部像素还是 375×600,CSS 把它拉伸显示,笔迹就被等比压扁了。
常见的翻车写法是:页面加载后立刻初始化 jSignature,用户把手机转横屏时,外层容器被 CSS 旋转,canvas 跟着被拉伸,签名笔画全部变形。解决思路只有一个——在页面朝向稳定、容器尺寸变成目标横屏尺寸之后,再调用初始化。
一个可靠的判断方式是组合监听resize和orientationchange,事件触发后先加旋转类,再用setTimeout等一帧布局稳定,最后读取window.innerWidth和window.innerHeight作为签名区尺寸。如果你发现初始化后尺寸还是取到了旧值,大概率就是初始化跑到了旋转类生效之前。
这一步的原理不难,但它是横屏签名能不能成功的关键。很多项目最后线上出问题,不是 jSignature 不会用,而是 canvas 初始化的时机和容器旋转的时机错开了。第 3 章的完整代码就是围绕这个顺序来组织的。
3. 完整版源码直接跑起来:三个文件与页面代码逐段拆解
3.1 项目文件构成:三个文件放在同一目录即可
“完整版、可直接运行”的含义,是这套源码不依赖任何构建工具,也不需要在线的 CDN 链接。把下面三个文件放进同一个目录,双击 index.html 就能在浏览器里看到签名页:
- index.html:签名页面,包含全部页面结构、横竖屏样式和业务脚本
- jquery.min.js:jSignature 的依赖库,jQuery 3.x 系列压缩版即可
- jSignature.min.js:签名库本体
文件越少,出问题的环节就越少。内网环境或者离线部署的 H5 应用,用 CDN 链接通常会被拦,把这两个库文件放本地是最稳妥的做法。我一般会把这三个文件整体作为一套签名模板目录,每次接新项目复制一份,只用改页面标题和回调函数。
3.2 入口页面:横屏容器与签名区域的完整 HTML/CSS
打开 index.html 后,页面要做三件事:判断当前横竖屏、调整容器旋转状态、在合适的时机初始化签名板。下面这份 HTML 是模板的核心,直接复制就能跑。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>H5 横屏签名</title> <style> html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #f5f6fa; } #sign_page { width: 100%; height: 100%; box-sizing: border-box; background: #f5f6fa; } /* 竖屏兜底:非横屏状态下由脚本添加 */ .rotate-to-landscape { position: absolute; top: 0; left: 100%; width: 100vh; height: 100vw; transform: rotate(90deg); transform-origin: left top; background: #f5f6fa; } #sign_tips { height: 44px; line-height: 44px; font-size: 15px; text-align: center; color: #666; background: #ffffff; border-bottom: 1px solid #e5e6eb; } #sig_area { width: 100%; height: calc(100% - 110px); background: #ffffff; } #sig_area canvas { display: block; } .btn_bar { height: 66px; display: flex; align-items: center; justify-content: space-around; padding: 0 16px; box-sizing: border-box; } .btn_bar button { width: 42%; height: 42px; border: none; border-radius: 6px; font-size: 16px; } #clear_btn { background: #ffffff; color: #333333; border: 1px solid #d9d9d9; } #save_btn { background: #2b7de9; color: #ffffff; } #save_hint { margin-top: 8px; padding: 0 20px; font-size: 13px; color: #999999; } </style> </head> <body> <div id="sign_page"> <div id="sign_tips">请在横屏状态下签名,竖着拿手机时会自动旋转</div> <div id="sig_area"></div> <div class="btn_bar"> <button id="clear_btn" type="button">清空重签</button> <button id="save_btn" type="button">确认签名</button> </div> <div id="save_hint"></div> </div> <script src="jquery.min.js"></script> <script src="jSignature.min.js"></script> <script> (function () { var sigInit = false; function isLandscape() { return window.innerWidth > window.innerHeight; } function getVisualSize() { // 横屏容器宽取视口宽,高要扣除顶部提示和底部按钮区域 var height = window.innerHeight - 44 - 66 - 40; return { width: Math.floor(window.innerWidth), height: Math.floor(Math.max(height, 300)) }; } function initSigIfNeeded() { if (sigInit) return; var size = getVisualSize(); $('#sig_area').jSignature({ width: size.width, height: size.height, color: '#333333', background: '#ffffff', lineWidth: 2, UndoButton: false }); sigInit = true; } function adjustOrientation() { var page = $('#sign_page'); if (isLandscape()) { page.removeClass('rotate-to-landscape'); } else { page.addClass('rotate-to-landscape'); } // 等旋转类和浏览器布局稳定后再初始化,避免宽高取到竖屏旧值 if (!sigInit) { setTimeout(initSigIfNeeded, 350); } } $(window).on('resize orientationchange', adjustOrientation); adjustOrientation(); $('#clear_btn').on('click', function () { if (!sigInit) return; $('#sig_area').jSignature('reset'); $('#save_hint').text(''); }); $('#save_btn').on('click', function () { if (!sigInit) return; var raw = $('#sig_area').jSignature('getData', 'image/png'); var dataUrl = raw ? raw[1] : ''; if (!dataUrl || dataUrl.length < 200) { $('#save_hint').text('请先完成签名再提交'); return; } $('#save_hint').text('签名获取成功,可对接后端上传'); // 把 dataUrl 提交给后端,见第 6 章 window._lastSignDataUrl = dataUrl; }); })(); </script> </body> </html>代码有点长,但逻辑就三段:样式层负责横屏容器,脚本层负责旋转和初始化,按钮层负责清空与导出。#sig_area只做容器,canvas 由 jSignature 自动创建;width和height在初始化时传入的是视觉尺寸,jSignature 会把它们设置成 canvas 的实际像素,导出图片的宽高就和用户看到的一致。
setTimeout(initSigIfNeeded, 350)是这段代码里最不能删的地方。浏览器在orientationchange之后有一个布局收敛过程,旋转类加上之后立即读window.innerWidth,拿到的还是切换前的值。350 毫秒不是固定标准,实测在大部分浏览器上足够,如果遇到初始化取到旧值,可以把这个值再调大。
3.3 核心脚本:初始化时机、旋转兜底与导出逻辑
脚本部分是上面代码的浓缩:isLandscape()判断横竖屏,adjustOrientation()管理旋转类,initSigIfNeeded()保证签名板只初始化一次。这里有一个容易被忽略的设计——不管用户一开始是横屏还是竖屏,都要执行一次adjustOrientation(),让第一帧的容器状态和视口对齐。
清空按钮调的是jSignature('reset'),不是重新初始化。如果不调用 reset 而是销毁重建 canvas,页面会出现短暂的白屏,而且已经画过的内容也不一定被回收。reset 是 jSignature 自己维护的复位方法,会把画布清空并复位内部状态,这是推荐用法。
导出按钮用jSignature('getData', 'image/png')拿到数组,第二项是带data:image/png;base64前缀的数据。判断“签没签过名”不能只判断 canvas 是否非空,最稳的是看 dataUrl 长度,大量示例用内容长度判断会误判。实际操作中签名不会短到 200 字符以下,所以这个阈值足够安全。
提示:把
setTimeout(initSigIfNeeded, 350)里的 350 改大,是解决“初始化取到竖屏尺寸”最直接的后悔药,不用去动其他逻辑。
4. 把默认示例改成生产可用:5 个必调参数与两处定制点
4.1 初始化参数:宽高、线条、背景与撤销按钮怎么设
jSignature 的初始化配置集中在jSignature({...})里,很多项目拿到模板后直接跑能出效果,但换到具体业务就不合身,问题基本出在这几个参数上。
| 参数 | 作用 | 建议值 |
|---|---|---|
| width | canvas 像素宽度 | 取容器宽度,横屏下等于视口宽 |
| height | canvas 像素高度 | 视口高减去提示栏、按钮栏和底部留白 |
| color | 笔迹颜色 | 合同场景用 #333333,避免太浅,扫描后看不清 |
| background | 画布背景 | 需要透明底导出时设为 'transparent' |
| lineWidth | 笔迹粗细 | 2 到 3,太粗签字会糊,太细在移动端很难看 |
| UndoButton | 是否显示库自带撤销按钮 | 业务统一按钮时设 false |
| resizeProportionally | 窗口变化时签名内容是否等比缩放 | 旋转场景建议 false,避免内容被缩放 |
width 和 height 不是不填就能自动撑满,jSignature 有默认画布大小,不显式传时生成的 canvas 很可能只有 300×150,后面导出图小得没法用。正确做法是每次初始化都从当前视口读取尺寸传入。
background 参数在对接后端时经常被忽视。如果后端要求透明底的签名图片,把 background 设为'transparent'而不是把页面背景涂白,这样导出的 PNG 自带 alpha 通道,后续叠加到合同模板上才不会有白块。
4.2 导出数据格式:base64 图片与 SVG 的取舍
jSignature 支持两种业务上常用的导出形态:PNG base64 和 SVG 字符串。PNG 适合直接展示、打印,SVG 适合让后端以矢量方式存储,放大不失真。
真实项目中我的选择逻辑是这样的:后端只做存档、合同预览、客户确认,用 PNG base64 最简单,接口字段直接放一个字符串;后端要做电子凭证归档,并且可能需要对签名做法律意义上的原始保存,SVG 更合适,因为它保存的是笔画路径,理论上可以在需要的时候还原笔迹轨迹,不是合成图片。
实际调用方式:
// 导出 PNG,第二项带 data:image/png;base64 前缀 var pngData = $('#sig_area').jSignature('getData', 'image/png'); var pngDataUrl = pngData[1]; // 导出 SVG,第二项是 XML 字符串,可直接入库 var svgData = $('#sig_area').jSignature('getData', 'image/svg+xml'); var svgString = svgData[1]; // 如果有接口只要纯 base64,去掉前缀 var pureBase64 = pngDataUrl.split(',')[1];这里要注意 jSignature 的返回值结构:getData 返回的是包含两个元素的数组,第一个是 MIME 类型,第二个才是数据本体。直接拿返回值当字符串用是新手最容易踩的坑,页面会显示undefined。
4.3 两处定制点:按钮区样式与多页面复用
模板里最需要定制的是按钮区和提示文案。默认按钮是“清空重签”和“确认签名”,到了真实业务里,这两个动作往往要改成“重新签名”和“提交审批”,按钮颜色要跟随项目主题。样式都在.btn_bar里,改颜色和文案不会影响签名逻辑。
多页面复用比改样式更重要。如果项目里有多个入口需要签名,不应该复制整份 HTML,而是把初始化、旋转、导出封装成一个模块。封装思路是用一个命名空间暴露三个方法:init(selector, options)、getDataUrl()、reset(),页面只需要引入一个 JS 文件,然后调用这几个方法。
jSignature 的reset和undo是两回事,写业务时不要混。reset 清空全部笔画,undo 只撤销最近一笔。合同签署场景里“撤销上一笔”是一个高频需求,可以在工具栏加一个“撤销”按钮,调用$('#sig_area').jSignature('undo'),配合模板里的清空按钮一起用,体验会完整很多。
注意:多页面共用同一个签名脚本时,页面卸载前最好调用一次
reset,否则容器销毁时 canvas 内部事件监听可能没有完全释放,低端机上会出现页面卡顿。
4.4 横屏导出的四个边界情况
边界情况在导出时最能看出参数设得好不好:第一,初始化时宽高取到了竖屏旧值,导出图片比例会是竖向的,这时候去改初始化时机,不要改宽高;第二,签名区底部留白不够,最后几个笔画的底部被截断,导出的图片内容不完整,解决方法是把 height 计算时多留 20 到 30 像素;第三,background 没设置时,导出 PNG 的背景取决于浏览器默认值,总有人在这里翻车,所以 background 一定要显式传;第四,签名区容器有 padding 或 border 时,canvas 的实际可绘制区域比传入的 width 小,笔迹右侧会被裁掉,把容器 padding 清零再初始化。
5. jSignature 横屏签名避坑:真机上最容易翻车的 5 个问题
5.1 笔迹错位,笔画不在手指下面
现象:在旋转之后的页面上签名,画出来的笔画和手指落点差一截,尤其在竖屏旋转兜底模式下明显。
原因:初始化拿到了竖屏尺寸,或者 canvas 在旋转后没有重新获取容器实际位置,事件坐标和显示区域产生偏差。jSignature 本身不会算错,错的是容器尺寸和 canvas 尺寸不一致。
解决:严格按第 3 章的流程,先加旋转类,等布局稳定,再读取视口尺寸初始化。如果已经翻车,最直接的后悔药是把初始化挪到setTimeout之后,并把延迟从 350 改成 500 再试。
5.2 页面边界错乱,底部按钮被地址栏遮住
现象:横屏后签名区正常,但“确认签名”按钮被浏览器底部地址栏挡住,点不到。
原因:100vh在移动端浏览器上不等于可见视口高度,部分浏览器把地址栏高度也算进了视口。CSS 里height: 100%和height: calc(100% - 110px)在这种环境下的表现不一致。
解决:外层容器高度用window.innerHeight读取,不要依赖纯 vh;同时在窗口尺寸变化时重新计算签名区高度。肉眼看到的效果是按钮始终在可见区域内,这个过程需要配合visualViewport事件监听,旧浏览器不支持时就退回 resize。
5.3 导出图片模糊,放大后全是锯齿
现象:签名图片放在 PC 上放大看,线条边缘一片锯齿,合同上完全不能看。
原因:canvas 的物理像素等于初始化时传入的 width 和 height,如果传入的视觉尺寸小于设备物理像素,导出图自然是低分辨率的。高分屏上 375 像素宽的画布,实际显示需要约 750 物理像素。
解决:初始化时把尺寸乘以window.devicePixelRatio,canvas 的宽高用物理像素,CSS 再用视觉尺寸显示。这样笔迹反而更细腻。注意 canvas 内部尺寸变大了,jSignature 的笔迹粗细 lineWidth 也要跟着适当放大,否则 2 像素的线在放大后的坐标系里看起来偏细。这是一个需要联调的点。
5.4 旋转屏幕后签名内容消失
现象:用户横屏签完名,把手机转回竖屏再转回横屏,画好的签名没了,或者只剩半截。
原因:orientationchange 触发时页面重排,canvas 被重绘或尺寸被重置。jSignature 的内容存储在内部状态里,但 canvas 恢复后未必会自动恢复到对应状态。
解决:在旋转前先把当前签名用 SVG 暂存,旋转完成后重新 setData 回去。代码模式我的做法是在 orientationchange 里保存,在 resize 里恢复:
var backup = null; $(window).on('orientationchange', function () { backup = sigInit ? $('#sig_area').jSignature('getData', 'image/svg+xml') : null; }); $(window).on('resize', function () { adjustOrientation(); if (backup) { $('#sig_area').jSignature('setData', backup); backup = null; } });注意 backup 只能保留一次,恢复后要清空,否则连续旋转会重复恢复旧数据。
5.5 Android 退回竖屏后页面还是横的
现象:用户竖着拿手机操作,签名页被 CSS 旋转成横屏兜底,退出页面再进,页面还是横的。
原因:旋转类在首次初始化后没有跟随真实横竖屏状态更新,或者浏览器pageshow触发时缓存了上一个横屏页面。
解决:在pageshow和visibilitychange里重新调用adjustOrientation(),重新判断宽高比。另外,部分移动浏览器确实做不到脚本强制横屏,需要用户在系统层关闭竖排锁定,这不是前端代码能完全控制的,项目接入时要提前跟业务方说清楚。
6. 让签名数据在业务里真正跑起来:后端对接与验证清单
签名页跑通之后,真正决定上线的往往是怎么把数据交到后端。两种最常见的方式:直接提交带前缀的 dataUrl 字符串,后端按文本字段存储,适合简单场景;用 FormData 上传 Blob 文件,后端走文件上传接口,适合对接现有存储服务。第 3 章模板在按钮回调里拿到window._lastSignDataUrl,转成 Blob 的方式很固定:
function dataUrlToBlob(dataUrl) { var arr = dataUrl.split(','); var mime = arr[0].match(/:(.*?);/)[1]; var bstr = atob(arr[1]); var n = bstr.length; var bytes = new Uint8Array(n); while (n--) { bytes[n] = bstr.charCodeAt(n); } return new Blob([bytes], { type: mime }); }这是常见做法,浏览器原生支持,不需要额外引入 FileReader。提交时把 Blob 放进 FormData:
var fd = new FormData(); fd.append('signature', dataUrlToBlob(window._lastSignDataUrl), 'signature.png'); $.ajax({ url: '/upload/sign', method: 'POST', data: fd, processData: false, contentType: false });验证这个方案时,不要只在开发者工具的设备模拟里转一圈就完事。我一般按这份清单过一遍:真机横屏直接进页面,签名、撤销、清空、导出是否顺畅;竖屏进入页面自动旋转后,初始化尺寸是否取到横屏值;转屏多次后签名内容是否还在;导出图片在 PC 上看是否清晰;后端拿到图片后底边是否截断。五项都过,模板就可以固化成团队标准。
现在接新项目,我不再每次重写签名逻辑,而是先把这份可直接运行的源码复制过去,改参数、换按钮颜色、接接口。省下来的时间都花在真机验证上,这才是整个方案里真正值钱的部分。希望帮到你。
本文还有配套的精品资源,点击获取