UniApp项目实战:我把uQRCode二维码生成做成了可复用的Vue组件(支持动态配置标题/Logo/样式)

发布时间:2026/6/14 2:19:06
UniApp项目实战:我把uQRCode二维码生成做成了可复用的Vue组件(支持动态配置标题/Logo/样式) UniApp高级实战打造企业级可配置二维码组件全攻略在移动互联网时代二维码已成为连接线上线下场景的重要媒介。对于UniApp开发者而言如何在项目中高效、灵活地生成各种风格的二维码同时保证代码的可维护性和复用性是一个值得深入探讨的技术课题。本文将带你从零开始构建一个支持动态配置标题、Logo和样式的企业级二维码组件解决实际开发中的痛点问题。1. 工程化思维下的组件设计在开始编码之前我们需要先明确组件的设计目标和架构思路。一个优秀的可复用组件应该具备以下特点高内聚低耦合组件内部逻辑自包含对外提供清晰的接口灵活可配置通过Props支持各种定制化需求性能优化合理处理异步操作和绘制性能易用性提供简洁的API和良好的开发者体验1.1 组件Props设计基于这些原则我们首先设计组件的Props接口props: { // 二维码内容 content: { type: String, required: true }, // 二维码尺寸 size: { type: Number, default: 300 }, // 二维码标题 title: { type: String, default: }, // 标题位置top/center/bottom titlePosition: { type: String, default: bottom, validator: (value) [top, center, bottom].includes(value) }, // Logo图片URL logo: { type: String, default: }, // 边框宽度 borderWidth: { type: Number, default: 0 }, // 二维码前景色 foregroundColor: { type: String, default: #000000 }, // 二维码背景色 backgroundColor: { type: String, default: #ffffff } }1.2 组件核心架构组件的主体结构如下template view classqrcode-container canvas :idcanvasId :canvas-idcanvasId :stylecanvasStyle / slot nameextra/slot /view /template script import UQRCode from uqrcode/js export default { name: QrCodeGenerator, props: { /* 上面定义的props */ }, data() { return { canvasId: qrcode-${Date.now()}, isLoading: false } }, computed: { canvasStyle() { return { width: ${this.size}px, height: ${this.size}px } } }, methods: { /* 核心方法 */ } } /script2. 核心绘制逻辑实现2.1 二维码基础生成首先实现最基本的二维码生成功能methods: { async generateQRCode() { if (this.isLoading) return this.isLoading true try { const qr new UQRCode() qr.data this.content qr.size this.size qr.foregroundColor this.foregroundColor qr.backgroundColor this.backgroundColor // 预留边框空间 if (this.borderWidth 0) { qr.margin this.borderWidth 10 } qr.make() const ctx uni.createCanvasContext(this.canvasId, this) qr.canvasContext ctx await this.drawBackground(ctx, qr) await qr.drawCanvas(false) this.$emit(generated, { canvasId: this.canvasId }) } catch (error) { console.error(生成二维码失败:, error) this.$emit(error, error) } finally { this.isLoading false } } }2.2 背景与边框绘制为了支持自定义背景和边框我们需要单独处理这些绘制逻辑async drawBackground(ctx, qr) { // 清空画布 ctx.setFillStyle(this.backgroundColor) ctx.fillRect(0, 0, this.size, this.size) // 绘制边框 if (this.borderWidth 0) { ctx.setFillStyle(this.foregroundColor) const offset this.title this.titlePosition top ? 40 : 0 // 四边边框 ctx.fillRect(0, offset, this.borderWidth, this.size) // 左 ctx.fillRect(this.size - this.borderWidth, offset, this.borderWidth, this.size) // 右 ctx.fillRect(0, offset, this.size, this.borderWidth) // 上 ctx.fillRect(0, this.size - this.borderWidth offset, this.size, this.borderWidth) // 下 } // 绘制标题非居中情况 if (this.title [top, bottom].includes(this.titlePosition)) { await this.drawTextTitle(ctx) } }2.3 标题与Logo处理标题和Logo的绘制是最复杂的部分需要考虑多种排列组合情况async drawTextTitle(ctx) { ctx.setFontSize(16) ctx.setFillStyle(this.foregroundColor) ctx.setTextAlign(center) const textWidth ctx.measureText(this.title).width const maxWidth this.size - 20 let lines [] // 文本换行处理 if (textWidth maxWidth) { let line for (const char of this.title) { if (ctx.measureText(line char).width maxWidth) { line char } else { lines.push(line) line char } } if (line) lines.push(line) } else { lines [this.title] } // 计算绘制位置 const lineHeight 20 const totalHeight lines.length * lineHeight let yPos 0 if (this.titlePosition top) { yPos 10 // 调整二维码位置 qr.getDrawModules().forEach(item { item.y totalHeight 10 }) } else { yPos this.size - totalHeight - 10 } // 绘制每行文本 lines.forEach((line, index) { ctx.fillText(line, this.size / 2, yPos index * lineHeight) }) } async drawCenterLogo(ctx) { if (!this.logo !this.title) return // 绘制Logo背景 const logoSize this.size * 0.2 const logoX (this.size - logoSize) / 2 const logoY (this.size - logoSize) / 2 ctx.setFillStyle(#ffffff) ctx.fillRect(logoX, logoY, logoSize, logoSize) if (this.logo) { // 处理网络图片 const tempFilePath await this.downloadImage(this.logo) ctx.drawImage(tempFilePath, logoX, logoY, logoSize, logoSize) } else if (this.title) { // 绘制居中标题 ctx.setFontSize(14) ctx.setFillStyle(#000000) ctx.setTextAlign(center) ctx.setTextBaseline(middle) ctx.fillText(this.title, this.size / 2, this.size / 2) } }3. 性能优化与高级功能3.1 图片下载与缓存网络Logo图片的处理需要考虑下载和缓存async downloadImage(url) { try { const cacheKey image_cache_${md5(url)} const cachePath uni.getStorageSync(cacheKey) if (cachePath) { return cachePath } const { tempFilePath } await uni.downloadFile({ url }) uni.setStorageSync(cacheKey, tempFilePath) return tempFilePath } catch (error) { console.error(图片下载失败:, error) throw error } }3.2 绘制完成回调为了更好的开发者体验我们提供绘制完成的回调watch: { content: { immediate: true, handler() { this.$nextTick(() { this.generateQRCode() }) } } } // 在drawCanvas完成后 await qr.drawCanvas(false) this.$emit(generated, { canvasId: this.canvasId, size: this.size, content: this.content })3.3 导出图片功能添加导出图片的便捷方法methods: { async exportToTempFilePath() { return new Promise((resolve, reject) { uni.canvasToTempFilePath({ canvasId: this.canvasId, success: (res) resolve(res.tempFilePath), fail: reject }, this) }) } }4. 组件集成与使用示例4.1 在页面中使用组件template view qrcode-generator contenthttps://example.com size300 title扫描二维码访问 title-positionbottom logohttps://example.com/logo.png border-width5 generatedhandleGenerated / button clicksaveQRCode保存二维码/button /view /template script import QrcodeGenerator from /components/QrcodeGenerator.vue export default { components: { QrcodeGenerator }, methods: { handleGenerated({ canvasId }) { console.log(二维码生成完成, canvasId) }, async saveQRCode() { const tempFilePath await this.$refs.qrcode.exportToTempFilePath() uni.saveImageToPhotosAlbum({ filePath: tempFilePath, success: () uni.showToast({ title: 保存成功 }) }) } } } /script4.2 动态配置示例// 动态改变二维码配置 updateQRCode() { this.qrConfig { content: https://example.com/user/${this.userId}, title: 用户专属二维码: ${this.userName}, logo: this.userAvatar, borderWidth: this.isVip ? 8 : 0, foregroundColor: this.isVip ? #FFD700 : #000000 } }4.3 多场景适配通过slot支持更灵活的布局qrcode-generator :contentqrContent :size300 template #extra view classqr-tip长按识别二维码/view /template /qrcode-generator5. 常见问题与解决方案5.1 Canvas层级问题在UniApp中canvas组件有较高的层级可能会覆盖其他元素。解决方案使用cover-view覆盖需要显示的内容通过条件渲染控制显示顺序将二维码生成后转换为图片显示5.2 图片跨域问题处理网络Logo图片时可能遇到的跨域问题// 在manifest.json中配置 networkTimeout: { downloadFile: 60000 }, mp-weixin: { permission: { scope.writePhotosAlbum: { desc: 用于保存二维码到相册 } } }5.3 性能优化建议对于频繁更新的二维码使用防抖控制生成频率考虑使用worker线程生成二维码对相同内容的二维码进行缓存// 防抖示例 import { debounce } from lodash-es export default { methods: { generateQRCode: debounce(function() { // 实际生成逻辑 }, 300) } }6. 扩展与进阶6.1 支持更多样式配置可以扩展支持更多样式选项props: { // 点形状square/circle/round/diamond dotShape: { type: String, default: square }, // 点缩放比例 dotScale: { type: Number, default: 1, validator: (value) value 0 value 2 } } // 在生成时应用 qr.dotShape this.dotShape qr.dotScale this.dotScale6.2 服务端渲染支持对于需要服务端生成的情况// 在Node.js环境中使用 const UQRCode require(uqrcode/js) const { createCanvas } require(canvas) function generateQRCodeOnServer(options) { const canvas createCanvas(options.size, options.size) const qr new UQRCode() qr.data options.content qr.size options.size qr.canvasContext canvas.getContext(2d) qr.make() return qr.drawCanvas() }6.3 二维码解析功能添加解析二维码的能力methods: { async scanQRCode() { try { const res await uni.chooseImage({ count: 1 }) const tempFilePath res.tempFilePaths[0] const result await this.parseQRCode(tempFilePath) this.$emit(parsed, result) } catch (error) { this.$emit(error, error) } }, parseQRCode(filePath) { return new Promise((resolve, reject) { // 使用第三方库解析二维码 // ... }) } }