前言
在现代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;
}
}
// 导出为PNG
async 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) {
// 回退到 require
const 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">
<button
onClick={handleExportConfig}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
获取配置
</button>
<button
onClick={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">
<div
className="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) => (
<img
key={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 && (
<img
src={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) => (
<div
key={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) => (
<div
key={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 && (
<ConfigModal
configString={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>
<textarea
value={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>
<input
type="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>
<select
value={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>
<select
value={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>
<input
type="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>
<input
type="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>
<button
onClick={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) => (
<div
key={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)
- 添加批量生成功能
- 集成云存储服务
- 支持动态二维码
- 添加数据分析功能
如果这篇文章对你有帮助,请点赞收藏支持一下! 🚀