纯前端二维码 / 条码生成器:从协议拼装到批量 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它不是一个重渲染工具页真正的难点在于协议拼装、状态组织、批量流程和错误处理。一、整体架构输入 → 协议 → 生成 → 预览 → 导出二维码条码否是是用户选择模式二维码 / 条码填写动态表单内容类型拼装协议字符串url / wifi / vcard / event ...校验码制与输入长度generateQRDataURL是否需要样式加工直接预览drawStyledQRCanvas 二次绘制generateBarcodeDataURL下载 PNG复制图片是否批量jszip 打包 ZIP从代码实现上看这页主要分成 5 层模式层二维码 / 条码切换输入层根据内容类型渲染不同表单协议层把表单数据转换成真正可编码的字符串生成层二维码走generateQRDataURL条码走generateBarcodeDataURL导出层下载单张、复制、批量 ZIP这里最关键的一点是真正决定结果对不对的首先不是样式而是协议字符串是不是拼对了。如果把这页的最短主流程再压缩一层其实就是下面这 6 步选择模式填写表单拼装内容字符串生成二维码 / 条码即时预览下载 / 复制 / ZIP二、为什么这个页面不能只做一个输入框很多二维码页面只有一个大输入框输入任意文本 - 生成二维码这种做法只能覆盖最基础的纯文本场景。而这个页面把二维码内容拆成了 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){caseurl:contentdata.url||breakcasetext:contentdata.text||breakcaseemail:contentmailto:${data.email||}?subject${encodeURIComponent(data.subject||)}body${encodeURIComponent(data.body||)}breakcasephone:contenttel:${data.phone||}breakcasesms:contentsms:${data.phone||}?body${encodeURIComponent(data.message||)}breakcasewifi:contentWIFI:T:${data.encryption||WPA};S:${data.ssid||};P:${data.password||};${data.hiddentrue?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:zhangsanexample.com ORG:GeekFormat END:VCARD这样扫码之后很多系统会直接进入“保存联系人”的流程。3. 日程不是纯文本而是事件协议事件类型最终拼的是BEGIN:VEVENT SUMMARY:产品评审会 LOCATION:会议室 A DTSTART:20260608T100000Z DTEND:20260608T113000Z DESCRIPTION:讨论二维码改版 END:VEVENT所以这页最核心的价值不是“生成二维码图片”而是把高频业务场景正确映射成规范字符串。四、二维码生成只是第一步样式层还要再走一遍 Canvas页面里真正生成二维码的函数是generateQRDataURLreturnQRCode.toDataURL(content,{width:size,margin:2,color:{dark:fgColor,light:bgColor},errorCorrectionLevel:errorLevel})如果只是黑白标准二维码到这里已经够了。但页面实际还支持这些配置前景色 / 背景色尺寸纠错级别方形 / 圆角 / 点状中心 Logo这时就不能只停在qrcode的默认输出而是要把生成出来的二维码再画进 Canvas 里继续处理。对应的就是drawStyledQR。1. 圆角 / 点状本质是二次裁剪这两种样式不是重新算二维码矩阵而是对基础二维码图片做遮罩裁剪。2. Logo先垫白底再贴图页面处理 Logo 的逻辑是constlogoSMath.floor(size*(logoSize/100))constx(size-logoS)/2consty(size-logoS)/2ctx.fillStylebgColor ctx.fillRect(x-4,y-4,logoS8,logoS8)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。从实现流程上看二维码样式层的处理链路其实很简单否是协议字符串qrcode 生成基础 DataURL是否需要样式加工直接预览 / 下载Canvas 二次绘制圆角 / 点状 / Logo五、为什么这页还要支持条码这页不是单纯的二维码生成器而是二维码 / 条码双模式。这个设计很贴近真实业务海报、菜单、名片、活动页更常用二维码商品标签、物流包装、库存编码更常用条码页面支持的条码格式有 6 种CODE128EAN13EAN8UPCCODE39ITF14其中条码不是交给外部条码库生成而是直接在generateBarcodeDataURL里用 Canvas 绘制。更关键的是这页在生成前先做了格式校验switch(format){caseEAN13:return/^\\d{13}$/.test(data)caseEAN8:return/^\\d{8}$/.test(data)caseUPC:return/^\\d{12}$/.test(data)caseITF14:return/^\\d{14}$/.test(data)default:returndata.length0}也就是说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)).defaultconstzipnewJSZip()constfolderzip.folder(qrcodes)这一层还有几个做得很实用的细节1. 批量条码支持失败跳过如果某几行条码数据不合法页面不会整批报废而是跳过失败项保留成功项把失败行号提示出来2. 文件名做了清洗如果原始内容里带有这些字符/ \\ ? % * : | 会先替换掉避免 ZIP 内文件名出问题。3. 批量预览不是把所有图都无脑塞满页面页面的策略是右侧优先展示批量缩略图下载区提供“全部 ZIP”同时保留“首张 PNG”和“复制首张”这套设计比只给一个“导出全部”按钮更顺手。七、这页里几个很容易被忽略的实现细节1. 不是所有二维码类型都开放批量现在二维码批量只对url和text开放这是一个很合理的边界。因为像这些类型wifivcardevent都不是“每行一段文本”就能舒服表达的。强行做成批量输入体验反而会变差。2. Logo 上传走的是本地 DataURL页面用的是constreadernewFileReader()reader.onload(){setQrConfig(prev({...prev,logo:reader.resultasstring}))}reader.readAsDataURL(file)这样后面的 Canvas 处理可以直接消费结果不需要单独上传图片。3. 生成逻辑做了轻微防抖useEffect((){consttimersetTimeout((){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 / H3. 条码不是“随便输点数字”不同码制有严格限制EAN13必须 13 位数字EAN8必须 8 位数字UPC必须 12 位数字ITF14必须 14 位数字4. 批量下载时文件名一定要清洗否则真实数据里一旦带/、:、?之类字符ZIP 文件很容易出问题。5. 样式和可扫性是此消彼长的圆角、点状、Logo 都会吃掉一定的扫码冗余。样式做得越重越要靠纠错级别兜底。6. 批量预览不能把页面塞爆几百张图全部直接渲染到 DOM 里会让页面又重又乱。这页现在“首张预览 批量结果 ZIP 下载”的组合是更稳的方案。九、这页真正解决的问题不是“生成一个二维码”如果一个页面只能做贴一个网址生成一张黑白二维码下载一张 PNG那它最多只能算一个最基础的 demo。而更接近真实业务的实现至少要同时解决这些问题二维码 / 条码双模式多种内容类型协议化拼装样式定制Logo批量生成ZIP 打包下载输入校验和错误提示这页真正补齐的不是“把内容变成一张码图”而是把业务里的码生成流程补完整。如果你也在做类似页面最值得优先抄走的不是某一个库而是这条链路动态表单 → 协议字符串 → 基础生成 → Canvas 二次样式 → 批量导出这条链路一旦搭顺后面不管你是补更多二维码协议还是继续扩展条码格式都会顺很多。