news 2026/10/3 5:34:09

前端HTML生成条形码与MQ消息队列:从JsBarcode到幂等消费的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前端HTML生成条形码与MQ消息队列:从JsBarcode到幂等消费的完整实践

如果你在技术群里说“前端HTML生成条形码——MQ”,大概率会收到两种完全不同的反应:一种人以为你要在浏览器里画一个一维码,另一种人直接开始背八股“MQ怎么保证消息不丢、怎么解决幂等”。

这就是这个标题最有意思的地方——条形码和消息队列,本来是两条技术线上的东西,但在真实项目里经常被绑在一起讨论。比如扫码枪扫一个条码,前端把条码内容发到后端,后端再丢进MQ给下游处理;再比如订单号生成条形码后,用户手滑点两下提交,前端发了两个请求,MQ里到底算几条消息?这些都是能吵一整场的经典话题。

这篇文章我就把“前端HTML生成条形码”和“MQ”这两个点全拆开讲清楚:先给你一套能直接落地的前端条形码生成方案,从引入库到参数调优、扫不出来怎么排查全讲透;再讲清楚条码背后的MQ链路怎么设计,尤其是“前端点两次算不算两条消息”这种高频面试题,我把原理和工程解法都摆出来。不管你是只想要个二维码库被领导临时抓去做条码的前端,还是后端想搞懂消费者端幂等的朋友,这篇都值得花十分钟看一下。

1. 先把“MQ”这个歧义说清楚:它到底指什么

标题里的“MQ”是个典型的程序员式缩写,在不同语境下指代完全不同的东西,我干脆把两种都讲明白,这样你以后看到类似说法也不会懵。

1.1 场景一:把MQ当成“消息队列”

先看第一种,也是大多数人第一反应:MQ是Message Queue,消息队列。这种情况下的业务链路通常是这样的:

  • 前端在页面里用HTML/JS渲染一个条形码,比如仓库入库单上的物料编码;
  • 扫码枪扫到条码,前端拦截扫码枪输入(扫码枪本质是模拟键盘输入,后面有细节);
  • 前端把条码内容通过HTTP请求发给后端;
  • 后端把这条数据写入MQ,比如RabbitMQ、Kafka或RocketMQ;
  • 下游消费者异步处理,比如入库校验、库存扣减、打印标签。

这条链路里,条形码只是数据的物理载体,真正的核心矛盾在:消息进MQ之后的重复、丢失、顺序问题。所以很多人在讨论“前端HTML生成条形码——MQ”时,实际想聊的是“条码扫完之后的MQ处理方案”。

1.2 场景二:把MQ当成条码库的简称

另一种情况就更乌龙了。有朋友会把某些条形码库的缩写记混,比如我见过把JsBarcode叫成“JSM”、把bwip-js叫成“BWIP”、把二维码库qrcode叫成“QRZero”这种国内社区才会冒出来的叫法。如果你搜“前端HTML生成条形码MQ”,很可能是某篇博客里把生成库名写成了MQ,读者就抄去用了,结果越看越糊涂。

要特别提醒一下:条形码是一维码,二维码是二维码,生成方案完全不是一回事。二维码现在最流行的是qrcode库,但如果你要生成的是Code128、EAN-13这类一维条码,用qrcode是画不出来的。后面实操部分我会直接用JsBarcode这套最主流的方案。

1.3 为什么这两个东西经常被放在一起

我觉得更深层的原因是:条形码在仓库、零售、医疗、制造场景里,从来不是终点。扫码只是为了拿到一个ID,拿到ID之后要做的事情才是业务核心——而这些后续操作里,消息队列几乎是绕不开的。

所以你可以把“前端HTML生成条形码——MQ”理解成一句话:扫码获得数据,入队驱动流程。这个理解放到求职面试里也适用,因为面试官最爱问的正是从点击到消费的完整链路,以及链路上每个环节的可靠性。

2. 前端HTML生成条形码:从零到能用的完整实现

先撇开MQ不谈,我把前端生成条形码这件事给你做到极致,至少你拿到代码改改参数就能上线。

2.1 为什么我最后选了JsBarcode

前端能生成条形码的库,我实际用过的有JsBarcode、bwip-js,还有纯Canvas手写的(这个非常折磨人,不建议),简单对比一下:

对比维度JsBarcodebwip-js纯Canvas手写
使用难度低,API极简中,参数多但功能强极高,需了解编码规则
支持条码类型常见类型全覆盖支持超120种,工业级看你自己写多少
体积较小,gzip后几十KB稍大,功能全但包也大最小,但开发成本无限大
浏览器兼容很好,IE10+都行很好看你水平
维护活跃度活跃,社区案例多活跃全靠自己

我的结论很直接:常规业务选JsBarcode就对了。它API简单、文档清晰、条码类型覆盖了日常95%的需求,尤其是Code128和EAN-13这两个最常见的。bwip-js更强大,适合要做邮政码、GS1复合码这种工业场景,普通项目用不上那么重的。

注意:如果你的项目是纯内网、不加载CDN,建议把JsBarcode的库文件下载到本地静态资源目录,别在生产环境依赖外部CDN,否则页面白屏你哭都来不及。

2.2 最小可运行示例:一个HTML文件搞定

直接上代码,这个HTML文件你保存下来,浏览器打开就能看到条形码。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>前端HTML条形码生成演示</title> <!-- 用CDN引入JsBarcode,如果你内网部署,把这个文件下载到本地 --> <script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script> </head> <body> <svg id="barcode"></svg> <script> // 核心调用:一行代码生成条形码 JsBarcode("#barcode", "ABC-12345678", { format: "CODE128", width: 2, // 每个条的宽度(px) height: 80, // 条形码高度(px) displayValue: true, // 条码下方是否显示数字/文字 fontSize: 16, textMargin: 4 }); </script> </body> </html>

就这么简单。JsBarcode的第一个参数传选择器或DOM元素,第二个参数是你要编码的内容,第三个参数是配置项。浏览器打开后,一个Code128条码就出现在页面上了。

这里我解释一下为什么示例选择了Code128而不是别的格式:Code128支持全ASCII字符(字母、数字、符号都能编),是通用性最强的一维码,也是扫码枪兼容性最好的。如果你编码内容是纯数字,可以用EAN-13,但EAN-13有强制位数限制,玩不转再回来用Code128就行。

2.3 把条形码调到“能出货”的参数细节

很多同学照着官方示例把条形码画出来了,但打印出来扫码枪就是扫不动,或者贴到产品上颜值一塌糊涂。下面这些参数细节是我踩过几次坑之后总结出来的。

条宽(width)是最影响扫描率的参数。不要低于1.5px,低于这个值打印出来条和空的比例就糊了。不同条码类型对宽度是有要求的,你用喷墨打印机还好,用热敏纸打印机如果宽度设太小,条边上会有毛刺,扫码枪很容易识别失败。我常用的值是2到3。

高度(height)别省。有些同学为了省空间把高度压到30px,结果扫码枪要贴得很近才能识别。合理的做法是:Code128高度至少50到100px;EAN-13因为要带着下方数字一起读,建议不低于70px。高度太低会减少扫描线的覆盖角度,仓库场景特别容易翻车。

displayValue那个文本框别遮住条码。默认显示在下方,如果想放上面,设置textPosition: "top"和textMargin,给文字留出空间。还有一个容易忽略的参数叫text,可以自定义展示在条码下方的文字,但注意它只是展示用,不影响编码内容。如果你想在条码下方额外加一行描述,可以直接改配置:

JsBarcode("#barcode", "PO-20240115-001", { format: "CODE128", width: 2, height: 80, displayValue: true, text: "采购单号 PO-20240115-001", // 自定义展示文字 font: "monospace", textPosition: "bottom", textMargin: 6, fontSize: 18, background: "#ffffff", lineColor: "#000000" });

背景色和条色。background改背景色,lineColor改条的颜色。注意一维码最忌讳的配色是“浅条深底”,扫码枪红外线对深色背景不敏感,绝大多数场景老老实实白底黑条。不要为了设计感反着来,否则打包发货那边会骂你的。

留够边距(margin)。条码左侧和右侧一定要留白,专业术语叫“静区”(quiet zone)。静区不够,扫码枪分不清条码从哪里开始、到哪里结束。JsBarcode默认会留边距,但如果你自己做CSS压缩布局,小心别把条码贴到容器边缘。

2.4 动态刷新、Canvas和移动端适配

业务里条码内容很少是写死的,最常见的是从接口拿数据后动态生成。这时候你需要用JsBarcode的API重新渲染。

// 页面里既有条码区域 const barcodeElement = document.getElementById("barcode"); function renderBarcode(dataStr) { if (!dataStr) { barcodeElement.innerHTML = ""; // 清空 return; } JsBarcode(barcodeElement, dataStr, { format: "CODE128", width: 2, height: 80, displayValue: true }); } // 模拟接口返回数据后刷新 setTimeout(() => { renderBarcode("RECV-20250120-0001"); }, 500);

补充一个很容易踩的坑:如果你把条码渲染在<canvas>元素上,想获取它的图片数据做打印或上传,可以用toDataURL()。不过JsBarcode写canvas时会导致canvas内部被清空重画,如果你绑定了canvas的点击事件,记得每次渲染后重新绑定,或者用div事件委托。

移动端适配主要是两件事,一是width、height用固定像素没问题,因为条形码是要打印的,打印分辨率下固定像素反而稳定;二是如果用<svg>渲染,要检查一下SVG在iOS Safari里是否会被CSS拉伸变形。最简单的兜底方案是生成后转成dataURL图片塞给<img>,然后用object-fit控制展示比例。

// 生成后转图片展示 const canvas = document.getElementById("barcodeCanvas"); JsBarcode(canvas, "MOBILE-001", { format: "CODE128" }); const img = document.getElementById("barcodeImg"); img.src = canvas.toDataURL("image/png");

3. 条形码扫码之后,消息推送的“MQ”环节怎么设计

现在进入标题后半段的核心。条码画出来了,扫到了,数据拿到之后,如果系统架构里有MQ,你就要思考一条完整链路。这一个章节我重点讲三件事:链路怎么拆、前端重复提交算不算重复消息、消费者端怎么保证幂等。

3.1 链路拆解:从条码到MQ消息要经过哪几步

我以一个真实的仓库收货场景举例:

  1. 供应商送来的货贴有条码,格式是GRN-20250120-0089,代表一个收货单号;
  2. 仓库人员用扫码枪扫码,前端页面收到条码内容;
  3. 前端先做本地校验,比如正则判断格式是否合法;
  4. 前端调用后端接口POST /api/grn/receive,携带条码值;
  5. 后端接口幂等校验通过后,把业务数据写到数据库,状态置为“已接收”;
  6. 后端同时把消息发到MQ的grn.received主题/队列;
  7. 下游库存模块、财务模块、通知模块各自监听这条消息,更新数据或推送通知。

这里有一个很多人没注意的细节:扫码枪本质上是一个HID键盘设备,它扫出来的内容会直接作为键盘输入流进入页面。所以前端要做一层“扫码输入监听”,而不是让焦点停留在某个输入框里让扫码枪乱打一通。

我常用的做法是监听全局keydown,把短时间内的键盘输入拼接起来,遇到回车键就当成一次扫码完成:

let scanBuffer = ""; let lastKeyTime = 0; document.addEventListener("keydown", function (e) { const currentTime = Date.now(); // 如果距上次按键超过100ms,说明不是连续扫码输入,清空缓存 if (currentTime - lastKeyTime > 100) { scanBuffer = ""; } lastKeyTime = currentTime; if (e.key === "Enter") { e.preventDefault(); if (scanBuffer.length > 0) { handleScan(scanBuffer); } scanBuffer = ""; return; } scanBuffer += e.key; }); function handleScan(code) { // 此时拿到条码内容,可以继续后续逻辑 renderBarcode(code); // 回显条码 submitToBackend(code); // 提交后端 }

这一层监听很有用,很多仓库项目就是因为没做这层处理,扫码枪扫一次,页面输入框收到一串字符后还得手动回车,效率极低。

3.2 前端点两次,算不算发了两条消息

这个问题在热搜词里反复出现,我直接给结论:如果你没有做任何防重和幂等处理,前端点两次,后端就会处理两次,MQ里大概率会有两条消息。浏览器层面没有天然的“请求去重”机制,用户双击按钮、网络超时后重试、前端路由切换后重复提交,都会导致重复请求。

从产品层面看,一个收货单扫了一遍却因为是双击按钮被处理了两次,库存直接翻倍,这就是事故。所以这个问题不能只靠后端兜底,前端也要做防御。

前端层防重复的最简单方案:按钮loading + 禁用。

<button id="submitBtn" onclick="submitScan()">提交收货</button>
let submitting = false; async function submitScan() { if (submitting) { console.log("正在提交中,请勿重复点击"); return; } submitting = true; const btn = document.getElementById("submitBtn"); btn.disabled = true; try { const res = await fetch("/api/grn/receive", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code: "GRN-20250120-0089" }) }); // 业务处理 handleSuccess(await res.json()); } catch (err) { // 提交失败,允许再次点击 console.error(err); } finally { submitting = false; btn.disabled = false; } }

这样做能挡住90%的重复提交。但还不够,因为如果用户在按钮置灰的瞬间通过浏览器控制台手动调用接口、或者后端调用超时但实际已成功,前端永远不知道真相。这时候必须引入后端幂等机制。

后端幂等的关键:给每个业务操作一个唯一ID。

最简单实用的方案是:前端生成一个Idempotency-Key,比如UUID或基于条码+时间戳生成的唯一串,在请求头里带上。后端收到请求后,先用这个Key查Redis,如果存在就直接返回第一次的结果;如果不存在,则执行业务逻辑并把Key写入Redis。这样即使前端发两次,后端也只会真正处理一次。

// 前端生成幂等键 function generateIdempotencyKey(code) { return `GRN:${code}:${Date.now()}`; } // 请求时携带 const resp = await fetch("/api/grn/receive", { method: "POST", headers: { "Content-Type": "application/json", "Idempotency-Key": generateIdempotencyKey("GRN-20250120-0089") }, body: JSON.stringify({ code: "GRN-20250120-0089" }) });

这个头加不加,区别很大:不加,Redis里没有去重数据,后端处理两次;加了,后端可以用它去重,MQ里自然只有一条消息。

3.3 消费者侧的幂等:消息重发是常态,处理不好才是事故

到了MQ消费者这边,我要说一个很多新手理解不了的事实:MQ消息重复是正常现象,不重复才是运气好。

为什么?三个环节都可能出现重复:

  • 生产者重试导致重复:后端发送消息时,如果confirm超时,生产者会认为发送失败并重试,但第一次的消息其实已经到达Broker,于是队列里出现两条一样的消息;
  • Broker自动重投:消费者处理完业务但没有及时发送ack,Broker等超时后认为消费失败,把消息重新投递;
  • 消费端Rebalance:消费者宕机或扩容触发分区重平衡,部分消息会被重新分配并消费一次。

所以消费者必须写幂等逻辑,这是硬性要求。我用最经典的“业务主键去重”方案给你演示:

假设消费者的任务是处理收货单:

// 伪代码,用Redis做去重 public void onMessage(ReceiptMessage msg) { String dedupKey = "RECEIPT_PROCESSED_" + msg.getReceiptNo(); // 用SETNX,只有第一次返回true boolean firstProcess = redisTemplate.opsForValue() .setIfAbsent(dedupKey, "1", Duration.ofHours(24)); if (!firstProcess) { log.warn("重复消息,直接跳过: {}", msg.getReceiptNo()); return; } try { // 真正的业务逻辑:扣减库存、更新状态、发送通知 inventoryService.deduct(msg.getSkuId(), msg.getQuantity()); receiptService.markProcessed(msg.getReceiptNo()); } catch (Exception e) { // 业务失败要删掉去重Key,否则下次重试会被挡 redisTemplate.delete(dedupKey); throw e; } }

这段代码里有两个关键点:

一是去重Key必须用业务唯一标识,比如收货单号GRN-xxx,不要用MQ自带的messageId。因为同一个业务操作,即使重试,业务单号不变;而messageId每次发送都可能不同(生产者重试会生成新id),用它去重等于没用。

二是业务失败时要删掉去重Key。假如第一次消费时扣库存抛异常,你如果没有删Key,第二次消费(MQ重投)直接命中Key跳过,业务就永久失败了,这是“假幂等”。

如果不想引入Redis,还能用数据库唯一索引实现同样的效果,思路是在表里加一个biz_unique_key字段,插入时撞唯一约束就说明重复了,捕获异常后直接返回成功。这个方案在单体应用里比Redis更可靠,因为和业务在同一个事务里,不会出现Redis和数据库不一致的问题。

4. 常见问题与排查技巧实录

这一章我把实际项目中遇到的高频问题整理成一个速查表,方便你遇到问题时对着排查。

4.1 条形码扫不出来,问题往往不在条形码本身

现象可能原因排查方向
扫码枪完全没反应扫码枪没切换为键盘模式看说明书,调成USB HID键盘模式
能扫但内容多一位少一位页面焦点问题,扫码枪输入到了输入框用3.1里的全局keydown拦截
扫出来是一串数字但业务报错格式与预期不符,比如Code128扫出来带前后缀检查条码内容是否有隐藏字符
打印出来扫不动条宽太窄或打印分辨率低width设到2以上,打印用300dpi
手机扫码能扫但扫码枪不行屏幕反光纸张打印测试,纸质才是实际场景
标签盖了膜之后扫不动覆膜反光干扰改用哑光膜

这里面最容易忽略的是“隐藏字符”。如果条码内容里带了\n或者ASCII控制字符,扫描枪扫出来后可能表现为比预期多一个回车。你可以在全局keydown拦截里把回车当成扫码结束符,但如果条码内容里本身就有换行,就会提前触发一次错误扫码。解决办法是编码时不要包含不可见字符,库里渲染前先做replace(/[\x00-\x1F]/g, "")清理。

4.2 一维码能存多少信息,为什么扫码枪经常串码

一维码不是为“存大量数据”设计的。Code128能存的字符数取决于条的密度,一般能存几十个字符,但条数越多、条码越长,打印出来密度越高,扫描失败率就越明显。

工程上的原则是:条码只放业务ID,不放业务详情。比如只放GRN-20250120-0089,收货人、供应商、商品详情都通过ID查数据库。这样条码短、打印清晰、扫描快,数据库挂了页面至少还能扫出ID。千万别把一整张JSON塞进条码里,一时省事将来各种问题都来了。

串码(扫到旁边的条码)的原因也很简单:相邻条码距离太近,扫码枪的激光束同时扫到了两个条码的静区。解决办法是条码间距拉大,至少留出条码本身高度的1/4以上,或者每个条码外面加一个白底边框。

4.3 MQ相关面试高频点速查

既然标题带了MQ,我顺手给你一份面试时最常问的点,不深挖每个理论,但保证你听到题目不慌:

面试题核心回答思路
消息为什么会重复生产者重试、Broker重投、消费者未ack导致重新投递
怎么保证消息不重复消费业务幂等:唯一键去重、数据库唯一索引、状态机校验
前端点两次算两条消息吗如果不做幂等就算;前端要做loading禁点,后端要做幂等键
幂等和去重的区别幂等是结果一致,去重是过程防重;去重是幂等的一种实现手段
怎么保证消息不丢失生产者confirm、Broker持久化、消费者手动ack,缺一不可
消息堆积了怎么办先扩容消费者,再查是否有消费者卡死,最后看是否处理逻辑太慢

我特别说一下“手动ack”这件事。很多人用RabbitMQ时图省事用自动ack,消费者一旦在业务处理中途宕机,消息就已经被标记为已消费,这条消息就永远丢了。正确做法是消费成功后再ack,业务失败就basicNack并决定是否重新入队。这样虽然可能带来重复消费,但至少不会丢消息——重复和丢失二选一的话,宁重复勿丢失,因为重复可以用幂等兜底,丢了就真的没了。

至于幂等方案选哪家,我的建议是:能用数据库唯一约束就别先上Redis;能用RedisSETNX撑住高频场景就不需要引入分布式锁。最简单可靠的方案往往最不容易出问题。

5. 写在最后的实操体会

做前端条码生成这件事,最大的坑反而在“条码本身之外”。我最早做仓库项目的时候,花了一整天研究JsBarcode的字体、边距、颜色,觉得条码漂亮极了,结果打印出来扫码枪死活不认。后来才明白,条码不是用来好看的,是给机器读的——你要优先听扫码枪的意见,而不是自己的审美。

条码和MQ结合的项目我也踩过“假幂等”的坑。当时只加了一个Redis去重Key,以为万事大吉,结果有一次消费时库存扣减成功、订单状态更新失败抛了异常,消息重投后直接命中Key跳过了,最后只能手动补数据。从那以后我养成了一个习惯:写消费者的时候,先问自己一句“如果消息被处理了三次,业务会不会出问题”。如果不敢回答“没问题”,那幂等就没做到位。

你可以先把文章里的JsBarcode代码跑起来,生成一个属于你业务的小条码,然后写个定时重发的模拟脚本,去验证一下你的消费者到底是不是真的幂等。这种实验比看多少篇文章都管用。等你把“条码生成”和“消息重复”这两件事都真正弄透了,再遇到类似的技术讨论,你就能一眼看出对方卡在哪一环了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 5:32:39

DMLS协议实战:智能电表通信的协议栈、OBIS对象与调试要点

简介&#xff1a;这是面向电力行业电能量数据采集终端开发者的 DMLS&#xff08;即 DLMS&#xff0c;IEC62056 协议族&#xff09;中文协议说明手册&#xff0c;旨在为采集终端与 DMLS 协议族电能表的通讯提供协议理解与实现参考。资源包约 620KB&#xff0c;共 1 个 doc 文档&…

作者头像 李华
网站建设 2026/10/3 5:31:25

从零构建AI工程:落地必备的六大核心能力

1. 为什么“从零构建AI工程”不是一句口号&#xff0c;而是当前最真实的生存技能最近三个月&#xff0c;我连续参与了四家不同规模企业的AI落地咨询&#xff0c;从刚融资的AI原生初创公司&#xff0c;到传统制造业的数字化转型部门&#xff0c;再到高校实验室的技术转化项目。一…

作者头像 李华
网站建设 2026/10/3 5:29:53

GPT-6与Opus 5.5双模型接入:用ServBay搭建统一AI网关的完整实践

1. 当两个旗舰模型同时降价&#xff0c;开发者真正该关心什么GPT-6 价格腰斩、Opus 5.5 上线&#xff0c;这两件事凑在一起&#xff0c;最直接的结果就是——原本因为成本问题只能"二选一"的团队&#xff0c;现在有了同时接入两个模型的空间。但问题也随之而来&#…

作者头像 李华
网站建设 2026/10/3 5:29:37

轮廓系数详解:聚类质量评估的数学原理与工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 5:29:17

知识管理实操框架:三道过滤网与四把手术刀

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 5:27:23

8GB显存跑35B大模型:消费级显卡本地部署完整实录

老实讲&#xff0c;看到“消费级显卡本地大模型实测&#xff1a;8GB 跑 35B 的完整实录”这个标题&#xff0c;我第一反应是“谁疯了&#xff1f;”但做技术的人嘴硬没用&#xff0c;得拿结果说话。这几天网上到处都是“消费级显卡跑glm-5.3”“本地大模型部署”的热搜词&#…

作者头像 李华