前言
在現代Web應用中,QR碼已成為連接線上線下的重要橋梁。本文將詳細介紹如何使用React + TypeScript + Vite構建一個功能強大、高度可定制的QR碼生成器,支持背景圖片、文本疊加、HTML模塊、圓角導出等高級功能。
前往試試
項目概述
技術棧
- 前端框架: React 19 + TypeScript
- 構建工具: Vite 6
- 樣式框架: TailwindCSS 4
- QR碼生成: qr-code-styling
- 圖像處理: html2canvas
- 狀態管理: React Hooks
核心功能
- 🎨 豐富的QR碼樣式定制(點樣式、顏色、漸變)
- 🖼? 背景圖片支持(多種適配模式)
- 📝 文本疊加(字體、顏色、位置可調)
- 🧩 HTML模塊嵌入
- 🔄 實時預覽
- 📤 高質量導出(PNG/JPEG/WebP)
- 🔄 圓角導出支持
- ?? 配置參數導入導出
項目架構設計
目錄結構
qr-vite-app-react/
├── src/
│ ├── components/ # React組件
│ │ ├── PreviewCanvas.tsx # 預覽畫布
│ │ ├── settings/ # 設置面板
│ │ └── test/ # 測試組件
│ ├── hooks/ # 自定義Hooks
│ │ └── useQRGenerator.ts # QR生成器Hook
│ ├── lib/ # 核心庫
│ │ ├── qr-generator-core.ts # QR生成器核心
│ │ └── package.json # 獨立包配置
│ ├── types/ # TypeScript類型定義
│ └── utils/ # 工具函數
├── package.json
└── vite.config.ts
核心架構
1. 配置接口設計
interface QRGeneratorConfig {// 基礎配置text: string;width: number;height: number;qrPosition: { x: number; y: number };qrSize: { width: number; height: number };// QR碼樣式qrOptions: {typeNumber: number;mode: 'Numeric' | 'Alphanumeric' | 'Byte' | 'Kanji';errorCorrectionLevel: 'L' | 'M' | 'Q' | 'H';};// 點樣式配置dotsOptions: {color: string;type: 'rounded' | 'dots' | 'classy' | 'square';gradient?: GradientConfig;};// 背景圖片backgrounds?: BackgroundImage[];// 文本疊加texts?: TextLayer[];// HTML模塊htmlModules?: HtmlModule[];// 導出配置exportOptions: {format: 'png' | 'jpeg' | 'webp';quality: number;borderRadius: number;};
}
2. 核心生成器類
export class QRGenerator {private config: QRGeneratorConfig;private container: HTMLDivElement | null = null;private qrCode: any | null = null;private isRendered = false;constructor(config: Partial<QRGeneratorConfig>) {this.config = this.mergeWithDefaults(config);}// 動態創建畫布private createCanvas(): HTMLDivElement {const canvas = document.createElement('div');canvas.style.cssText = `position: relative;width: ${this.config.width}px;height: ${this.config.height}px;background: ${this.config.backgroundOptions.color};overflow: hidden;`;return canvas;}// 添加背景圖片private async addBackgrounds(canvas: HTMLDivElement): Promise<void> {if (!this.config.backgrounds?.length) return;const loadPromises = this.config.backgrounds.map(bg => this.loadBackgroundImage(canvas, bg));await Promise.all(loadPromises);}// 添加QR碼private async addQRCode(canvas: HTMLDivElement): Promise<void> {const QRCodeStyling = await this.loadQRCodeStyling();const qrContainer = document.createElement('div');qrContainer.style.cssText = `position: absolute;left: ${this.config.qrPosition.x}px;top: ${this.config.qrPosition.y}px;width: ${this.config.qrSize.width}px;height: ${this.config.qrSize.height}px;z-index: 100;`;this.qrCode = new QRCodeStyling({width: this.config.qrSize.width,height: this.config.qrSize.height,data: this.config.text,qrOptions: this.config.qrOptions,dotsOptions: this.config.dotsOptions,// ... 其他配置});this.qrCode.append(qrContainer);canvas.appendChild(qrContainer);}// 渲染完整畫布async render(): Promise<HTMLDivElement> {this.container = this.createCanvas();// 添加到DOM(隱藏位置)this.container.style.position = 'absolute';this.container.style.left = '-9999px';document.body.appendChild(this.container);try {await this.addBackgrounds(this.container);await this.addQRCode(this.container);this.addTexts(this.container);this.addHtmlModules(this.container);this.isRendered = true;return this.container;} catch (error) {this.cleanup();throw error;}}// 導出為PNGasync exportAsPNG(options?: ExportOptions): Promise<Blob> {if (!this.isRendered) await this.render();const canvas = await html2canvas(this.container!, {scale: options?.scale || 2,useCORS: true,allowTaint: false,backgroundColor: null,});return new Promise((resolve, reject) => {canvas.toBlob(blob => {blob ? resolve(blob) : reject(new Error('導出失敗'));}, 'image/png', options?.quality || 0.9);});}
}
關鍵技術實現
1. 動態模塊加載
為了解決qr-code-styling
的模塊導入問題,采用動態加載策略:
const loadQRCodeStyling = async (): Promise<any> => {try {// 嘗試 ES6 導入const module = await import('qr-code-styling');const QRCodeStyling = module.default || module.QRCodeStyling || module;if (typeof QRCodeStyling !== 'function') {throw new Error('QRCodeStyling is not a constructor');}return QRCodeStyling;} catch (error) {// 回退到 requireconst qrModule = require('qr-code-styling');return qrModule.default || qrModule.QRCodeStyling || qrModule;}
};
2. 背景圖片處理
支持多種適配模式的背景圖片:
private getObjectFitStyle(mode: string): string {const modeMap = {'fill': 'width: 100%; height: 100%;','contain': 'width: 100%; height: 100%; object-fit: contain;','cover': 'width: 100%; height: 100%; object-fit: cover;','stretch': 'width: 100%; height: 100%;'};return modeMap[mode] || modeMap['fill'];
}private async loadBackgroundImage(canvas: HTMLDivElement, bg: BackgroundImage): Promise<void> {return new Promise((resolve, reject) => {const img = document.createElement('img');img.onload = () => {img.style.cssText = `position: absolute;left: ${bg.position.x}px;top: ${bg.position.y}px;width: ${bg.size.width}px;height: ${bg.size.height}px;z-index: ${bg.zIndex};opacity: ${bg.opacity};${this.getObjectFitStyle(bg.mode)}`;canvas.appendChild(img);resolve();};img.onerror = () => reject(new Error(`背景圖片加載失敗: ${bg.src}`));img.src = bg.src;});
}
3. 圓角導出功能
實現圓角導出的核心算法:
private applyRoundedCorners(canvas: HTMLCanvasElement, borderRadius: number): HTMLCanvasElement {if (borderRadius <= 0) return canvas;const roundedCanvas = document.createElement('canvas');const ctx = roundedCanvas.getContext('2d')!;roundedCanvas.width = canvas.width;roundedCanvas.height = canvas.height;// 創建圓角路徑ctx.beginPath();ctx.roundRect(0, 0, canvas.width, canvas.height, borderRadius);ctx.clip();// 繪制原始圖像ctx.drawImage(canvas, 0, 0);return roundedCanvas;
}
4. React Hook集成
使用自定義Hook管理狀態:
export const useQRGenerator = () => {const [qrConfig, setQrConfig] = useState<QRConfig>(defaultQRConfig);const [exportConfig, setExportConfig] = useState<ExportConfig>(defaultExportConfig);const [qrDataUrl, setQrDataUrl] = useState<string>('');const [isGenerating, setIsGenerating] = useState(false);const generateQRCode = useCallback(async () => {setIsGenerating(true);try {const qrCode = new QRCodeStyling({width: 300,height: 300,data: qrConfig.content,qrOptions: qrConfig.qrOptions,dotsOptions: qrConfig.dotsOptions,// ... 其他配置});const dataUrl = await qrCode.getRawData('png');setQrDataUrl(URL.createObjectURL(dataUrl!));} catch (error) {console.error('QR碼生成失敗:', error);} finally {setIsGenerating(false);}}, [qrConfig]);const exportImage = useCallback(async () => {const generator = new QRGenerator({text: qrConfig.content,width: exportConfig.width,height: exportConfig.height,// ... 其他配置});const blob = await generator.exportAsPNG({quality: exportConfig.quality,borderRadius: exportConfig.borderRadius,});// 下載文件const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = `qr-code-${Date.now()}.png`;a.click();URL.revokeObjectURL(url);}, [qrConfig, exportConfig]);return {qrConfig,setQrConfig,exportConfig,setExportConfig,qrDataUrl,isGenerating,generateQRCode,exportImage,};
};
組件設計
1. 預覽畫布組件
interface PreviewCanvasProps {qrConfig: QRConfig;exportConfig: ExportConfig;qrDataUrl: string;onExport: () => void;isExporting: boolean;
}export const PreviewCanvas: React.FC<PreviewCanvasProps> = ({qrConfig,exportConfig,qrDataUrl,onExport,isExporting
}) => {const [showConfigModal, setShowConfigModal] = useState(false);const [configString, setConfigString] = useState('');const generateConfigString = () => {const config = {qrConfig,exportConfig,timestamp: new Date().toISOString(),};return JSON.stringify(config, null, 2);};const handleExportConfig = () => {const configStr = generateConfigString();setConfigString(configStr);setShowConfigModal(true);};return (<div className="bg-white rounded-lg shadow-lg p-6">{/* 工具欄 */}<div className="flex justify-between items-center mb-4"><h2 className="text-xl font-semibold">預覽</h2><div className="flex gap-2"><buttononClick={handleExportConfig}className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">獲取配置</button><buttononClick={onExport}disabled={isExporting}className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:opacity-50">{isExporting ? '導出中...' : '導出圖片'}</button></div></div>{/* 畫布容器 */}<div className="border-2 border-dashed border-gray-300 rounded-lg p-4 min-h-[400px] flex items-center justify-center"><divclassName="relative bg-white shadow-lg"style={{width: `${exportConfig.width}px`,height: `${exportConfig.height}px`,borderRadius: `${exportConfig.borderRadius}px`,transform: 'scale(0.5)',transformOrigin: 'center',}}>{/* 背景層 */}{qrConfig.backgrounds.map((bg, index) => (<imgkey={index}src={bg.src}alt={`背景 ${index + 1}`}className="absolute"style={{left: `${bg.position.x}px`,top: `${bg.position.y}px`,width: `${bg.size.width}px`,height: `${bg.size.height}px`,zIndex: bg.zIndex,opacity: bg.opacity,objectFit: bg.mode === 'contain' ? 'contain' : 'cover',}}/>))}{/* QR碼層 */}{qrDataUrl && (<imgsrc={qrDataUrl}alt="QR Code"className="absolute"style={{left: `${qrConfig.qrPosition.x}px`,top: `${qrConfig.qrPosition.y}px`,width: `${qrConfig.qrSize.width}px`,height: `${qrConfig.qrSize.height}px`,zIndex: 100,}}/>)}{/* 文本層 */}{qrConfig.texts.map((text, index) => (<divkey={index}className="absolute whitespace-pre-wrap"style={{left: `${text.position.x}px`,top: `${text.position.y}px`,fontSize: `${text.fontSize}px`,color: text.color,fontFamily: text.fontFamily,fontWeight: text.fontWeight,zIndex: text.zIndex,opacity: text.opacity,textAlign: text.textAlign || 'left',lineHeight: text.lineHeight || 1.2,}}>{text.content}</div>))}{/* HTML模塊層 */}{qrConfig.htmlModules.map((module, index) => (<divkey={index}className="absolute overflow-hidden"style={{left: `${module.position.x}px`,top: `${module.position.y}px`,width: `${module.size.width}px`,height: `${module.size.height}px`,zIndex: module.zIndex,opacity: module.opacity,}}dangerouslySetInnerHTML={{ __html: module.content }}/>))}</div></div>{/* 畫布信息 */}<div className="mt-4 text-sm text-gray-600"><div>畫布尺寸: {exportConfig.width} × {exportConfig.height}px</div><div>圓角半徑: {exportConfig.borderRadius}px</div><div>圖層數量: {qrConfig.backgrounds.length + qrConfig.texts.length + qrConfig.htmlModules.length + 1}</div></div>{/* 配置模態框 */}{showConfigModal && (<ConfigModalconfigString={configString}onClose={() => setShowConfigModal(false)}/>)}</div>);
};
2. 設置面板組件
export const QRSettings: React.FC<QRSettingsProps> = ({qrConfig,onConfigChange
}) => {return (<div className="space-y-6">{/* 基礎設置 */}<div className="bg-white rounded-lg p-4 shadow"><h3 className="text-lg font-semibold mb-4">基礎設置</h3><div className="space-y-4"><div><label className="block text-sm font-medium mb-2">QR碼內容</label><textareavalue={qrConfig.content}onChange={(e) => onConfigChange({ content: e.target.value })}className="w-full p-2 border rounded-md"rows={3}placeholder="輸入要生成QR碼的內容..."/></div><div className="grid grid-cols-2 gap-4"><div><label className="block text-sm font-medium mb-2">QR碼大小</label><inputtype="range"min="100"max="500"value={qrConfig.qrSize.width}onChange={(e) => onConfigChange({qrSize: {width: parseInt(e.target.value),height: parseInt(e.target.value)}})}className="w-full"/><span className="text-sm text-gray-500">{qrConfig.qrSize.width}px</span></div><div><label className="block text-sm font-medium mb-2">容錯級別</label><selectvalue={qrConfig.qrOptions.errorCorrectionLevel}onChange={(e) => onConfigChange({qrOptions: {...qrConfig.qrOptions,errorCorrectionLevel: e.target.value as 'L' | 'M' | 'Q' | 'H'}})}className="w-full p-2 border rounded-md"><option value="L">低 (7%)</option><option value="M">中 (15%)</option><option value="Q">高 (25%)</option><option value="H">最高 (30%)</option></select></div></div></div></div>{/* 樣式設置 */}<div className="bg-white rounded-lg p-4 shadow"><h3 className="text-lg font-semibold mb-4">樣式設置</h3><div className="space-y-4"><div><label className="block text-sm font-medium mb-2">點樣式</label><selectvalue={qrConfig.dotsOptions.type}onChange={(e) => onConfigChange({dotsOptions: {...qrConfig.dotsOptions,type: e.target.value as any}})}className="w-full p-2 border rounded-md"><option value="square">方形</option><option value="rounded">圓角</option><option value="dots">圓點</option><option value="classy">經典</option><option value="extra-rounded">超圓角</option></select></div><div><label className="block text-sm font-medium mb-2">點顏色</label><inputtype="color"value={qrConfig.dotsOptions.color}onChange={(e) => onConfigChange({dotsOptions: {...qrConfig.dotsOptions,color: e.target.value}})}className="w-full h-10 border rounded-md"/></div><div><label className="block text-sm font-medium mb-2">背景顏色</label><inputtype="color"value={qrConfig.backgroundOptions.color}onChange={(e) => onConfigChange({backgroundOptions: {...qrConfig.backgroundOptions,color: e.target.value}})}className="w-full h-10 border rounded-md"/></div></div></div></div>);
};
構建與部署
1. 構建配置
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'export default defineConfig({plugins: [react()],resolve: {alias: {'@': path.resolve(__dirname, './src'),'@lib': path.resolve(__dirname, './src/lib')}},optimizeDeps: {include: ['html2canvas', 'qr-code-styling'],},build: {rollupOptions: {output: {manualChunks: {vendor: ['react', 'react-dom'],qr: ['qr-code-styling', 'html2canvas']}}}}
})
2. 獨立庫打包
// src/lib/rollup.config.js
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';export default {input: 'index.ts',output: [{file: 'dist/index.js',format: 'cjs',exports: 'named'},{file: 'dist/index.esm.js',format: 'esm'}],plugins: [nodeResolve({browser: true,preferBuiltins: false}),commonjs({include: ['node_modules/**'],transformMixedEsModules: true}),typescript({tsconfig: './tsconfig.json'})],external: ['qr-code-styling', 'html2canvas']
};
性能優化
1. 懶加載優化
// 組件懶加載
const QRSettings = lazy(() => import('./components/settings/QRSettings'));
const ExportSettings = lazy(() => import('./components/settings/ExportSettings'));// 在使用時
<Suspense fallback={<div>加載中...</div>}><QRSettings {...props} />
</Suspense>
2. 內存管理
export class QRGenerator {// 清理資源cleanup(): void {if (this.container && this.container.parentNode) {this.container.parentNode.removeChild(this.container);}this.container = null;this.qrCode = null;this.isRendered = false;}// 銷毀實例destroy(): void {this.cleanup();// 清理事件監聽器等}
}
3. 緩存策略
// 圖片緩存
const imageCache = new Map<string, HTMLImageElement>();const loadImage = async (src: string): Promise<HTMLImageElement> => {if (imageCache.has(src)) {return imageCache.get(src)!;}return new Promise((resolve, reject) => {const img = new Image();img.onload = () => {imageCache.set(src, img);resolve(img);};img.onerror = reject;img.src = src;});
};
測試與調試
1. 單元測試
// QRGenerator.test.ts
import { QRGenerator } from '../lib/qr-generator-core';describe('QRGenerator', () => {let generator: QRGenerator;beforeEach(() => {generator = new QRGenerator({text: 'Test QR Code',width: 800,height: 600});});afterEach(() => {generator.destroy();});test('should create QR generator with default config', () => {expect(generator.getConfig().text).toBe('Test QR Code');expect(generator.getConfig().width).toBe(800);});test('should render canvas successfully', async () => {const canvas = await generator.render();expect(canvas).toBeInstanceOf(HTMLDivElement);expect(canvas.style.width).toBe('800px');});test('should export PNG blob', async () => {const blob = await generator.exportAsPNG();expect(blob).toBeInstanceOf(Blob);expect(blob.type).toBe('image/png');});
});
2. 集成測試組件
export const QRGeneratorTest: React.FC = () => {const [testResults, setTestResults] = useState<TestResult[]>([]);const [isRunning, setIsRunning] = useState(false);const runTests = async () => {setIsRunning(true);const results: TestResult[] = [];try {// 基礎功能測試const basicTest = await testBasicGeneration();results.push(basicTest);// 導出功能測試const exportTest = await testExportFunctionality();results.push(exportTest);// 配置序列化測試const configTest = await testConfigSerialization();results.push(configTest);} catch (error) {results.push({name: '測試執行失敗',success: false,error: error.message});} finally {setTestResults(results);setIsRunning(false);}};return (<div className="p-6"><h2 className="text-2xl font-bold mb-4">QR生成器測試</h2><buttononClick={runTests}disabled={isRunning}className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50">{isRunning ? '測試中...' : '運行測試'}</button><div className="mt-6 space-y-4">{testResults.map((result, index) => (<divkey={index}className={`p-4 rounded-lg ${result.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}><div className="font-semibold">{result.name}</div>{result.error && <div className="text-sm mt-1">{result.error}</div>}{result.duration && <div className="text-sm mt-1">耗時: {result.duration}ms</div>}</div>))}</div></div>);
};
總結
本文詳細介紹了如何構建一個功能完整的QR碼生成器,涵蓋了從架構設計到具體實現的各個方面。主要特點包括:
技術亮點
- 模塊化設計: 核心庫可獨立發布使用
- TypeScript支持: 完整的類型定義和類型安全
- 高度可定制: 支持豐富的樣式和布局選項
- 性能優化: 懶加載、緩存、內存管理
- 測試完善: 單元測試和集成測試
應用場景
- 營銷活動二維碼生成
- 產品包裝二維碼定制
- 活動海報二維碼嵌入
- 品牌二維碼標準化生成
擴展方向
- 支持更多導出格式(SVG、PDF)
- 添加批量生成功能
- 集成云存儲服務
- 支持動態二維碼
- 添加數據分析功能
如果這篇文章對你有幫助,請點贊收藏支持一下! 🚀