微信小程序二维码生成器开发实战:Canvas渲染、weapp.qrcode与相册保存全解
简介这是面向微信小程序初学者的二维码生成器学习版源码完整展示了在小程序端将文本或链接转换为二维码的实现思路。资源包共15个文件、约48KB包含5个JavaScript逻辑文件、3个WXSS样式表、3个JSON页面配置、2个WXML结构模板和2张PNG素材全局配置、页面布局、事件交互与工具函数分层清晰便于快速理解小程序项目的基本组织方式。目前已有1882人学习既可作为独立练手的完整小项目也能在阅读源码后自行扩展二维码颜色、尺寸、容错率等个性化参数进一步巩固小程序开发技能。压缩包内保留了应用入口、页面目录与utils工具脚本方便对照修改与断点调试熟悉核心逻辑后可直接迁移到名片分享、商品溯源、活动签到等真实业务场景非常适合课程设计或个人作品集积累。1. 学习版二维码生成器卡在小程序渲染链路而不是二维码算法二维码生成器是微信小程序里最典型的“麻雀虽小、五脏俱全”的学习样本它同时牵涉到第三方库选型、Canvas 渲染、分享海报、保存到相册、隐私授权正好覆盖小程序开发者从入门到上线的完整链路。但很多拿到“学习版源码”的人真正动手时才发现卡点根本不在“生成二维码”这一步而在wx.canvasToTempFilePath的调用时机、Canvas 2D 接口与旧版 Canvas Context 的差异、以及新版小程序对getUserProfile的限制上。这篇文章按我自己会走的排查顺序来展开先从二维码生成的库选型和原理说起再把完整代码分模块拆开讲透最后专门写保存图片和 Canvas 自适应这两块最容易被“学习版源码”带偏的细节。2. 二维码生成器先选对库weapp.qrcode 与 qrcodejs 的取舍2.1 为什么学习版源码普遍用 weapp.qrcode微信小程序没有内置二维码生成 API所以“学习版”二维码生成器源码的核心必然是引入一个纯前端二维码生成库。目前 GitHub 上星标最高、小程序适配最完善的是weapp.qrcode它是对qrcodejs的小程序移植版。两者的根本区别在于渲染目标qrcodejs面向浏览器 DOM依赖document.createElement而weapp.qrcode把渲染目标换成CanvasContext也就是小程序里通过wx.createCanvasContext拿到的旧版画布上下文。选型时主要看三点对比维度weapp.qrcodeqrcodejs 自行适配渲染底层CanvasContext / Canvas 2DDOM Canvas小程序免改程度直接可用需要封装一层纠错级别L / M / Q / HL / M / Q / H是否支持 logo 中嵌支持image参数需要额外处理包体积约 9 KB约 14 KB“学习版”源码之所以清一色选weapp.qrcode核心原因就是它把draw函数封装到了this.createQuery的_this作用域里免去自己处理 Canvas 坐标转换的工作。实际项目里我一般也是优先用它除非要同时输出到服务端做海报那才会换用qrcode的 Node 端。2.2 用 npm 在微信小程序里引入 weapp.qrcode 的最小命令在微信小程序项目根目录执行npm init -y npm install weapp.qrcode安装完成后打开微信开发者工具的“工具 → 构建 npm”构建成功后项目里会出现miniprogram_npm目录。这一步漏掉是“学习版源码跑不起来”的第一大原因——只装包不构建require(weapp.qrcode)直接报module not found。如果你拿到的学习版源码已经带miniprogram_npm目录但运行报错优先检查project.config.json是否设置了packNpmManually: true, packNpmRelationList: [ { packageJsonPath: ./package.json, miniprogramNpmDistDir: ./miniprogram/ } ]这个配置决定微信开发者工具把 npm 构建产物输出到哪里。常见错误是工具把miniprogram_npm放在了项目根目录而app.json里的miniprogramRoot指向的是miniprogram/子目录导致找不到包。2.3 生成二维码核心逻辑draw 的第三个参数才是关键weapp.qrcode的核心调用方式如下这段代码也是“学习版”源码里最值得读的部分const QRCode require(weapp.qrcode) Page({ data: { qrText: https://example.com, qrSize: 200, }, onReady() { this.drawQRCode() }, drawQRCode() { QRCode({ canvasId: qrCanvas, ctx: wx.createCanvasContext(qrCanvas), content: this.data.qrText, width: this.data.qrSize, height: this.data.qrSize, padding: 20, background: #ffffff, foreground: #000000, correctLevel: QRCode.CorrectLevel.M, image: { imagePath: /images/logo.png, width: 40, height: 40, }, }) }, })QRCode函数接收一个对象返回值是绘制完成的 Canvas 上下文。correctLevel决定二维码容错率如果中间要嵌 logo建议用M或HL级别在 logo 遮挡后很容易扫不出来。padding参数控制二维码的静区宽度苹果和安卓的扫码系统对静区要求不一样实测padding小于 10 时部分国产安卓机型会识别失败。这里有个容易踩的坑content如果是纯数字二维码会走数字模式Numeric Mode密度明显更低、更容易扫如果是以http://开头的字符串会自动切到 Byte Mode。学习版源码里如果固定生成链接建议保留协议头不要只填裸域名。用户输入不规范时可以做一个前置处理函数function normalizeContent(input) { const trimmed input.trim() if (/^[a-zA-Z][a-zA-Z0-9.-]*:\/\//.test(trimmed)) { return trimmed } return https:// trimmed }这个函数把用户输入的裸域名自动补全为https://协议头避免生成出来的二维码在扫码时被识别成普通文本。3. 把源码拆成模块Canvas 绘制、输入绑定、尺寸计算的完整代码3.1 WXML 结构与 Canvas 画布放置位置学习版源码里 WXML 的核心布局并不复杂但 Canvas 的放置位置会影响后续保存图片的裁剪范围view classcontainer canvas canvas-idqrCanvas classqr-canvas stylewidth: {{qrSize}}px; height: {{qrSize}}px; margin: 0 auto; /canvas view classinput-area input value{{qrText}} bindinputonInput placeholder请输入链接或文本 / button bindtapregenerate sizemini重新生成/button /view button bindtapsaveToAlbum保存到相册/button /view.qr-canvas的样式必须显式声明宽高否则 Canvas 的 CSS 尺寸和内部绘图缓冲区尺寸不一致会出现图片模糊。微信小程序旧版 Canvas 的实现里width/height属性同时决定画布坐标系和输出图片的分辨率所以这里的qrSize直接透传给样式和QRCode参数即可。有同学把 Canvas 放在scroll-view或swiper内部结果wx.canvasToTempFilePath导出的图片出现黑边或尺寸不对。这是因为滚动容器会让 Canvas 的视口位置偏移旧版 Canvas 绘制接口不支持区域外渲染。学习版源码里把 Canvas 放在普通view中、没有滚动容器包裹这个设计不是随意为之是为了后续保存图片少出问题。3.2 JS 侧完整逻辑监听输入、防抖重绘、保存图片const QRCode require(weapp.qrcode) Page({ data: { qrText: , qrSize: 248, timer: null, }, onLoad(options) { const initialText options.text || https://example.com this.setData({ qrText: initialText }) }, onReady() { this.drawQRCode() }, onInput(e) { const value e.detail.value this.setData({ qrText: value }) if (this.data.timer) { clearTimeout(this.data.timer) } this.data.timer setTimeout(() { this.drawQRCode() }, 300) }, regenerate() { this.drawQRCode() }, drawQRCode() { QRCode({ canvasId: qrCanvas, ctx: wx.createCanvasContext(qrCanvas), content: this.data.qrText || , width: this.data.qrSize, height: this.data.qrSize, padding: 20, correctLevel: QRCode.CorrectLevel.M, }) }, saveToAlbum() { wx.canvasToTempFilePath({ canvasId: qrCanvas, success: (res) { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: () { wx.showToast({ title: 已保存, icon: success }) }, fail: (err) { if (err.errMsg.includes(auth deny)) { wx.showModal({ title: 提示, content: 需要相册权限请在设置中开启, confirmText: 去设置, success(modalRes) { if (modalRes.confirm) { wx.openSetting() } }, }) } }, }) }, }) }, })这段代码的input事件处理里加了 300ms 防抖避免用户每敲一个字符就重绘一次二维码。content: this.data.qrText || 这个细节来自学习版源码的注释weapp.qrcode在content为空字符串时会抛content is empty传一个空格可以规避但生成出来的二维码扫码结果是空格不算真正解决。更稳的是在进入drawQRCode前统一做normalizeContent。wx.canvasToTempFilePath必须在 Canvas 绘制完成后调用否则导出的图片是空白。这里没有在drawQRCode内部保存回调是因为QRCode函数的回调时机在不同版本里表现不一致。最稳妥的写法是在QRCode调用后使用setTimeout延后100ms再导出或者干脆在saveToAlbum里重新绘制一次并利用draw回调saveToAlbum() { const ctx wx.createCanvasContext(qrCanvas) QRCode({ canvasId: qrCanvas, ctx, content: this.data.qrText, width: this.data.qrSize, height: this.data.qrSize, padding: 20, correctLevel: QRCode.CorrectLevel.M, }) setTimeout(() { wx.canvasToTempFilePath({ canvasId: qrCanvas, success: (res) { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath }) }, }) }, 200) }3.3 尺寸参数与界面匹配样式 px 和 rpx 的换算问题小程序里默认使用 rpx 做响应式尺寸但 Canvas 的绘图单位是物理像素 px两者在渲染到高分屏时存在缩放倍数。学习版源码里如果直接用 rpx 作为qrSize传给QRCode在 iPhone 12 这类设备上会生成一个尺寸值超大的二维码再经过 CSS 缩放显示就模糊了。实际项目里我一般这样换算const systemInfo wx.getSystemInfoSync() const pixelRatio systemInfo.pixelRatio || 2 // 希望显示宽度为 250rpx换算成物理像素 const displayWidth 250 / (750 / systemInfo.windowWidth) const qrSize Math.floor(displayWidth * pixelRatio)windowWidth是逻辑像素宽度750 是设计稿基准宽度pixelRatio把逻辑像素映射到物理像素。这样算出来的qrSize即使用来导出图片也能保证在绝大多数机型上清晰。3.4 学习版源码常见缺陷缺少错误处理与加载态大量“学习版”源码在QRCode()调用时没有try...catch包裹当用户输入特殊字符如、、或 Emoji时部分版本会直接抛异常。实际生产环境里需要补一层drawQRCode() { try { QRCode({ canvasId: qrCanvas, content: normalizeContent(this.data.qrText), width: this.data.qrSize, height: this.data.qrSize, padding: 20, correctLevel: QRCode.CorrectLevel.M, }) } catch (e) { wx.showToast({ title: 生成失败请检查输入, icon: none }) console.error(QRCode generate error:, e) } }weapp.qrcode在_getCorrectLevel这一步会对非法correctLevel抛错对content类型也不是完全宽容。所以输入框限制maxlength也是一种防御比如将maxlength设为 500避免超长文本生成高密度二维码导致扫描困难。4. 从学习版到生产版Canvas 2D 迁移与自适应尺寸的 3 个必调参数4.1 新版 Canvas 2D 接口与旧版 CanvasContext 的差异微信小程序基础库 2.9.0 开始支持 Canvas 2D 接口也就是type2d的 Canvas。学习版源码多数停留在wx.createCanvasContext的旧版接口上但新版本开发者工具已经开始提示废弃2023 年后上线的项目里用旧接口会在部分安卓机型出现渲染错位。Canvas 2D 的用法差异集中在获取节点和上下文的方式async drawQRCodeWithCanvas2D() { const query this.createSelectorQuery() const node await new Promise((resolve) { query.select(#qrCanvas) .fields({ node: true, size: true }) .exec((res) { if (res res[0]) { resolve(res[0]) } else { resolve(null) } }) }) if (!node) return const { node: canvas, width, height } node const ctx canvas.getContext(2d) const dpr wx.getSystemInfoSync().pixelRatio canvas.width width * dpr canvas.height height * dpr ctx.scale(dpr, dpr) QRCode({ canvas, ctx, content: this.data.qrText, width: this.data.qrSize, height: this.data.qrSize, padding: 20, correctLevel: QRCode.CorrectLevel.M, }) }注意这里传给QRCode的canvas参数必须是type2d的 Canvas 节点本身。weapp.qrcode在内部会判断canvas是否带getContext方法有则走 Canvas 2D 分支没有则回退到旧版wx.createCanvasContext。WXML 对应改为canvas type2d idqrCanvas classqr-canvas stylewidth: {{displayWidth}}px; height: {{displayWidth}}px; /canvastype2d必须显式写出默认值是旧版接口。4.2 二维码中心 Logo 的绘制参数与容错率的关系学习版源码里如果包含 Logo 嵌入功能常见实现方式有两种一是weapp.qrcode自带的image参数二是绘制完二维码后在 Canvas 上手动drawImage。前者更简单但 logo 尺寸过大时二维码识别率急剧下降。Logo 占二维码比例纠错级别 L纠错级别 M纠错级别 H10%可正常扫描可正常扫描可正常扫描20%偶发失败可正常扫描可正常扫描30%基本无法扫描偶发失败可正常扫描所以 logo 宽度不要超过二维码尺寸的 25%用correctLevel: QRCode.CorrectLevel.H是个稳妥起见的组合。手动绘制 Logo 的方式更适合需要圆角或边框的场景const logoSize 40 const logoX (this.data.qrSize - logoSize) / 2 const logoY (this.data.qrSize - logoSize) / 2 ctx.drawImage(/images/logo.png, logoX, logoY, logoSize, logoSize)注意路径要使用本地绝对路径不能是网络 URL否则drawImage直接失败且不报错。4.3 二维码尺寸的自适应动态计算qrSize的黄金比例学习版源码通常把二维码写死为正方形但真实场景里二维码生成器页面有可能被放在不同屏幕尺寸的设备上。自适应参数计算的核心是把容器宽度和固定 padding 纳入公式const PAGE_PADDING 32 // 页面左右留白 const QR_PADDING_RATIO 0.08 // 二维码静区占整体比例 function calcQRSize(windowWidth) { const base windowWidth - PAGE_PADDING * 2 return Math.floor(base * (1 - QR_PADDING_RATIO * 2)) }这里的QR_PADDING_RATIO同时用于QRCode的padding参数换算const padding Math.floor(calcQRSize(windowWidth) * QR_PADDING_RATIO) QRCode({ padding, width: calcQRSize(windowWidth), height: calcQRSize(windowWidth), })如果padding设置的值大于二维码模块大小的一半可能出现绘制区域超出预期画布、二维码被截断的现象。排查方法是把padding临时改成 0看是否恢复正常以此确认问题来源。5. 学习版源码最容易忽略的三个坑以及验证二维码是否可用的方法5.1wx.saveImageToPhotosAlbum的授权链fail 回调必须处理学习版源码最大的问题出在保存到相册的授权路径第一次点击保存时微信会弹出授权框用户如果点了拒绝之后wx.saveImageToPhotosAlbum会一直走fail回调。此时必须用wx.openSetting引导用户重新开启相册权限而且要在fail回调里判断errMsgfail: (err) { if (err.errMsg err.errMsg.includes(auth deny)) { wx.showModal({ title: 需要相册权限, content: 请在设置中允许保存图片到相册, confirmText: 去设置, success: (modalRes) { if (modalRes.confirm) { wx.openSetting() } }, }) } }注意 iOS 上errMsg的值可能是saveImageToPhotosAlbum:fail auth deny而安卓旧版本是saveImageToPhotosAlbum:fail authorize no response统一用includes(auth)判断更稳妥。5.2 基于wx.env.user_data_path的文件缓存保存前先落盘学习版源码把wx.canvasToTempFilePath的结果直接传给wx.saveImageToPhotosAlbum一般情况下能成功。但有些场景比如 Canvas 内容较大、基础库版本偏低临时文件路径在异步回调之间会失效。稳妥做法是先通过wx.getFileSystemManager().saveFile把图片转存到本地wx.canvasToTempFilePath({ canvasId: qrCanvas, success: (res) { const fs wx.getFileSystemManager() const basePath wx.env.USER_DATA_PATH const targetPath ${basePath}/qr_${Date.now()}.png fs.saveFile({ tempFilePath: res.tempFilePath, filePath: targetPath, success: (saveRes) { wx.saveImageToPhotosAlbum({ filePath: saveRes.savedFilePath, success: () wx.showToast({ title: 已保存 }), }) }, }) }, })官方文档里wx.env.USER_DATA_PATH指代用户数据目录这个目录下的文件在应用启动期间不会触达临时文件清理逻辑。这也是为什么多张二维码保存时要带时间戳文件名避免注入同名文件覆盖之前的内容。5.3 扫码验证的闭环用字节长度估算二维码版本提前预判可扫性二维码的编码版本Version决定了它能容纳的数据量。学习版源码通常不展示这个信息但排查扫码失败时版本号是最直观的定位工具。weapp.qrcode内部没有暴露版本返回值但可以根据输入文本字节数估算输入字节数UTF-8纠错级别 M 下的大致版本推荐的二维码尺寸1 - 14 字节Version 121x21 模块15 - 26 字节Version 225x25 模块27 - 42 字节Version 329x29 模块43 - 62 字节Version 433x33 模块63 - 100 字节Version 5-637x37 - 41x41 模块更简单的方式是调用库内部暴露的QRCodeModelconst model QRCode.QRCodeModel(...) // 部分版本支持如果不想深挖 API最常见的验证方式是生成后用系统相机扫码二维码整体边长在屏幕上不小于 2 厘米、静区干净无噪点这就是一个基本可扫的基线。微信开发者工具里的“真机调试”模式对 Canvas 渲染的二维码与模拟器显示有差异必须用真机实测一次扫码流程这不是可有可无的环节而是学习版源码到生产交付之间必须补上的验收步骤。真正把二维码生成器做上线的人至少会再补一套带参数分享海报和动态内容替换的逻辑这些可以留给下一个迭代。本文还有配套的精品资源点击获取