简介这是一份面向微信小程序开发者、企业技术团队及对电子签约场景感兴趣的进阶学习者的实战代码压缩包聚焦在线电子合同签署组件的完整实现帮助解决纸质合同签署效率低、身份认证与存证难等痛点。包内共10个文件以js逻辑脚本、wxml页面结构、wxss样式表、json配置及一份md说明文档为主整体约12KB结构紧凑覆盖合同展示、数字签名、存证对接与合同管理等核心模块。已有86人学习下载适合希望快速理解小程序原生组件开发与第三方电子合同API对接思路的读者。通过阅读源码与说明可掌握用户身份认证、签名板组件封装、合同数据加密传输及测试排错等关键实现并借鉴其目录组织方式将签约能力作为独立模块集成到自有小程序中提升业务效率与用户体验。1. 电子合同签署组件从散落文件到可复用代码压缩包很多做微信小程序的开发者都遇到过这种需求业务方要在小程序里加一个“签合同”的环节用户看完条款、手写签名、生成一份带签名的 PDF 或图片最后双方各留一份。听起来简单真动手时才发现坑不少——签名板在小程序里的触摸事件和 H5 完全不是一回事canvas 导出图片的清晰度、合同模板的动态字段填充、签署完成后的文件存储与回传每一项都能耗掉一两天。这份电子合同签署组件开发实战的代码压缩包就是把这些环节打包成一个可复用的组件省去从零搭轮子的时间。它适合正在做小程序端签约功能的开发者也适合想了解小程序 canvas 签名与文件生成完整链路的同学。下面按“组件结构 → 签名板实现 → 合同生成 → 避坑 → 进阶”的顺序拆开讲。2. 组件目录结构与核心模块拆解拿到代码压缩包后先别急着往项目里塞。花十分钟把目录结构看清楚后面改起来会顺手很多。这个组件的组织方式遵循微信小程序自定义组件的标准规范同时把签名、合同渲染、文件导出拆成了独立模块方便按需引入。2.1 目录树与文件职责解压后的核心目录大致如下不同版本可能略有差异以实际压缩包为准components/ contract-sign/ index.js // 组件入口暴露属性和方法 index.json // 组件声明 index.wxml // 签名板与合同预览的布局 index.wxss // 样式含签名板尺寸适配 utils/ canvas-sign.js // 签名轨迹采集与重绘 contract-gen.js // 合同模板字段填充与导出 file-helper.js // 临时文件转存、base64 处理 assets/ template.png // 默认合同底图可替换index.js是组件的“总控”它对外暴露了open、clear、save三个主要方法分别对应打开签名面板、清空重签、保存签署结果。canvas-sign.js负责处理touchstart、touchmove、touchend三个事件把触摸点转成 canvas 路径。contract-gen.js做的是把业务数据比如甲方名称、金额、日期填进合同模板的指定坐标位置。file-helper.js则处理小程序里比较烦人的临时文件生命周期问题。提示如果你只需要签名功能可以只引入canvas-sign.js和对应的 wxml 片段不必整个组件搬进去。2.2 组件属性与事件设计组件对外暴露的属性决定了它的复用程度。这个组件设计了几个关键属性属性名类型默认值说明contractDataObject{}合同动态字段如甲方、乙方、金额templateSrcString合同底图路径支持本地或临时文件penColorString#000000签名笔迹颜色penWidthNumber4笔迹粗细单位 pxexportFormatStringpng导出格式可选 png 或 jpg事件方面组件在签署完成时会触发signsuccess回传一个包含临时文件路径和签署时间戳的对象。业务层拿到这个对象后可以上传到自己的服务器或存入云存储。这种设计的好处是组件不关心你用什么后端只负责把文件生成出来。常见做法是在页面的 json 里注册组件然后在 wxml 里写contract-sign contractData{{data}} bind:signsuccessonSignSuccess /。onSignSuccess里拿到e.detail.tempFilePath再走wx.uploadFile上传。整个链路清晰没有多余的耦合。2.3 初始化与参数配置在页面加载时你需要把合同数据准备好。假设合同里需要填甲方名称、乙方名称和签署日期数据结构可以这样组织// pages/contract/contract.js Page({ data: { contractData: { partyA: 某公司, partyB: 某开发者, amount: 10000, date: 2025-01-01 } }, onLoad() { // 如果合同底图是网络图片先下载到本地临时文件 wx.downloadFile({ url: https://example.com/template.png, // 替换为实际模板地址 success: (res) { this.setData({ templateSrc: res.tempFilePath }); } }); }, onSignSuccess(e) { const { tempFilePath, signTime } e.detail; // 这里做上传或本地保存 console.log(签署完成, tempFilePath, signTime); } });这段代码做了两件事一是把合同动态字段挂到data上组件内部会读取并渲染二是提前把合同底图下载到本地避免 canvas 绘制时因网络图片跨域或加载失败导致白屏。参数contractData的字段名需要和contract-gen.js里的占位符一一对应否则填充位置会错乱。我一般会在contract-gen.js顶部维护一个坐标映射表改字段时只动那一处。3. 签名板实现canvas 触摸轨迹与高清导出签名板是整个组件里最容易翻车的部分。小程序 canvas 的 API 在基础库 2.9.0 之后分成了旧版wx.createCanvasContext和新版Canvas 2D两者在导出图片时的表现差异很大。这个组件默认走的是 Canvas 2D 方案因为它在高清屏上的清晰度明显更好。3.1 触摸事件采集与轨迹绘制签名板的核心逻辑是监听触摸事件把每个触摸点的坐标记录下来然后用lineTo连成路径。关键代码如下// utils/canvas-sign.js class SignCanvas { constructor(canvasId, pageContext) { this.canvasId canvasId; this.ctx null; this.points []; this.isDrawing false; this.init(pageContext); } init(pageContext) { const query pageContext.createSelectorQuery(); query.select(#${this.canvasId}) .fields({ node: true, size: true }) .exec((res) { const canvas res[0].node; const ctx canvas.getContext(2d); // 处理高清屏按设备像素比放大 canvas const dpr wx.getSystemInfoSync().pixelRatio; canvas.width res[0].width * dpr; canvas.height res[0].height * dpr; ctx.scale(dpr, dpr); ctx.lineWidth 4; ctx.lineCap round; ctx.lineJoin round; ctx.strokeStyle #000000; this.ctx ctx; this.canvas canvas; }); } touchStart(e) { this.isDrawing true; const { x, y } e.touches[0]; this.points [{ x, y }]; this.ctx.beginPath(); this.ctx.moveTo(x, y); } touchMove(e) { if (!this.isDrawing) return; const { x, y } e.touches[0]; this.points.push({ x, y }); this.ctx.lineTo(x, y); this.ctx.stroke(); } touchEnd() { this.isDrawing false; this.ctx.closePath(); } }这里有几个参数值得注意。dpr是设备像素比iPhone 上通常是 2 或 3如果不做这个缩放导出的签名图片会模糊得像马赛克。lineCap和lineJoin设为round是为了让笔迹拐角圆润否则快速书写时会出现尖锐的折角。lineWidth设为 4 是经验值太细了在手机上看起来费劲太粗了签名容易糊成一团。3.2 导出图片与清晰度控制签名完成后需要把 canvas 内容导出成图片。Canvas 2D 方案下用canvas.toDataURL或wx.canvasToTempFilePath// 导出签名图片 exportSign() { return new Promise((resolve, reject) { wx.canvasToTempFilePath({ canvas: this.canvas, x: 0, y: 0, width: this.canvas.width, height: this.canvas.height, destWidth: this.canvas.width * 2, // 再放大一倍提升导出清晰度 destHeight: this.canvas.height * 2, fileType: png, success: (res) resolve(res.tempFilePath), fail: reject }); }); }destWidth和destHeight控制导出图片的实际像素尺寸。我一般会设成 canvas 尺寸的 2 倍这样即使把签名图片放到合同里再整体导出也不会因为二次缩放而模糊。fileType选png是因为它支持透明背景方便叠加到合同底图上。如果选jpg透明区域会变成黑色叠上去就是一块黑斑。3.3 签名板尺寸适配与横竖屏小程序里 canvas 的尺寸适配是个老生常谈的问题。组件的做法是用wx.createSelectorQuery拿到 canvas 节点的实际渲染尺寸再结合dpr设置画布缓冲区大小。这样无论手机屏幕是宽是窄签名板的绘制区域都和视觉区域一致。如果业务场景需要横屏签名比如平板设备需要在页面的onResize里重新初始化 canvas。常见做法是监听wx.onWindowResize在回调里重新执行init方法。注意重新初始化前要先保存已有笔迹否则用户签了一半转屏签名就没了。这个细节在代码压缩包的canvas-sign.js里有对应处理搜resize关键字就能找到。4. 合同生成与文件处理模板填充、导出与上传签名只是第一步把签名和合同内容合成一份完整文件才是最终交付物。这个环节涉及模板坐标映射、canvas 合成、临时文件转存三个子问题。4.1 合同模板字段填充合同模板通常是一张底图上面留了空白位置给甲方、乙方、金额、日期等字段。组件的做法是在contract-gen.js里维护一个坐标表// utils/contract-gen.js const FIELD_POSITIONS { partyA: { x: 120, y: 300, fontSize: 16, align: left }, partyB: { x: 120, y: 360, fontSize: 16, align: left }, amount: { x: 400, y: 420, fontSize: 18, align: right }, date: { x: 120, y: 500, fontSize: 14, align: left } }; function fillContract(ctx, data, templateImg) { // 先绘制底图 ctx.drawImage(templateImg, 0, 0, canvasWidth, canvasHeight); // 再逐字段绘制文字 Object.keys(FIELD_POSITIONS).forEach(key { const pos FIELD_POSITIONS[key]; ctx.font ${pos.fontSize}px sans-serif; ctx.fillStyle #333333; ctx.textAlign pos.align; ctx.fillText(data[key] || , pos.x, pos.y); }); }坐标表里的x、y是相对于合同底图左上角的像素位置。这个值需要根据实际模板调整没有万能公式。我一般会先用画图工具在模板上量一下大致位置填进去后再微调两轮。fontSize和align也是按模板留白区域来定align设为right时x坐标对应的是文字右边缘。4.2 合成签名与合同并导出签名图片和合同底图合成时需要把签名绘制到合同指定的签署区域。代码逻辑如下async function composeContract(signTempPath, contractData, templatePath) { const canvas wx.createOffscreenCanvas({ type: 2d, width: 800, height: 1200 }); const ctx canvas.getContext(2d); // 加载底图 const templateImg canvas.createImage(); await new Promise((resolve) { templateImg.onload resolve; templateImg.src templatePath; }); ctx.drawImage(templateImg, 0, 0, 800, 1200); // 填充文字字段 fillContract(ctx, contractData, templateImg); // 加载签名图片并绘制到签署区 const signImg canvas.createImage(); await new Promise((resolve) { signImg.onload resolve; signImg.src signTempPath; }); ctx.drawImage(signImg, 500, 800, 200, 100); // 签署区坐标和尺寸 // 导出最终合同 return new Promise((resolve) { wx.canvasToTempFilePath({ canvas, success: (res) resolve(res.tempFilePath) }); }); }这里用了wx.createOffscreenCanvas创建离屏画布好处是不占用页面渲染资源适合在后台做图片合成。签署区的drawImage参数(500, 800, 200, 100)分别对应 x 坐标、y 坐标、宽度、高度需要根据合同模板上签名栏的实际位置来改。如果签名被拉伸变形检查一下宽高比是否和签名原图一致。4.3 临时文件转存与上传小程序里的临时文件在本次启动后有效重启后可能被清理。所以签署完成后要尽快转存或上传。组件提供了file-helper.js来做这件事// utils/file-helper.js function saveToLocal(tempFilePath) { return new Promise((resolve, reject) { wx.saveFile({ tempFilePath, success: (res) resolve(res.savedFilePath), fail: reject }); }); } function uploadToServer(tempFilePath, url) { return new Promise((resolve, reject) { wx.uploadFile({ url, filePath: tempFilePath, name: contract, success: (res) resolve(JSON.parse(res.data)), fail: reject }); }); }wx.saveFile把临时文件转成持久化文件但注意小程序对本地存储有 10MB 的总量限制合同文件多了会存不下。所以更稳妥的做法是直接上传到服务器本地只保留最近几份。wx.uploadFile的name参数要和后端接收字段一致否则后端拿不到文件。上传超时时间默认是 60 秒合同文件大的话可以在wx.uploadFile里加timeout参数。5. 避坑与常见问题排查这一章记录的是我在实际项目里踩过的坑每一条都对应一个具体的现象和解决方式。如果你在调试时遇到类似情况可以直接对照排查。5.1 签名笔迹断断续续或偏移现象手指快速滑动时签名轨迹出现断点或者笔迹位置和手指位置有明显偏移。原因touchmove事件的触发频率有限快速滑动时两个事件之间的坐标跨度大lineTo直接连线会跳过中间点。偏移问题通常是因为 canvas 的 CSS 尺寸和缓冲区尺寸不一致触摸坐标没有按比例映射。解决在touchmove里用二次贝塞尔曲线平滑连接或者至少把lineTo改成quadraticCurveTo。偏移问题需要检查canvas.width和canvas.style.width的比例触摸坐标要乘以dpr再使用。组件里canvas-sign.js的touchMove方法已经做了坐标转换如果你自己改过样式记得同步改转换逻辑。5.2 导出图片模糊或背景变黑现象导出的签名图片放大后模糊或者背景从透明变成黑色。原因模糊是因为 canvas 缓冲区尺寸等于 CSS 尺寸没有乘以dpr。背景变黑是因为导出格式选了jpg而 jpg 不支持透明通道透明区域默认填充黑色。解决初始化 canvas 时把canvas.width设为cssWidth * dpr并调用ctx.scale(dpr, dpr)。导出格式强制用png。如果业务必须用 jpg在绘制前先给 canvas 填充白色背景。5.3 合同底图加载失败导致白屏现象签名完成后合成合同导出的图片只有签名没有合同底图或者整个画布是白的。原因canvas.createImage加载网络图片时如果图片没有下载到本地或者域名没有加入小程序的 downloadFile 合法域名列表加载会静默失败。解决提前用wx.downloadFile把模板图片下载到本地临时路径再用本地路径去createImage。同时检查小程序后台的 downloadFile 合法域名配置把模板图片所在的域名加进去。如果模板是本地资源直接放assets目录引用即可。5.4 临时文件在真机上找不到现象开发者工具里一切正常真机上签署完成后上传失败提示文件不存在。原因开发者工具对临时文件的保留策略比真机宽松真机上临时文件可能在页面跳转或一段时间后被清理。解决签署完成后立即调用wx.saveFile或直接上传不要等用户点了别的按钮再处理。如果业务允许可以在onHide里也做一次转存。另外注意wx.saveFile的存储上限定期清理旧文件。5.5 签名区域在全面屏手机上被遮挡现象iPhone 全面屏机型上签名板底部被 Home Indicator 挡住用户签不到最下面。原因页面布局没有考虑安全区域canvas 的底部和屏幕底部重合。解决在 wxss 里给签名板容器加padding-bottom: env(safe-area-inset-bottom)或者用wx.getSystemInfoSync().safeArea拿到安全区域高度动态设置 canvas 高度。这个适配在组件的index.wxss里有注释说明搜safe-area就能找到。6. 进阶技巧签名压感模拟与合同验签思路基础功能跑通之后如果想让签名体验更接近真实纸笔可以试试压感模拟。小程序目前没有直接暴露触摸压力值但可以通过触摸面积来近似——e.touches[0]里虽然没有force字段但部分安卓机型支持e.touches[0].radius或e.touches[0].force。我一般会先判断字段是否存在存在就根据压力值动态调整lineWidthtouchMove(e) { if (!this.isDrawing) return; const touch e.touches[0]; const { x, y } touch; // 压感模拟有 force 字段时按压力调整笔宽 const force touch.force || 0.5; const dynamicWidth 2 force * 6; // 笔宽范围 2~8 this.ctx.lineWidth dynamicWidth; this.ctx.lineTo(x, y); this.ctx.stroke(); this.points.push({ x, y, force }); }这段代码在支持压感的设备上会让签名有明显的粗细变化写起来更像真实笔迹。不支持压感的设备会走默认值 0.5笔宽稳定在 5 左右也不会难看。注意每次touchmove都改lineWidth会影响性能可以每 3 个点更新一次。另一个值得花时间的方向是合同验签。虽然小程序端做完整验签不现实但可以在生成合同时把关键字段甲方、乙方、金额、签署时间拼成一个字符串用wx.getFileSystemManager().writeFile写一份校验文件一起上传。后端收到后重新拼一遍字符串做比对能挡住大部分篡改场景。这个思路在代码压缩包的contract-gen.js里留了扩展接口搜verify关键字能看到预留的函数签名。还有一个实际项目里总结的习惯每次改完合同模板坐标我都会先用一组假数据跑一遍完整流程把导出的合同图片存到手机相册放大检查每个字段的位置和签名清晰度。这个动作花不了两分钟但能避免上线后用户反馈“金额印到边框外面了”这种尴尬。希望帮到你。本文还有配套的精品资源点击获取