简介这是一款基于HTML5与JavaScript的条形码/二维码扫码插件核心采用html5-qrcode库帮助Web前端开发者快速在网页中集成摄像头实时扫描能力适用于电商商品查找、物联网设备配对、移动支付验证等场景。压缩包共88个文件大小约9.27MB主要包含TypeScript源码与编译后的JS库文件、markdown说明文档、png/gif/jpeg示例图片以及json/yml等配置文件目录结构覆盖核心解码、相机状态管理、原生条形码检测、zxing-js第三方解码等模块并内置Electron、Vue.js、原生HTML5等多端示例目录便于在不同框架中对照学习。目前已有2127人学习该资源。通过阅读源码与配套示例可掌握getUserMedia权限申请、扫描区域初始化、回调函数处理以及停止扫描的完整流程并理解实时图像识别原理、性能优化与XSS安全防护思路对需要自研或二次开发扫码功能的Web前端开发者具有直接参考价值。无论是直接集成还是学习原理都能从中获得清晰指引。1. 扫一扫条形码和二维码一个网页插件怎么把摄像头变成扫码枪写过仓储盘点和门店核销页面的前端应该都经历过同一个尴尬业务方甩来一张带条码的物料图说“你帮我扫一下”而你手边只有浏览器。实际上htmljs 里的扫一扫条形码和二维码早就不用依赖原生 App 了。浏览器可以直接调摄像头一个开源的 JavaScript 扫码插件就能完成从画面取流、条码定位到结果回调的整条链路几十行代码就能产出可用的功能页面。这套方案适合给 Web 端 ERP、MES、会员核销、图书盘点这类场景补上扫码能力也适合不想为一个小功能单独打包 App 的团队直接把插件嵌进现有系统里跑。2. 解码链路与选型扫一扫背后发生了什么为什么是 html5-qrcode 而不是 jsQR2.1 一维码和二维码的差别解码器到底在解什么条形码一维码本质是按条纹宽度比例编码的数字和字符超市收银台最常见的 EAN-13、物流贴单上的 Code128、图书馆使用的 Code39 都属于这一类。解码器识别的是条纹的相对宽度比它对横向分辨率特别敏感条形码必须横着、完整地铺满取景框识别率才上得去。二维码的思路完全不同QR Code 靠左上、右上、左下三个角的寻像图形定位内容压缩进矩阵里还带了里德-所罗门纠错。这意味着码面被挡住一块、打印有点糊、甚至沾了污渍解码器也能靠容错级别把内容还原出来。实际体验中二维码远比一维码抗造一维码稍微缺墨拉丝就解不动这是两类码的物理特性决定的不是插件写得不好。常见码制的差异可以看这张表码制编码内容典型场景容错能力识别难点EAN-1313 位数字商品零售低横向拉伸必须到位Code128ASCII 全字符物流、制造业中密度高对焦距敏感Code39数字大写字母图书、汽车行业低需要较大的码面QR Code任意字符串、二进制支付、链接、单据高L/M/Q/H反光、遮挡但大多可纠错搞清楚码制差异有个实际好处当现场报“扫不出来”时你至少能判断是码面质量的问题、取景框尺寸的问题还是格式列表配置的问题而不是把锅全甩给插件。一维码需要的横向空间、二维码需要的完整寻像图形这两条是排查时最先要确认的硬条件。2.2 三种主流方案的取舍html5-qrcode / jsQR / zxing-js结论放在前面做产品级页面我一般直接上 html5-qrcode如果你要完全自己定 UI、自己控制摄像头画布而且只扫二维码jsQR 更轻zxing-js 是 Java 生态 Zxing 的 JS 移植码制覆盖最全但体积大正常 Web 项目用它是杀鸡用牛刀。三者的能力边界方案摄像头管理码制范围体积维护状态适用场景html5-qrcode内置 getUserMedia、扫码框 UI、手电筒QR、EAN、Code128 等 20 种中等活跃开箱即用的产品页面jsQR不自带需自己调取摄像头流主要是 QR Code一维码支持很弱小低频更新私有 UI 的二维码识别zxing-js不自带PDF417、DataMatrix 等全码制大更新缓慢特殊码制的硬需求html5-qrcode 内部把图像采集、帧抽取、格式解析封装成了黑匣子对外暴露两个类。Html5QrcodeScanner 自带完整 UI页面放一个 divrender 之后它会自己渲染摄像头画面、取景框和关闭按钮Html5Qrcode 是去掉 UI 的底层封装适合要把扫一扫融进自定义布局的场景。这两个类在同一个库里先按需求选定入口再谈参数配置。这里其实还可以对比另一条路线用扫码枪硬件。一个 USB 扫码枪本质是模拟键盘输入插上就能用但它解决不了线上 H5 页面的扫码诉求——用户没有那台硬件。而微信的 JS-SDK 只能在微信内置浏览器里跑出了微信就失效。html5-qrcode 这类纯浏览器插件没有这两个限制这是它在这个场景里站得住的核心原因。团队里如果已经有 Vite 工程直接 npm 安装这个包就行不需要额外引入原生插件。2.3 版本锁定与资源加载为什么不能一把梭 CDN latesthtml5-qrcode 的 1.x 和 2.x 在回调行为上不兼容1.x 的扫描器回调直接传识别文本2.x 的 render 回调传的是 (decodedText, decodedResult) 结构没看文档就升级线上会直接出 undefined。所以引用时一定要锁版本号别用 latestscript srchttps://cdn.jsdelivr.net/npm/html5-qrcode2.3.8/html5-qrcode.min.js/script这段脚本建议放在页面底部或者加 defer 让它在 DOM 解析完再执行。锁版本的好处是库升级不会悄悄改变线上行为坏处是不会自动吃到 bug 修复所以我的习惯是跟着 2.x 的 minor 版本走隔两个月人工看一次 changelog评估完再升。本地调试时用 latest 图省事没问题提交前务必改回锁定版本。另外这个库没有像 Vue 那样的全局安装机制直接 script 引入后挂在 window.Html5Qrcode 下用模块化打包时记得处理全局变量冲突。提示生产环境必须固定版本号2.3.x 的构造参数与 1.x 不兼容升级前先看类型定义。3. 把扫一扫装进页面最小可用实现的完整拆解3.1 页面骨架摄像头取景框需要哪几块一个能跑的最小页面只需要三样东西一个放扫码界面的容器 div、一个触发启动的按钮、一个显示结果的区域。我把 html5-qrcode 官方 demo 精简成下面这个结构你可以直接复制成 html 文件在本地跑!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title扫一扫条形码和二维码 demo/title script srchttps://cdn.jsdelivr.net/npm/html5-qrcode2.3.8/html5-qrcode.min.js/script /head body div idreader stylewidth: 100%; max-width: 480px;/div button idstartBtn onclickstartScan()开始扫码/button p识别结果span idscanResult stylecolor: #d00;未识别/span/p script function startScan() { const scanner new Html5QrcodeScanner( reader, { fps: 10, qrbox: { width: 220, height: 220 }, rememberLastUsedCamera: true, showTorchButton: true }, false ); scanner.render( function (decodedText) { document.getElementById(scanResult).textContent decodedText; }, function (errorMessage) { console.log(识别失败帧 errorMessage); } ); } /script /body /html这段代码的职责分得很清楚div#reader 是扫描器的挂载点扫描器渲染出的摄像头视频流和扫码框都会被塞进这个容器按钮负责触发 startScan符合摄像头硬件对用户手势的要求span#scanResult 承接识别结果。render 的第二个回调每帧都可能触发一次正常识别时它会被频繁调用所以只做日志输出不要在失败回调里做弹窗这类高成本操作。3.2 初始化与回调Html5QrcodeScanner 三个参数的含义Html5QrcodeScanner 构造函数接收三个参数第一个是容器 id第二个是配置对象第三个是 verbose 开关控制台要不要打印内部调试信息。实际项目里我把 verbose 设成 false否则一帧报一次错控制台会被刷爆。配置对象里最常用的几个字段字段取值示例作用与注意事项fps10每秒处理的帧数越高识别越快但 CPU 和耗电也越高低端机建议 5-8qrbox{ width: 220, height: 220 }取景框尺寸二维码用方形一维码要横向长条aspectRatio1.0 / 16:9 等视频流宽高比建议与取景框形状匹配否则画面变形formatsToSupport数组声明要识别哪些码制越短越好会显著影响识别速度和误识别率rememberLastUsedCameratrue记忆上次使用的摄像头避免多摄像头设备每回都要重选showTorchButtontrue自动渲染手电筒按钮暗光场景的救命配置render 的成功回调拿到的是识别文本和 decode 结果对象。这里有个常见误区二维码内容不一定是一个 URL它可能是 JSON 字符串、纯编号、甚至带特殊字符的文本。回调里第一件事应该是先判断字符串格式再决定接下来的业务动作而不是无脑 location.href 跳走。识别错误回调在没扫到码时几乎每秒都在触发千万别在这个回调里加防抖弹窗否则用户会被提示框淹没。3.3 把一维码加进来formatsToSupport 的配置与取舍如果你的业务主要是扫条形码比如仓储物料、图书 ISBN只配置二维码是不够的得把一维码格式加进 formatsToSupport。注意这个字段是放在构造函数第二个参数config里的不是 render 方法的参数const scanner new Html5QrcodeScanner( reader, { fps: 8, qrbox: { width: 320, height: 120 }, formatsToSupport: [ Html5QrcodeSupportedFormats.QR_CODE, Html5QrcodeSupportedFormats.EAN_13, Html5QrcodeSupportedFormats.EAN_8, Html5QrcodeSupportedFormats.CODE_128, Html5QrcodeSupportedFormats.CODE_39, Html5QrcodeSupportedFormats.UPC_A ], rememberLastUsedCamera: true }, false ); scanner.render(onScanSuccess, onScanFail);注意我把 qrbox 从方形改成了宽 320、高 120 的横向矩形。一维码的条纹是横向排列的解码器要在一帧图像里采集足够多的条纹宽度信息取景框越宽单帧内能采到的完整码面就越大识别率提升非常明显。fps 也从 10 降到了 8原因是低帧率下每一帧的曝光时间更长条纹边缘更清晰这算是我在低配安卓机上试出来的经验值。formatsToSupport 有个容易被忽略的副作用你启用的格式越多解码器在每帧里要试的匹配路径就越多识别耗时和误识别率都会上升。实际项目里我会按业务把码制收窄到最小集合比如只扫 ISBN 就只留 EAN_13 加 EAN_8别把 CODE_128 也带上。代码里的 Html5QrcodeSupportedFormats 是一个枚举对象可以在初始化前 console.log 出来确认每个格式的常量名避免拼写错误。4. 业务对接扫码结果怎么变成表单数据、请求和跳转4.1 扫码结果的三种消费方式解析、回填、提交扫码识别出文本只是开始业务上要处理的是文本后面的动作。我在真实项目里遇到过三种主要消费方式结果是一个 URL 就跳转结果是一个单据号就回填表单结果需要和后端校验就发请求。下面这段 onScanSuccess 是我常用的模板let lastResult null; let resolveTimer null; function onScanSuccess(decodedText, decodedResult) { // 防抖同一个码在连续帧里会被重复识别 if (decodedText lastResult) return; lastResult decodedText; clearTimeout(resolveTimer); resolveTimer setTimeout(() { lastResult null; }, 2000); const trimmed decodedText.trim(); // 先用 js 判断字符串类型再决定业务动作 if (/^https?:\/\//i.test(trimmed)) { window.location.href trimmed; return; } // 兼容后端 Java 体系生成的二维码内容比如 easypoi 导出的单据码 if (/^[A-Z]{2}\d{12,20}$/.test(trimmed)) { document.getElementById(orderNoInput).value trimmed; submitValidate(trimmed); return; } // 兜底当成普通文本回填 document.getElementById(orderNoInput).value trimmed; } async function submitValidate(orderNo) { const resp await fetch(/api/order/validate, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ orderNo }) }); const data await resp.json(); document.getElementById(statusText).textContent data.valid ? 单号有效 : 单号不存在; }这段逻辑里有三个值得注意的地方。第一防抖用的 lastResult 加 2 秒窗口避免用户扫一次码页面发出多次请求但 2 秒后要复位否则用户连续扫两个内容相同的码第二个会被吞掉。第二正则判断字符串类型用的是 js 里的常见写法先锚定开头再匹配格式避免把普通文本误当成 URL 跳走。第三后端如果是 Java 技术栈easypoi 之类工具导出的二维码内容往往有一定的单号前缀规范正则的前两位字母匹配就是给这类单号留的口子具体规则按你们业务的实际单号格式调整。4.2 移动端适配全屏取景、Android WebView 权限、iOS 安全要求扫一扫主要跑在手机上移动端适配比桌面端敏感得多。摄像头取流依赖 getUserMedia浏览器对安全上下文有硬性要求HTTPS 域名、localhost 或 WebView 内的安全配置三者缺一不可。iOS 上从 16 开始对摄像头权限提示更频繁用户第一次进页面必须点允许这个交互没法跳过。Android WebView 里做 H5 扫码除了要在原生层申请 CAMERA 权限还要在 WebSettings 里显式开启 JavaScript 和媒体权限#reader video { width: 100%; height: auto; object-fit: cover; border-radius: 8px; }这段 CSS 是为了让视频流在容器内铺满而不变形。object-fit: cover 会让画面按容器比例裁剪适合全屏扫码的沉浸式布局如果扫码框只是页面里一小块可以把 object-fit 改成 contain画面完整但两侧会有黑边。容器本身不要设固定高度让视频流自适应即可否则不同机型的分辨率会把布局撑乱。Android WebView 的原生配置里需要在 onCreate 时这样补齐权限设置webView.getSettings().setJavaScriptEnabled(true); webView.setWebChromeClient(new WebChromeClient()); if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 100); }这段 Java 配置的作用是让 WebView 具备调起摄像头的入口权限。setJavaScriptEnabled 是基础WebChromeClient 负责处理网页里的 getUserMedia 授权请求CAMERA 运行时权限是 Android 6.0 之后的硬性要求缺任何一个环节页面里会一直停在“正在启动摄像头”的状态。如果你们的 App 用的是混合开发框架记得在框架的权限配置里也把 camera 声明加进去。4.3 手电筒与暗光torch 的启用条件和降级方案扫码场景里光线差是常态货架底层、仓库角落、晚上盘点全都很暗。html5-qrcode 的配置项里有一个 showTorchButton设成 true 会在扫码界面自动渲染一个手电筒按钮但它依赖设备摄像头对 torch 能力的支持不是所有机型都有。如果你想在自定义界面里自己控制手电筒可以用无 UI 的 Html5Qrcode 类配合 getRunningTrackCapabilities 和 applyVideoConstraints 实现。下面这段逻辑是基于无 UI 的 Html5Qrcode 类写的注意它和 Html5QrcodeScanner 是同一个库里的两个级别用 Scanner 时直接开 showTorchButton 就好function tryTurnOnTorch(html5Qrcode, callback) { html5Qrcode.getRunningTrackCapabilities().then(capabilities { if (capabilities.torch) { html5Qrcode.applyVideoConstraints({ torch: true }) .then(callback) .catch(() showBrightnessTip()); } else { showBrightnessTip(); } }); } function showBrightnessTip() { document.getElementById(lightTip).textContent 当前设备不支持补光请到光线充足处扫码; }这段代码里 getRunningTrackCapabilities 返回的是当前视频轨的能力描述对象torch 字段存在且为 true 才说明设备具备手电筒能力。applyVideoConstraints 是让已经启动的摄像头流应用新的约束torch 参数在运行中是热切换的不需要重启扫码器。注意这段代码要在扫描已经启动之后调用扫码还没开始就取 track 能力会拿不到东西。降级方案里我只会提示用户换环境不会尝试把屏幕亮度拉满这种玄学操作实测意义不大。5. 避坑排查扫码插件在真实环境里的五个翻车现场5.1 现象页面一直转圈控制台报 NotAllowedError原因NotAllowedError 是 getUserMedia 被拒绝的典型报错常见于两种情况。第一种是页面跑在 HTTP 非安全域名下浏览器直接禁止摄像头第二种是页面被嵌在 iframe 里iframe 标签没有声明摄像头权限授权弹窗根本弹不出来。还有团队把页面套在 Android WebView 里原生层没申请 CAMERA 运行时权限同样会以这个报错收尾。解决先在浏览器地址栏确认协议和域名localhost 和 HTTPS 是硬条件内网 IP 想用摄像头就得先解决证书问题。iframe 场景需要给 iframe 标签加 allowcamera 属性并且父页面和子页面都要 HTTPS。WebView 场景按前文 4.2 节的 Java 配置检查原生层重点看 WebChromeClient 有没有设置这一步经常被漏掉。5.2 现象二维码在手机上扫不出电脑却能秒识别原因这类问题多数不是插件坏了而是取景框可识别区域太小、格式列表过于宽松导致的。手机端摄像头默认对焦距离近qrbox 如果设成 150 像素以下二维码稍微拿远一点就占不满取景框解码器拿到的特征点不够同时格式列表太长每一帧要试的匹配路径多低端机 CPU 跟不上识别时间被拉长。解决把 qrbox 调大到屏幕宽度的 60% 到 70%保留 QR_CODE 一种格式fps 降到 5 到 8。另外可以在页面里加一行使用提示告诉用户“请让二维码填满取景框”这行提示对降低现场咨询量非常有效。还有一个隐蔽因素反光严重的屏幕码。手机屏幕本身会反射环境光摄像头的自动曝光会因此过曝此时把手机亮度调高一点或者让扫码设备与屏幕保持一点角度往往就出来了。5.3 现象一维码条形码识别率极低二维码却完全正常原因一维码的解码依赖横向条纹的宽度比例而很多人的扫码框沿用二维码场景的方形配置。方形框在竖屏手机上只占到中间一小条宽度完整的条形码根本进不了取景范围解码器看到的都是截断的条纹自然识别不出来。解决把 qrbox 改成宽高比 3:1 左右的横向矩形比如 { width: 320, height: 100 }同时提示用户横拿手机。一维码场景 fps 建议降到 5每帧曝光时间长条纹边缘更清晰。代码片段见 3.3 节的配置唯一要改的是 qrbox 的具体宽高按屏幕宽度自适应可以用 window.innerWidth 动态计算const qrboxWidth Math.floor(window.innerWidth * 0.8); const scanner new Html5QrcodeScanner( reader, { fps: 5, qrbox: { width: qrboxWidth, height: 100 }, formatsToSupport: [Html5QrcodeSupportedFormats.CODE_128] }, false );这段配置里我把 qrbox 宽度直接绑定了屏幕宽度的 80%竖屏和横屏都能拿到尽可能宽的取景范围。高度固定 100 像素是为了专注条形码所在的那条窄带避免把画面的其他区域也纳入检测减少干扰。格式列表只留 CODE_128按实际码制替换成 EAN_13 或 CODE_39 即可。5.4 现象扫码成功后回调连续触发同一个码录进去好几次原因扫码器在连续帧里反复识别到同一个码这是个正常行为而不是 bug。很多业务回调里直接做了落库或发起请求结果就是一次扫码产生多笔记录数据库里刷出一串重复数据这是最典型的回调没做防抖的翻车现场。解决在成功回调里维护一个 lastResult 全局变量相同结果直接 return然后用 setTimeout 在 2 秒后复位。前文 4.1 节的代码就是这个思路这里单独把关键行拎出来强调判断要在业务动作之前复位定时器要清掉旧的否则连续扫两个相同码会丢第二个。如果业务要求每单必扫且不允许重复可以把复位窗口拉长到 5 秒甚至改成扫码后强制停止视频流。5.5 现象iOS Safari 一打开页面就黑屏点了按钮才有画面原因Safari 对 getUserMedia 有用户手势限制页面 onload 时自动调用 render 启动摄像头会被直接拒绝页面表现就是一直黑屏或者停在加载状态。Android Chrome 上自动启动通常没问题所以这类问题只在 iOS 设备上暴露排查时很容易误判成插件兼容性问题。解决把所有启动扫码的代码放进按钮的 click 事件里由用户点击触发而不是 window.onload 自动执行。如果产品要求页面一进来就能扫折中做法是渲染一个半透明的遮罩层写着“点击开始扫码”用户点击后再调 render体验上接近自动启动但绕开了手势限制。顺带一提iOS 上扫码后如果有振动或音效反馈需求也得放在用户手势链里否则同样被拦截。6. 进阶验证不插摄像头用本地图片做离线回归测试摄像头扫码的调试成本很高每次都要拿手机对准屏幕、调整距离和光线改一次配置验证一次效率很低而且没法自动化。html5-qrcode 的底层 Html5Qrcode 类提供了一个不依赖摄像头的解码入口scanFile。它接收一个图片文件直接对图片做条码定位和解码返回 Promise 字符串。我用这个能力把扫码回归测试做成了离线脚本。const html5Qrcode new Html5Qrcode(reader); const testFile document.getElementById(testImageFile).files[0]; const decodedText await html5Qrcode.scanFile(testFile, false); console.log(离线解码结果 decodedText);scanFile 的第一个参数是图片文件对象第二个参数控制是否把图片渲染到页面上调试时设成 true 可以看到识别过程回归测试时设成 false 避免闪烁。这个方法内部同样会走完整的定位解码链路所以它验证的是“码本身能被解出来”这件事排除了摄像头、光线、对焦这些环境变量。注意 scanFile 要求页面里有个容器 div即使不显示图片构造函数里传的 id 也得真实存在。我现在的正式做法是这样的用二维码生成器比如 qrcode 库生成一张固定内容的 QR Code 测试图再准备一张 CODE_128 的条形码测试图两张图放进测试目录。每次改动扫码配置或者升级插件版本就打开一个本地测试页把这两张图依次拖进 scanFile 里跑一遍断言解码文本和预期一致。这个方法帮我拦住过一次真实事故有一次 formatsToSupport 改成只留 QR_CODE顺手把 CODE_128 也过滤掉了仓储那边的条形码全部扫不出来靠离线回归脚本在测试阶段就发现了没有推到生产环境。从那以后我每次提交扫码相关代码之前都强制走一遍离线图片回归再结合真机手动验证一次两条链路都过了才敢上。改版本号、改 qrbox、改格式列表全都要重新跑一次都不能偷懒。希望帮到你。本文还有配套的精品资源点击获取