ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

前端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 langzh-CN head meta charsetUTF-8 title前端HTML条形码生成演示/title !-- 用CDN引入JsBarcode如果你内网部署把这个文件下载到本地 -- script srchttps://cdn.jsdelivr.net/npm/jsbarcode3.11.6/dist/JsBarcode.all.min.js/script /head body svg idbarcode/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到100pxEAN-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消息要经过哪几步我以一个真实的仓库收货场景举例供应商送来的货贴有条码格式是GRN-20250120-0089代表一个收货单号仓库人员用扫码枪扫码前端页面收到条码内容前端先做本地校验比如正则判断格式是否合法前端调用后端接口POST /api/grn/receive携带条码值后端接口幂等校验通过后把业务数据写到数据库状态置为“已接收”后端同时把消息发到MQ的grn.received主题/队列下游库存模块、财务模块、通知模块各自监听这条消息更新数据或推送通知。这里有一个很多人没注意的细节扫码枪本质上是一个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 idsubmitBtn onclicksubmitScan()提交收货/buttonlet 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自动重投消费者处理完业务但没有及时发送ackBroker等超时后认为消费失败把消息重新投递消费端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代码跑起来生成一个属于你业务的小条码然后写个定时重发的模拟脚本去验证一下你的消费者到底是不是真的幂等。这种实验比看多少篇文章都管用。等你把“条码生成”和“消息重复”这两件事都真正弄透了再遇到类似的技术讨论你就能一眼看出对方卡在哪一环了。
返回列表