简介:这是面向微信小程序初学者的二维码生成器学习版源码,完整展示了在小程序端将文本或链接转换为二维码的实现思路。资源包共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.qrcode | qrcodejs 自行适配 |
|---|---|---|
| 渲染底层 | CanvasContext / Canvas 2D | DOM + Canvas |
| 小程序免改程度 | 直接可用 | 需要封装一层 |
| 纠错级别 | L / M / Q / H | L / 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或H,L级别在 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 class="container"> <canvas canvas-id="qrCanvas" class="qr-canvas" style="width: {{qrSize}}px; height: {{qrSize}}px; margin: 0 auto;"> </canvas> <view class="input-area"> <input value="{{qrText}}" bindinput="onInput" placeholder="请输入链接或文本" /> <button bindtap="regenerate" size="mini">重新生成</button> </view> <button bindtap="saveToAlbum">保存到相册</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 接口,也就是type="2d"的 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参数必须是type="2d"的 Canvas 节点本身。weapp.qrcode在内部会判断canvas是否带getContext方法,有则走 Canvas 2D 分支,没有则回退到旧版wx.createCanvasContext。
WXML 对应改为:
<canvas type="2d" id="qrCanvas" class="qr-canvas" style="width: {{displayWidth}}px; height: {{displayWidth}}px;"> </canvas>type="2d"必须显式写出,默认值是''旧版接口。
4.2 二维码中心 Logo 的绘制参数与容错率的关系
学习版源码里如果包含 Logo 嵌入功能,常见实现方式有两种:一是weapp.qrcode自带的image参数,二是绘制完二维码后在 Canvas 上手动drawImage。前者更简单,但 logo 尺寸过大时二维码识别率急剧下降。
| Logo 占二维码比例 | 纠错级别 L | 纠错级别 M | 纠错级别 H |
|---|---|---|---|
| 10% | 可正常扫描 | 可正常扫描 | 可正常扫描 |
| 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回调里判断errMsg:
fail: (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 1 | 21x21 模块 |
| 15 - 26 字节 | Version 2 | 25x25 模块 |
| 27 - 42 字节 | Version 3 | 29x29 模块 |
| 43 - 62 字节 | Version 4 | 33x33 模块 |
| 63 - 100 字节 | Version 5-6 | 37x37 - 41x41 模块 |
更简单的方式是调用库内部暴露的QRCodeModel:
const model = QRCode.QRCodeModel(...) // 部分版本支持如果不想深挖 API,最常见的验证方式是生成后用系统相机扫码:二维码整体边长在屏幕上不小于 2 厘米、静区干净无噪点,这就是一个基本可扫的基线。
微信开发者工具里的“真机调试”模式对 Canvas 渲染的二维码与模拟器显示有差异,必须用真机实测一次扫码流程,这不是可有可无的环节,而是学习版源码到生产交付之间必须补上的验收步骤。真正把二维码生成器做上线的人,至少会再补一套带参数分享海报和动态内容替换的逻辑,这些可以留给下一个迭代。
本文还有配套的精品资源,点击获取