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 class="qrcode-container"> <canvas :id="canvasId" :canvas-id="canvasId" :style="canvasStyle" /> <slot name="extra"></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: { /* 核心方法 */ } } </script>2. 核心绘制逻辑实现
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 content="https://example.com" size="300" title="扫描二维码访问" title-position="bottom" logo="https://example.com/logo.png" border-width="5" @generated="handleGenerated" /> <button @click="saveQRCode">保存二维码</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: '保存成功' }) }) } } } </script>4.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 :content="qrContent" :size="300" > <template #extra> <view class="qr-tip">长按识别二维码</view> </template> </qrcode-generator>5. 常见问题与解决方案
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) => { // 使用第三方库解析二维码 // ... }) } }