纯前端二维码 / 条码生成器:从协议拼装到批量 ZIP 下载完整拆解
在线体验:geekformat.com/zh-CN/other/qrcode-gen
故事开头
做二维码页面时,需求几乎不会停在“输入一个网址,生成一张码图”。
真实场景通常是这样的:
- 运营要一个活动报名二维码
- 门店要一个顾客扫码直连的 WiFi 二维码
- 销售要一个扫码存联系人的名片二维码
- 仓库要一批商品条码,最好一次性导出
这时候你会发现,问题的重点根本不是“怎么画一个二维码”,而是:
- 怎么把不同业务场景转换成正确的协议字符串
- 怎么同时支持二维码和条码
- 怎么把颜色、尺寸、Logo、样式这些配置接进同一套生成流程
- 怎么把单张下载和批量 ZIP 导出一起做好
这篇文章不讲泛泛的二维码科普,而是直接结合这个页面的真实实现来拆:
- 页面为什么要做成二维码 / 条码双模式
- 动态表单和协议层是怎么组织的
- 二维码生成后为什么还要再走一遍 Canvas
- 批量生成和 ZIP 打包是怎么落地的
这页最终的结构很清楚:
- 左侧是二维码 / 条码模式切换
- 中间是不同内容类型的输入表单
- 下方是颜色、尺寸、纠错级别、样式、Logo 等配置
- 右侧是即时预览、下载、复制和批量导出
技术栈一览
| 类别 | 技术 | 用途 |
|---|---|---|
| 框架 | React + Next.js + TypeScript | 页面结构、状态管理、类型约束 |
| 二维码生成 | qrcode | 把协议字符串转成基础二维码 DataURL |
| 二次绘制 | Canvas 2D | 圆角、点状、Logo 叠加等样式加工 |
| 条码生成 | Canvas 2D | 按码制规则直接绘制条纹和文本 |
| 批量导出 | jszip | 多张二维码 / 条码打包下载 |
| 浏览器能力 | Clipboard API / FileReader / Blob | 复制图片、上传 Logo、下载文件 |
这页的选型很直接:
- 二维码交给成熟库
qrcode - 样式加工交给 Canvas
- 条码单独绘制,不和二维码共用一套生成器
- 批量打包交给
jszip
它不是一个重渲染工具页,真正的难点在于协议拼装、状态组织、批量流程和错误处理。
一、整体架构:输入 → 协议 → 生成 → 预览 → 导出
从代码实现上看,这页主要分成 5 层:
- 模式层:二维码 / 条码切换
- 输入层:根据内容类型渲染不同表单
- 协议层:把表单数据转换成真正可编码的字符串
- 生成层:二维码走
generateQRDataURL,条码走generateBarcodeDataURL - 导出层:下载单张、复制、批量 ZIP
这里最关键的一点是:真正决定结果对不对的,首先不是样式,而是协议字符串是不是拼对了。
如果把这页的最短主流程再压缩一层,其实就是下面这 6 步:
二、为什么这个页面不能只做一个输入框
很多二维码页面只有一个大输入框:
输入任意文本 -> 生成二维码这种做法只能覆盖最基础的纯文本场景。
而这个页面把二维码内容拆成了 9 类:
urltextemailphonesmswifivcardgeoevent
对应代码里的类型定义就是:
typeQRContentType=|'url'|'text'|'email'|'phone'|'sms'|'wifi'|'vcard'|'geo'|'event'这样做的意义不是“表单更丰富”,而是把输入数据先结构化:
wifi需要ssid / password / encryption / hiddenvcard需要name / phone / email / company / title / address / urlevent需要title / location / start / end / description
只有先把输入拆开,后面协议拼装、批量逻辑和错误提示才能写得稳。
三、协议层才是核心:同样是二维码,内容字符串完全不一样
页面里最关键的函数之一,是generateQRDataURL之前那段协议拼装逻辑:
switch(contentType){case'url':content=data.url||''breakcase'text':content=data.text||''breakcase'email':content=`mailto:${data.email||''}?subject=${encodeURIComponent(data.subject||'')}&body=${encodeURIComponent(data.body||'')}`breakcase'phone':content=`tel:${data.phone||''}`breakcase'sms':content=`sms:${data.phone||''}?body=${encodeURIComponent(data.message||'')}`breakcase'wifi':content=`WIFI:T:${data.encryption||'WPA'};S:${data.ssid||''};P:${data.password||''};${data.hidden==='true'?'H:true;':''};`break}这段代码说明了一件事:
二维码不是“把用户看到的文本原样塞进去”就行,而是要拼成目标 App 或系统能识别的协议。
1. WiFi 不是普通文本
如果用户只是输入:
TP-Link_5G / 12345678扫码器大概率只会把它当成文本显示。
而页面真正生成的是:
WIFI:T:WPA;S:TP-Link_5G;P:12345678;H:false;这才是系统能识别的 WiFi 连接协议。
2. 名片不是 JSON,而是 vCard
名片类型最终拼成的是:
BEGIN:VCARD VERSION:3.0 FN:张三 TEL:13800138000 EMAIL:zhangsan@example.com ORG:GeekFormat END:VCARD这样扫码之后,很多系统会直接进入“保存联系人”的流程。
3. 日程不是纯文本,而是事件协议
事件类型最终拼的是:
BEGIN:VEVENT SUMMARY:产品评审会 LOCATION:会议室 A DTSTART:20260608T100000Z DTEND:20260608T113000Z DESCRIPTION:讨论二维码改版 END:VEVENT所以这页最核心的价值,不是“生成二维码图片”,而是把高频业务场景正确映射成规范字符串。
四、二维码生成只是第一步:样式层还要再走一遍 Canvas
页面里真正生成二维码的函数是generateQRDataURL:
returnQRCode.toDataURL(content,{width:size,margin:2,color:{dark:fgColor,light:bgColor},errorCorrectionLevel:errorLevel})如果只是黑白标准二维码,到这里已经够了。
但页面实际还支持这些配置:
- 前景色 / 背景色
- 尺寸
- 纠错级别
- 方形 / 圆角 / 点状
- 中心 Logo
这时就不能只停在qrcode的默认输出,而是要把生成出来的二维码再画进 Canvas 里继续处理。对应的就是drawStyledQR。
1. 圆角 / 点状:本质是二次裁剪
这两种样式不是重新算二维码矩阵,而是对基础二维码图片做遮罩裁剪。
2. Logo:先垫白底,再贴图
页面处理 Logo 的逻辑是:
constlogoS=Math.floor(size*(logoSize/100))constx=(size-logoS)/2consty=(size-logoS)/2ctx.fillStyle=bgColor ctx.fillRect(x-4,y-4,logoS+8,logoS+8)ctx.drawImage(logoImg,x,y,logoS,logoS)这里有两个很实际的点:
- Logo 不是直接贴上去,而是先垫一层白底
- Logo 尺寸不是写死,而是按百分比可调
另外,页面里Logo 大小滑杆只有在上传 Logo 后才会出现,这也是一个很合理的交互细节。
3. 纠错级别决定了样式容忍度
页面提供了 4 档纠错级别:
L (7%)M (15%)Q (25%)H (30%)
如果只是普通分享码,用M基本够了;
如果中间放 Logo,通常更适合提高到Q或H。
从实现流程上看,二维码样式层的处理链路其实很简单:
五、为什么这页还要支持条码
这页不是单纯的二维码生成器,而是二维码 / 条码双模式。
这个设计很贴近真实业务:
- 海报、菜单、名片、活动页更常用二维码
- 商品标签、物流包装、库存编码更常用条码
页面支持的条码格式有 6 种:
CODE128EAN13EAN8UPCCODE39ITF14
其中条码不是交给外部条码库生成,而是直接在generateBarcodeDataURL里用 Canvas 绘制。
更关键的是,这页在生成前先做了格式校验:
switch(format){case'EAN13':return/^\\d{13}$/.test(data)case'EAN8':return/^\\d{8}$/.test(data)case'UPC':return/^\\d{12}$/.test(data)case'ITF14':return/^\\d{14}$/.test(data)default:returndata.length>0}也就是说:
EAN13必须是 13 位数字EAN8必须是 8 位数字UPC必须是 12 位数字ITF14必须是 14 位数字
这一步非常重要,因为条码和二维码不一样,码制和输入不匹配时,不应该继续往下走。
六、批量生成才是这页最像“生产工具”的部分
如果这页只能一张一张生成,那它解决的只是轻场景。
真正把它和普通在线小工具拉开差距的,是批量模式。
页面目前支持这几种批量:
- 二维码
url - 二维码
text - 条码数据
规则很清楚:
- 每行一条
- 空行忽略
- 先生成首张预览
- 所有结果进入
batchItems
对应的基础函数就是:
functionparseBatchLines(raw:string):string[]{returnraw.split(/\\r?\\n/).map((s)=>s.trim()).filter(Boolean)}然后生成阶段会逐条循环处理,最后通过jszip打包:
constJSZip=(awaitimport('jszip')).defaultconstzip=newJSZip()constfolder=zip.folder('qrcodes')这一层还有几个做得很实用的细节:
1. 批量条码支持失败跳过
如果某几行条码数据不合法,页面不会整批报废,而是:
- 跳过失败项
- 保留成功项
- 把失败行号提示出来
2. 文件名做了清洗
如果原始内容里带有这些字符:
/ \\ ? % * : | " < >会先替换掉,避免 ZIP 内文件名出问题。
3. 批量预览不是把所有图都无脑塞满页面
页面的策略是:
- 右侧优先展示批量缩略图
- 下载区提供“全部 ZIP”
- 同时保留“首张 PNG”和“复制首张”
这套设计比只给一个“导出全部”按钮更顺手。
七、这页里几个很容易被忽略的实现细节
1. 不是所有二维码类型都开放批量
现在二维码批量只对url和text开放,这是一个很合理的边界。
因为像这些类型:
wifivcardevent
都不是“每行一段文本”就能舒服表达的。强行做成批量,输入体验反而会变差。
2. Logo 上传走的是本地 DataURL
页面用的是:
constreader=newFileReader()reader.onload=()=>{setQrConfig(prev=>({...prev,logo:reader.resultasstring}))}reader.readAsDataURL(file)这样后面的 Canvas 处理可以直接消费结果,不需要单独上传图片。
3. 生成逻辑做了轻微防抖
useEffect(()=>{consttimer=setTimeout(()=>{generateCode()},300)return()=>clearTimeout(timer)},[generateCode])这 300ms 看着小,但非常有必要。
否则用户每输入一个字,都会立即触发一轮生成和重绘。
4. 复制不是复制文本,而是复制图片
页面走的是 Clipboard 图片写入:
awaitnavigator.clipboard.write([newClipboardItem({[blob.type]:blob})])这比“复制链接”更接近日常使用场景,尤其是海报、文档和设计稿场景。
八、真实项目里最容易踩的 6 个坑
1. 二维码能扫,不代表扫出来就是对的
最常见的问题不是“图片坏了”,而是协议没拼对。
比如:
- WiFi 没用
WIFI: - 邮件没用
mailto: - 电话没用
tel: - 名片没按 vCard 格式拼
2. Logo 不是越大越好
Logo 放太大,会明显影响扫码率。
经验上更稳的做法是:
- Logo 占比控制在15% ~ 25%
- 同时把纠错级别提高到Q / H
3. 条码不是“随便输点数字”
不同码制有严格限制:
EAN13必须 13 位数字EAN8必须 8 位数字UPC必须 12 位数字ITF14必须 14 位数字
4. 批量下载时,文件名一定要清洗
否则真实数据里一旦带/、:、?之类字符,ZIP 文件很容易出问题。
5. 样式和可扫性是此消彼长的
圆角、点状、Logo 都会吃掉一定的扫码冗余。
样式做得越重,越要靠纠错级别兜底。
6. 批量预览不能把页面塞爆
几百张图全部直接渲染到 DOM 里,会让页面又重又乱。
这页现在“首张预览 + 批量结果 + ZIP 下载”的组合是更稳的方案。
九、这页真正解决的问题,不是“生成一个二维码”
如果一个页面只能做:
- 贴一个网址
- 生成一张黑白二维码
- 下载一张 PNG
那它最多只能算一个最基础的 demo。
而更接近真实业务的实现,至少要同时解决这些问题:
- 二维码 / 条码双模式
- 多种内容类型
- 协议化拼装
- 样式定制
- Logo
- 批量生成
- ZIP 打包下载
- 输入校验和错误提示
这页真正补齐的,不是“把内容变成一张码图”,而是把业务里的码生成流程补完整。
如果你也在做类似页面,最值得优先抄走的不是某一个库,而是这条链路:
动态表单 → 协议字符串 → 基础生成 → Canvas 二次样式 → 批量导出
这条链路一旦搭顺,后面不管你是补更多二维码协议,还是继续扩展条码格式,都会顺很多。
