尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Vue3电子签名组件实战:从Canvas原理到前后端对接

Vue3电子签名组件实战:从Canvas原理到前后端对接 电子签名这个需求在Vue项目里出现的频率比我预想的高得多。最早我是在一个审批系统里接到这个任务当时第一反应是直接找一个开源的签名组件但搜来搜去要么是React版要么是jQuery时代的老古董勉强能用但问题一堆移动端画线偏移、导出图片模糊、组件卸载报错。后来迫不得已用vue3-signature这个思路自己造了一个轮子把整个流程彻底跑通之后才发现里面藏着不少值得记录的细节。这篇文章就把我从组件选型、Canvas签名原理、前后端数据打通到踩坑排查的完整经验整理出来给正在被电子签名折磨的朋友一个参考。1. 为什么需要一个专门的电子签名组件1.1 真实业务场景谁的桌面需要一支虚拟笔电子签名不只是画个名字那么肤浅。实际业务里合同签署、业务确认、审批流、物流签收、医疗知情同意书到处都需要。这类场景的共同特点是用户得留下一个可追溯的、不可抵赖的书面确认痕迹。键盘打字可以伪造但手写轨迹在一定程度上有身份识别的辅助价值至少从业务流程上讲它更接近传统的白纸黑字。我记得那个审批系统的需求大致是这样的用户在PC端浏览器里用鼠标签名在手机端用触屏或手写笔签名签完以后生成的图片要跟随表单一起提交给后端后端要能存储、回显、打印。当时第一版方案找的是一个npm包叫vue-signature-pad但那是基于Vue 2的拿到Vue 3项目里直接用会在插件初始化阶段报错。后来又试了几个要么体积太大要么API设计得很别扭传参数都搞不清楚。最后我决定基于canvas自己封装一个满足vue3-signature思路的组件既可控又轻量。如果你也在纠结到底该不该自己写我的建议是只要你有时间去调试自己写是值得的。因为电子签名组件最大的坑不是画一条线而是围绕签名产生的兼容性、清晰度、数据格式、交互体验这些外围问题。第三方组件往往封装得太死出问题之后你根本不知道它内部做了什么排查起来非常痛苦。自己写你对每一根线条、每一个像素都有完全的掌控力。1.2 为什么不用插件自己写要绕过哪些坑市面上已有的签名库不少但仔细深挖你会发现它们大多存在三个层面的问题。第一是维护状态。很多签名库的仓库已经几年不更新了而浏览器API、Vue版本、打包工具都在飞速变化。一个不维护的库今天能跑不代表一年后还能跑。第二是兼容性设计。签名场景横跨PC和移动端不同设备的输入方式差异极大鼠标是绝对坐标触摸屏是相对位移手写笔还有压力参数。很多库只处理了mouseEvent和touchEvent的简单映射在个别浏览器里会触发重复事件、坐标偏移等问题。第三是定制自由度。业务方几乎一定会提一些奇怪的需求比如签名颜色要跟随企业主题色变化、要能撤回上一步、要支持用户选择不同粗细的笔迹、要能自动生成透明背景图供合同叠印。这些需求在通用库里面不一定有配置项但自己封装组件的时候这些都可以作为核心能力直接设计进去。所以结论是如果你的项目只需要一个最基础的手写板任何现成库都行。但如果你需要把签名作为功能模块长期维护自己写一定是最好的答案。接下来我就把这套自研组件的完整实现过程从画布原理、API设计、前后端对接到高清适配和问题排查一条龙讲清楚。2. 核心原理签名组件到底在做什么2.1 Canvas签名从鼠标画线到坐标轨迹很多人对Canvas的认知停留在画图表、画游戏画面上其实签名的本质也是Canvas绘图只不过它不是一次性画完的而是根据用户的输入行为动态地绘制轨迹。你可以这么理解用户的手指或鼠标在画布上移动时系统记录下途经的每一个坐标点然后把这些点用线连起来就形成了一条连续的笔迹。具体到代码层面我们需要做三件事监听输入事件、记录坐标、重绘路径。这里有一个非常重要的设计决策事件监听应该绑定在canvas元素上还是绑定在document上我建议绑定在canvas元素上并且需要对不同的输入设备做统一处理。现代浏览器提供了一套Pointer Events指针事件可以同时覆盖鼠标、触摸、手写笔让兼容变得异常简单。核心流程是这样的用户按下画布时pointerdown我们记录起始点坐标并设置一个标记表示正在绘画。用户移动时pointermove我们持续读取当前坐标并从上一次的点用直线连接过来。用户抬手或移出画布pointerup / pointercancel我们清除绘画标记。这个从上一个点到当前点做线段连接的策略听上去简单但它是整个签名流畅度、准确度的基础。如果每次都从头开始重新绘制整条路径性能会非常差如果只绘制新点而不连接线条就会断成一堆孤立的点。所以正确的做法是维护一个当前绘制路径的数据结构。这里我给出一个最小的绘画逻辑伪代码// 记录所有签名坐标点 let points []; let drawing false; // 事件回调 function onPointerDown(e) { drawing true; const point getCanvasPoint(e); points [point]; ctx.beginPath(); ctx.moveTo(point.x, point.y); } function onPointerMove(e) { if (!drawing) return; const point getCanvasPoint(e); points.push(point); ctx.lineTo(point.x, point.y); ctx.stroke(); } function onPointerUp() { drawing false; }有经验的读者看到这里可能会问每次move都直接lineTostroke会不会导致笔画之间出现接缝确实会。当你快速绘图时如果两次stroke之间没有平滑过渡线宽在两段路径的交界处会出现折痕。常见的优化方案是使用quadraticCurveTo或者计算中点来实现贝塞尔曲线插值。但如果在移动端高频采样下你直接用直线连接每个点效果也完全可以接受因为采样点足够密集肉眼几乎看不出折角。最关键的是保证每个移动事件都能及时处理不然就会出现断笔。2.2 让签名清晰的三个关键细节高清屏、笔画压力、防手抖画线基础逻辑有了但实际跑起来你会发现导出图片一片模糊笔画粗细不均匀手机上一笔画下去只能在初始点留下一个斑点。这些问题如果不解决组件完全没法上线。第一个致命问题是高清屏适配。很多人没意识到CSS像素和设备物理像素不是一回事。一块视口宽度375px的手机屏幕物理像素可能是750px甚至更多。如果你创建的canvas元素宽度是375px高度是200px那么在视网膜屏幕上它只有375个物理像素宽度浏览器会把它拉伸到750物理像素于是一旦缩放文字、笔迹边缘全部虚化。解决办法是让canvas的物理像素尺寸对齐设备像素比devicePixelRatio。具体做法是创建一个比CSS尺寸放大dpr倍的画布然后用ctx.scale(dpr, dpr)把坐标系同步放大。这样画出来的线条既保持了CSS坐标的直观性又能拿到物理分辨率的清晰度。还有一个很多人忽略的细节导出图片时如果你只是把canvas内容导出为base64图片的实际分辨率取决于canvas物理尺寸。如果你想拿到更高分辨率的图片可以在导出前临时把canvas放大再调用toDataURL导出后再复原。我在组件里做了一个scale参数默认导出2倍图既保证清晰度又不会让文件大得太离谱。第二个问题是笔画压力。手写笔和部分手机的触摸屏会提供pointer事件的pressure属性范围是0到1。你可以用这个值动态调整线宽。比如默认线宽是3px压力是0.8时线宽变成3.5px压力是0.2时线宽变成2px。这会让签名看起来更自然、有笔锋更像真人手写。不过鼠标事件里pressure永远是1或0所以这个逻辑必须兼容非压力输入设备。第三个问题是防抖。实际使用中用户签名时手会发抖笔迹在快速移动时会出现锯齿或震动纹理。一个可行的优化是点集采样 平均滤波我们把最近几帧的坐标存起来计算平均位置后再真正绘制。这样即使输入坐标有微小抖动绘制出来的线条也会平滑很多。代价是手写延迟会稍微增加但3-5个点的窗口大小对签名来说没感知。2.3 组件该对外暴露哪些能力API设计思路自研组件最大的优势就是可以自定义API。我在设计vue3-signature组件时重点考虑了以下几个方面这些也是你在使用任何签名组件时需要留意的关键点核心能力签名行为。至少要覆盖开始签名、清空画布、撤销上一步、导出图片、判断是否为空、替换背景色、批量设置样式。数据能力签名内容。签名最终要变成什么数据最常见的是图片base64。其次是原始坐标点数组。坐标点数据的好处是可以做回放动画、可以重新渲染到不同尺寸的画布上、可以做签名真实性校验。我在组件里同时保留了getSignature()返回图片base64和getPoints()返回坐标点两个方法给业务层充分的选择空间。视觉能力动态配置。常见的可配置项有线条颜色、线条宽度、背景颜色透明或白色、canvas尺寸、是否锁定签名编辑后不允许修改。我通过一个configprop统一传入并且使用watch监听变化一旦用户修改配置就重新渲染画布。事件能力交互反馈。组件需要对外发出开始签名start、正在签名drawing、结束签名end、画布被清空clear、已撤销undo等事件。业务层拿到这些事件可以做引导提示、按钮置灰、自动保存等操作。用一个表格总结一下我最终设计的API方法名参数功能说明典型用途init()无初始化画布与事件监听组件挂载后调用clear()无清空画布所有内容用户点击重签按钮undo()无撤销上一笔用户画错时撤回getSignature(){ type: image/png, scale: 2 }导出签名图片base64提交给后端getPoints()无导出所有坐标点数据回放、重新渲染、校验isEmpty()无判断画布是否为空控制提交按钮状态setConfig()config对象动态修改样式配置切换主题色时更新3. 实操过程从零搭建一个可用的签名板3.1 先解决画得上初始化画布与事件绑定直接上完整代码。我用Vue 3的组合式API来写首先创建一个SignatureBoard.vue组件。template div classsignature-wrapper :style{ width: width, height: height } canvas refcanvasRef classsignature-canvas pointerdownhandlePointerDown pointermovehandlePointerMove pointeruphandlePointerUp pointercancelhandlePointerUp /canvas /div /template script setup import { ref, reactive, onMounted, onBeforeUnmount, watch } from vue; const props defineProps({ width: { type: String, default: 100% }, height: { type: String, default: 200px }, lineColor: { type: String, default: #1f2937 }, lineWidth: { type: Number, default: 3 }, bgColor: { type: String, default: transparent }, lock: { type: Boolean, default: false } }); const emit defineEmits([start, drawing, end, clear, undo, update]); const canvasRef ref(null); let ctx null; let isDrawing false; let lastPoint null; let undoStack []; const state reactive({ points: [], hasSigned: false }); function init() { const canvas canvasRef.value; if (!canvas) return; // 高清适配 const dpr window.devicePixelRatio || 1; const rect canvas.getBoundingClientRect(); canvas.width rect.width * dpr; canvas.height rect.height * dpr; ctx canvas.getContext(2d); ctx.scale(dpr, dpr); ctx.lineWidth props.lineWidth; ctx.strokeStyle props.lineColor; ctx.lineCap round; ctx.lineJoin round; // 背景 if (props.bgColor ! transparent) { ctx.fillStyle props.bgColor; ctx.fillRect(0, 0, rect.width, rect.height); } redrawFromStack(); }这段代码里有几个细节要强调。第一getBoundingClientRect()必须在初始化时调用并且要在canvas真正渲染到DOM之后否则拿到的宽高是0。所以init()最好在onMounted里执行必要时再套一层nextTick。第二初始化时我们做了ctx.scale(dpr, dpr)这之后所有的坐标都沿用CSS像素坐标不用再跟dpr打交道非常方便。如果你把scale放在了绘制过程中坐标就会错乱这一点要切记。第三lineCap和lineJoin必须同时设置为round否则快速画线时线条首尾和拐角处会出现突兀的平角或尖刺观感差很多。事件绑定层面我用了Pointer Events它一次搞定鼠标、触摸、笔输入function getCanvasPoint(e) { const canvas canvasRef.value; const rect canvas.getBoundingClientRect(); return { x: e.clientX - rect.left, y: e.clientY - rect.top }; } function handlePointerDown(e) { if (props.lock) return; isDrawing true; const point getCanvasPoint(e); lastPoint point; // 保存当前画布内容到撤销栈 const snapshot ctx.getImageData(0, 0, canvasRef.value.width, canvasRef.value.height); undoStack.push(snapshot); emit(start); } function handlePointerMove(e) { if (!isDrawing) return; const point getCanvasPoint(e); ctx.beginPath(); ctx.moveTo(lastPoint.x, lastPoint.y); ctx.lineTo(point.x, point.y); ctx.stroke(); lastPoint point; state.hasSigned true; emit(drawing); } function handlePointerUp() { if (!isDrawing) return; isDrawing false; emit(end); emit(update); }3.2 再解决画得美高清适配与笔触优化基础画线是通了但如果你把上面代码直接拿到项目里有几个美中不足很快会出现。第一快速画线时线条会断截。原因很简单pointermove事件的触发频率是有限的如果手指移动速度特别快两次事件之间的坐标距离会很大一条十几像素的长线段画出来就不像连续笔迹。解决思路是插值把上一坐标和当前坐标之间的距离拆成若干小段逐段绘制。function drawLine(from, to) { const distance Math.hypot(to.x - from.x, to.y - from.y); // 如果距离小于1没必要插值 if (distance 1) return; const steps Math.ceil(distance / 2); // 每2px插一个点 for (let i 1; i steps; i) { const x from.x (to.x - from.x) * (i / steps); const y from.y (to.y - from.y) * (i / steps); ctx.lineTo(x, y); } ctx.stroke(); }这个2px插值粒度在实际使用中效果非常好既保证连续性又不会因为插值过密导致性能下降。第二签名颜色/宽度和背景色没跟随配置变化。通过watch监听props变化然后重新设置ctx.strokeStyle、ctx.lineWidth必要时重绘整个画布。这里的坑在于修改ctx.strokeStyle只影响新绘制的笔画不会改变已经画好的内容所以如果业务上要求改色后已画内容也变化就必须在修改配置后重新绘制全部点数据。watch(() props.lineColor, (val) { if (ctx) ctx.strokeStyle val; });第三撤销功能存在内存隐患。我上面用getImageData保存画布快照这个API在画布尺寸较大的时候是非常占内存的。一个200px高度、2倍dpr、宽度400px的画布一次快照就是几十万字节的数据堆几百个快照相册内存很快就爆了。所以我强制限制撤销栈长度最多30步超出就弹掉最老的。30步对签名场景来说绰绰有余毕竟用户不可能连续画几十笔往上撤。3.3 签名要能存下来图片导出与前后端对接签名画好了接下来最关键的一步就是把它变成可提交的数据。后端要的通常是一张图片或者一个base64字符串前端要做的就是导出、压缩、传输。我导出的方法设计如下function getSignature(options {}) { const { type image/png, scale 2, quality 0.9 } options; const canvas canvasRef.value; const rect canvas.getBoundingClientRect(); // 临时放大canvas导出高清图 const exportCanvas document.createElement(canvas); exportCanvas.width rect.width * scale; exportCanvas.height rect.height * scale; const exportCtx exportCanvas.getContext(2d); if (props.bgColor ! transparent) { exportCtx.fillStyle props.bgColor; exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height); } exportCtx.drawImage(canvas, 0, 0, exportCanvas.width, exportCanvas.height); const dataUrl exportCanvas.toDataURL(type, quality); return dataUrl; }这里有几个关键决策要解释一下。为什么临时创建新的canvas而不是直接把原canvas的尺寸调整成放大版因为直接调整原canvas的width属性会清空画布你签名还没提交呢画布先变白了用户体验稀碎。临时创建导出canvas原画布纹丝不动。为什么用drawImage(canvas, 0, 0, exportCanvas.width, exportCanvas.height)而不是直接调用原canvas的toDataURL因为原canvas的物理像素只有rect.width * dpr你强行导出到更大尺寸时放大过程会模糊。但如果我们以scale 2为目标放大同时drawImage源图像本身的分辨率只有1倍dpr放大两倍后还是会糊。所以真正好的做法是让源canvas的物理分辨率也足够高。我当前的组件里canvas初始化的分辨率就是rect * dpr也就是物理分辨率对齐屏幕。如果导出时要2倍是在此基础上再放大的。对于绝大多数合同、审批场景这个清晰度已经远超打印需求了。如果你把scale传到4倍甚至更高图片会非常清晰但同时base64字符串会变得很长。一个经验值白色背景的签名PNG格式下2倍清晰度一般文件大小在10KB到50KB之间如果长宽都不夸张base64也就几万字符走HTTP接口完全没问题。导出之后传给后端这个动作我建议放在业务页面里做不要在组件内部直接发请求。组件只负责导出数据传输是业务层的事。这样组件才能保持纯粹的可复用性。3.4 顺手把界面做好清空、撤销、换背景签名组件不只是画布本身还需要一配套的操作交互。我见过很多项目只是提供一个画板然后让用户自行想办法清空重画体验非常糟糕。一个合格的签名板至少要有三个操作按钮清空、撤销、确认提交。清空逻辑相对简单直接把整个画布填成背景色同时清空撤销栈和坐标点数组。function clear() { const canvas canvasRef.value; const rect canvas.getBoundingClientRect(); ctx.clearRect(0, 0, canvas.width, canvas.height); if (props.bgColor ! transparent) { ctx.fillStyle props.bgColor; ctx.fillRect(0, 0, canvas.width, canvas.height); } undoStack []; state.points []; state.hasSigned false; emit(clear); emit(update); }撤销功能需要结合快照栈来实现。每次pointerdown开始时我们保存一次快照撤销就是恢复栈顶快照。但要注意恢复快照时要同时恢复坐标点数组否则接下来继续画线时新笔画和旧内容之间会出现断点坐标。function undo() { if (undoStack.length 0) return; const snapshot undoStack.pop(); ctx.putImageData(snapshot, 0, 0); state.hasSigned undoStack.length 0; if (state.points.length 0) state.points.pop(); emit(undo); emit(update); }另外换背景也算一个很实用的能力。默认透明背景适合叠印在合同上。但有的业务方希望背景是白色方便文件存储。我通过bgColor配置项处理组件内部在初始化、清空、导出时都会读取当前背景色保证一致性。4. 前后端打通签名数据怎么传到服务端4.1 前端只做三件事采集、压缩、传递签名数据的生命周期在前端其实就三步采集把Canvas变成base64、压缩控制尺寸和格式、传递通过接口发送。采集我们已经做了就是getSignature()。压缩方面主要有三种手段第一是控制导出分辨率。默认2倍足够不需要盲目追求4K级签名图。第二是选择图片格式。如果签名没有颜色要求建议导出image/png因为PNG支持透明背景如果签名需要叠印到红色印章、深色合同背景上透明背景的PNG是唯一选择。如果你想要更小的体积可以用image/jpeg但JPEG不支持透明背景而且有压缩伪影合同场景不推荐。第三是base64裁剪。如果你只是想传原始坐标数据也就是getPoints()可以用JSON序列化后直接发送体积通常比图片小得多缺点是后端要用Canvas或其他绘图库重新渲染比较麻烦。传到服务端时我走的是常见做法const signatureData signatureRef.value.getSignature({ type: image/png, scale: 2 }); const body { contractId: xxx, signerName: 张三, signatureBase64: signatureData }; fetch(/api/contract/sign, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(body) });有些后端要求纯base64而不带data:image/png;base64,前缀这时前端要做一个简单的字符串截取const pureBase64 signatureData.split(,)[1] || signatureData;这个细节如果没对接好后端解析base64为二进制时会直接报错而且错误信息往往让你一头雾水。4.2 后端接收与存储建议后端拿到base64字符串之后一般有两种处理路径。第一种直接存base64。这种方案最简单适合签名信息量小、数据库存储体量可控的场景。在MySQL里可以用LONGTEXT或MEDIUMTEXT字段存放也能直接外键绑定到业务记录。缺点是每次读取都要做base64解码对接存储时会多一层开销。第二种把base64还原成图片文件存储到对象存储或文件服务器数据库里只存一个URL。这是生产环境最稳妥的做法。签名图片是静态资源放CDN上访问快数据库负担小后续打印、下载都不卡。后端还原图片的代码以Node.js为例大概是这样的const base64Data req.body.signatureBase64.replace(/^data:image\/\w;base64,/, ); const buffer Buffer.from(base64Data, base64); // 保存到本地或上传云存储 fs.writeFileSync(uploads/signature_${Date.now()}.png, buffer);这里要注意base64字符串里可能包含换行符有些框架在解析JSON时会自动去掉有些不会。后端在还原之前一定要做一次replace(/\s/g, )不然还原出来的文件字节数比真实数据多图片可能损坏。存储的位置上我建议给每条业务记录都留一个signatureUrl字段并且签名图片命名时带上业务ID比如signature_contract_20240101_001.png。这样即使文件散落在存储桶里也能快速定位到具体是哪笔业务的签名。4.3 签名回显从base64/URL还原到页面签名提交之后往往还需要在详情页、审批记录里把当时签名回显出来。如果后端存的是URL直接用img :srcsignatureUrl就可以。如果存的是base64有两种做法第一种是直接把base64塞进img标签img :srcdata:image/png;base64, record.signatureBase64 alt签名 /这种做法简单但base64字段可能很长塞进Vue响应式数据里也不用担心性能问题因为只是拼接字符串没有额外的解析压力。要注意的是过长的base64在部分浏览器地址栏或传参时会被截断但作为img的src属性现代浏览器处理起来没什么问题。第二种是前端把base64转成Blob对象再通过URL.createObjectURL生成临时链接。这个方法适合需要下载、打印、放大查看的场景function base64ToBlob(base64, mimeType image/png) { const byteCharacters atob(base64); const byteArrays []; for (let offset 0; offset byteCharacters.length; offset 512) { const slice byteCharacters.slice(offset, offset 512); const byteNumbers new Array(slice.length); for (let i 0; i slice.length; i) { byteNumbers[i] slice.charCodeAt(i); } byteArrays.push(new Uint8Array(byteNumbers)); } return new Blob(byteArrays, { type: mimeType }); }两种方式我都用过强烈建议用第二种。因为base64字符串作为img的src时浏览器每次都要解码一遍如果列表页有几十条记录一次性加载几十个大base64字段页面渲染会出现显著卡顿。而URL.createObjectURL生成的blob URL是一次性的本地引用加载速度飞快内存释放由系统管理只要记得在组件卸载时调用URL.revokeObjectURL清理即可。5. 常见问题与排查技巧实录5.1 签名错位、偏斜坐标系不对的典型症状这是签名组件最常见的坑没有之一。症状是在页面上画画时光标或手指的落点和笔迹的实际位置不一致越靠近画布边缘偏得越远。根本原因只有一个事件坐标没有减去canvas元素自身的offsetTop/offsetLeft/rect位置。很多人直接从e.clientX和e.clientY拿坐标那是相对于浏览器视口的坐标而canvas绘制需要的是相对于canvas元素本身的坐标。两者只有在canvas恰好位于视口左上角时才相等。我刚才代码里的getCanvasPoint就是解决这个问题的x: e.clientX - rect.left, y: e.clientY - rect.top还有一个次级坑如果canvas元素被CSS缩放了比如设置了transform: scale(0.5)那么事件坐标也要做对应的缩放处理。不过正常业务里不会去缩放签名画布本身所以遇到这个坑的几率不大但如果你的画布在弹出框、抽屉、丝滑变形的动画里就要务必小心。我的经验是尽量让canvas元素在弹出层显示时尺寸稳定不要给它加动画过渡否则坐标会在动画过程中变化签名精度全毁。5.2 导出图片模糊devicePixelRatio是罪魁祸首如果你发现导出的签名图片在手机上、打印机上看上去有毛边或者帽子皱纹一样模糊十有八九是没做高清适配。现代Retina屏、高分屏设备的devicePixelRatio一般是2或3如果你只按CSS像素创建canvas实际物理像素只有像素值的一半那么图片必然糊。解决办法就是我前面提到的创建canvas时把宽高设置为rect.width * dpr并且紧跟着ctx.scale(dpr, dpr)。这样所有绘制命令都在CSS像素坐标里操作但画布的物理像素已经翻倍导出图片自然清晰锐利。另外一个经验是在导出时临时用scale参数进行放大比在原canvas上直接调整尺寸更安全。原canvas的宽高一变整个画布内容就会被清空这个坑我不希望你再踩一遍。5.3 触摸屏只能画一个点事件穿透与监听有一个让人抓狂的问题在手机上签名手指按下去只留下一个小点手指移动也没有线条出现。我排查了半天最后发现问题出在事件绑定上。如果你用的是mouse事件mousedownmousemove在触摸屏上是不会触发的。如果你用touch事件touchstarttouchmove需要额外注意触摸事件的坐标对象在e.touches[0]里而不是e.clientX。如果你用Pointer Events虽然统一了鼠标和触摸但触摸屏上的pointermove可能在元素离开触摸范围后停止派发。我建议统一使用Pointer Events。在使用Pointer Events时还有一个很隐蔽的问题某些Android浏览器在触摸开始后如果事件绑定在canvas元素上而手指移动出了canvas元素范围后续的move事件可能不再派发。解决办法是在handlePointerDown之后通过setPointerCapture把指针事件捕获到canvas元素上canvas.setPointerCapture(e.pointerId);这样无论手指滑到哪里事件都会继续交给canvas处理不会再出现画着画着断线的问题。这个是移动端签名组件能稳定运行的真正秘密。你可以在handlePointerUp时再调用releasePointerCapture释放捕获否则可能会影响页面其他元素的交互。5.4 组件卸载后仍报错内存清理与事件解绑Vue组件销毁时如果没有解绑事件监听或者有未清理的定时器、未回收的临时canvas轻则内存泄漏重则导致Failed to execute getImageData on CanvasRenderingContext2D这类报错。我在onMounted里绑定了事件就要在onBeforeUnmount里解绑或者直接使用Vue的事件系统自动监听。如果使用了setPointerCapture卸载时要记得释放onBeforeUnmount(() { const canvas canvasRef.value; if (canvas) { canvas.releasePointerCapture(canvas.activePointerId); } // 清空引用帮助GC回收 ctx null; canvasRef.value null; });另外如果你在组件里创建了exportCanvas这类临时canvas也要在卸载时调用exportCanvas.width 0或者直接置null不然canvas相关的内存可能不会被垃圾回收器及时清理。上面这些坑我几乎都真金白银地踩过一遍。尤其是在移动端高清屏适配和事件捕获这两个问题上之前的第三方库更是坑得我一度怀疑人生。自研组件之后这些问题都变成了一行行自己可控的代码调试起来踏实得多。如果你也想在项目里快速落地一个vue3-signature我强烈建议不要直接复制粘贴一段代码就完事最好把组件内部的结构、数据流、导出逻辑都走一遍这样后面需求一升级你才能顺手接住。签名这个功能说小不小说大不大但把它做好了你的业务系统会给人一种非常专业、可靠的印象。
返回列表