尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

H5扫码实战:jsQR与html5-qrcode选型及摄像头扫码原理

H5扫码实战:jsQR与html5-qrcode选型及摄像头扫码原理 简介在移动端网页开发中二维码识别是高频需求用户往往希望不安装独立 App直接用浏览器完成扫码。此份资源面向这类 H5 扫码场景以 jsQR、html5-qrcode 和 uni-app 交叉集成为主线既解释纯前端摄像头视频流中的二维码定位与解码也展示在 uni-app 工程中引入扫码库的页面配置和生命周期处理思路。压缩包共 43 个文件包含 13 个 TypeScript 文件、9 个 JavaScript 文件、4 个 Vue 组件、2 个 HTML 页面以及 JSON 配置、CSS/SCSS 样式和说明文档整包大小约 488KB目录清晰地区分了源码、静态资源、依赖包与全局配置便于按需查阅。已有 3565 人浏览学习实战参考价值较高。具体内容包括调用 getUserMedia 获取摄像头权限、利用 Canvas 绘制帧画面、通过 jsQR 逐帧识别二维码数据、用 html5-qrcode 定义相机初始化和扫码回调同时补充了 HTTPS 环境限制、移动端兼容性以及 uni-app 打包 H5 时的常见注意事项适合需要快速落地扫码功能的前端开发者直接参考和改造。 H5扫码这个需求我最早是接一个内部系统登录入口时接触到的。当时第一版图省事直接上了html5-qrcode一上午就跑通了当时还觉得这玩意儿真简单。后来需求加了“自定义扫码框和扫码结果连续读取”的要求我才发现用这个库去改细节远不如直接上jsQR自己拼来得顺手。这两个库我后来都仔细用过这里就把它们的原理、选型逻辑和实战中踩过的坑一起整理出来。1. 扫码这件事在H5里是怎么跑通的从摄像头到二维码结果很多人一开始会把H5扫码想简单了觉得既然手机有摄像头那网页里直接调一下不就行了。实际上H5端的扫码链路比原生App要长不少而且每一环都有兼容性问题。整个流程可以拆成这样打开摄像头 → 拿到视频流 → 把视频帧画到Canvas → 从Canvas读取像素数据 → 交给二维码解码库 → 解出字符串 → 停止摄像头。这里头最关键的一个认知是“取流”和“识别”是两件事。摄像头取流靠的是浏览器的navigator.mediaDevices.getUserMedia接口这是浏览器底层能力而二维码识别靠的是纯JavaScript算法库比如jsQR就是拿一张图片的像素数据来做图像处理和解码。这两部分互相独立甚至可以拆开用。还有一个容易被新手走歪的路子有人觉得可以用input typefile captureenvironment acceptimage/*让用户拍照再识别。这条路我在技术方案评审时直接否决了因为它的体验是断层的用户先拍一张再等图片上传再拿到识别结果扫码的“连续感”完全没了。而且拍照解析的成功率受手抖、对焦影响非常大远不如实时取流扫描来得自然。所以正经的H5扫码方案归根结底就是getUserMedia Canvas 解码库这三件套的组合。搞清楚了这条链路再看jsQR和html5-qrcode的区别就非常清楚jsQR只是链路里“识别”这一环的解码库它不关心你的视频流怎么来也不帮你渲染界面而html5-qrcode是把你从getUserMedia到解码到UI显示整个闭环都封装好了的一个完整解决方案。这两者的定位完全不同选哪个不能凭感觉得看你的具体场景。2. jsQR 与 html5-qrcode两款开源方案的定位差异与选型依据这两个库在社区里经常被放在一起比较但它们的抽象层级其实完全不同。对比维度jsQRhtml5-qrcode定位纯二维码解码库摄像头UI解码完整封装是否包含摄像头取流不包含需自己调 getUserMedia内置取流逻辑是否生成UI组件无任何UI有默认UI也可只取核心类样式/交互定制完全自由自己画需要覆盖内置样式定制成本较高包体大小体积极小压缩后几十KB级别相对更重包含取流与UI逻辑典型使用场景自定义扫码页、特殊交互逻辑快速集成、内部工具、原型依赖环境需Canvas/ImageData支持需浏览器兼容getUserMedia选型我的经验是看两件事一是你对扫码页的UI要求高不高二是你愿不愿意自己维护取流和扫描循环的代码。如果只是给内部后台做个扫码入口容纳扫码框在屏幕正中间html5-qrcode直接new一个类调用start方法摄像头、取流、扫码、提示UI全给你弄好了十分钟搞定。但如果你要在扫码框上叠加自己的视觉、要做连续扫码、要在扫码区域画特殊图形或者你的页面本身就有复杂的Canvas渲染逻辑那就别折腾html5-qrcode的覆盖层了老老实实用jsQR自己写取流和扫描逻辑反而更可控。顺带提一个细节jsQR的识别函数接收的是ImageData也就是说它的输入是“一张图片的像素点数组”你完全可以不依赖摄像头拿内部的二维码图片截图或者上传的图片去调用它。这个特性在做“从相册选图识别”的需求时特别有用。而html5-qrcode虽然也支持文件扫描模式但主体思路仍然围绕摄像头实时取流。3. 用 jsQR 从零写一个扫码页核心代码与原理解释如果你想用小成本实现一个完全可控的H5扫码页jsQR是很顺手的选择。下面这套代码是我在项目里实际用过的结构清晰每一步都有明确的意图可以直接往下抄。3.1 摄像头获取g etUserMedia的参数选择不只是写个true先看核心代码async function startCamera() { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new Error(当前浏览器不支持摄像头调用); } const stream await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: environment } }, audio: false }); const video document.getElementById(video); video.srcObject stream; await video.play(); return stream; }这里有个很有意思的细节facingMode我写的是{ ideal: environment }而不是environment字符串。原因是ideal字段是一个“理想值建议”浏览器在硬件不支持时可以退而求其次返回任何可用的摄像头而如果直接传字符串environment在某些Android WebView里会被当成强约束一旦设备没有标记为“环境摄像头”的后置摄像头整个调用会直接失败。实测下来Android上某些定制系统的WebView对facingMode支持并不好ideal写法能最大限度提高兼容性。3.2 扫描循环不要每一帧都去解码视频流拿到后不能直接拿去识别它还是视频流格式需要先绘制到Canvas上再用getImageData取出像素点数组。真正的核心代码如下const canvas document.createElement(canvas); const ctx canvas.getContext(2d, { willReadFrequently: true }); let scanning false; function scanFrame(timestamp) { if (!scanning) return; if (video.readyState video.HAVE_ENOUGH_DATA) { const width video.videoWidth; const height video.videoHeight; canvas.width width; canvas.height height; ctx.drawImage(video, 0, 0, width, height); const imageData ctx.getImageData(0, 0, width, height); const code jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: dontInvert }); if (code code.data) { handleResult(code.data); return; } } requestAnimationFrame(scanFrame); } function startScan() { scanning true; requestAnimationFrame(scanFrame); }这里我说三个值得注意的点。第一个是getContext(2d, { willReadFrequently: true })。这个参数很多教程不会提但如果你用Canvas做频繁的像素读取这个hint能让浏览器选择更适合读像素的底层实现避免性能卡顿。加上这个参数在部分设备上能明显减少getImageData的延迟强烈建议写上。第二个是inversionAttempts: dontInvert。jsQR默认会自动尝试区分“白底黑码”和“黑底白码”这个尝试需要额外算力。日常业务场景基本都是白底黑码所以可以直接关掉反转尝试省下一部分CPU。如果你发现部分深色底的二维码识别不出来再改成attemptBoth也不迟。第三个是关于扫描频率的取舍。理论上requestAnimationFrame每秒跑60次很正常但每一次都要执行drawImage、getImageData、jsQR解码这三步特别是getImageData在较大画布上非常耗内存和CPU。我在低端Android机上实测过全速扫描会让手机发烫、掉电飞快。所以在实际项目里我通常会在循环里加一个间隔判断比如每隔200ms或300ms才真正解码一帧。别小看这个间隔它牺牲的只是毫秒级的识别速度换来的是设备不发热、不卡顿。3.3 停止扫描摄像头灯不灭的罪魁祸首扫码成功后第一件事不是弹恭喜而是释放摄像头。很多人第一次写都会漏掉这一步结果扫码成功后摄像头指示灯一直亮着页面退出了还占着麦克风权限。async function stopCamera(stream) { scanning false; if (stream) { stream.getTracks().forEach(track track.stop()); } const video document.getElementById(video); if (video) { video.srcObject null; } }这里要彻底释放不只是video.srcObject null。真正占用摄像头硬件的是底层MediaStream里的track你必须把每个track的stop()调用一遍硬件资源才会真正释放。这也是为什么我在项目里会用一个全局变量记住当前stream因为video.srcObject虽然能拿回stream对象但清理时直接遍历原始stream引用更稳妥。3.4 识别结果处理与连续扫码识别结果本身就是一个字符串可能是URL、可能是纯编号、也可能是JSON字符串。拿到结果后具体怎么处理看业务。如果要做连续扫码比如扫完一个继续扫下一个注意扫描成功后不要直接重新开摄像头正确的做法是清理状态、重置视频元素然后重新调用startScan()。如果只是扫一次就跳转直接在handleResult里做跳转并关闭摄像头就行。4. 用 html5-qrcode 快速集成适合把功能先跑起来的场景如果你的需求就是“页面要有扫码功能摄像头调起来二维码放进去能识别出来”那你真的没必要自己手写html5-qrcode一行start就能实现。import { Html5Qrcode } from html5-qrcode; const scanner new Html5Qrcode(reader); scanner.start( { facingMode: environment }, { fps: 10, qrbox: { width: 250, height: 250 } }, (decodedText) { console.log(识别结果, decodedText); scanner.stop() .then(() { // 处理结果并关闭 }) .catch(err console.error(err)); } ).catch(err { console.error(扫码启动失败, err); });这里的reader是一个页面里已经存在的div的idhtml5-qrcode会在这个div里生成视频画面和扫码框。fps: 10是每秒扫描次数qrbox是扫码框尺寸。我实际测试下来fps 在 10-15 之间体验最好超过15识别速度没有明显提升CPU消耗反而明显上去了。要切换前后摄像头可以用它提供的Html5Qrcode.getCameras()静态方法获得摄像头列表然后调用scanner.switchCamera()。不过这个方法在不同浏览器的行为略有差异iOS上适配还凑合Android部分WebView调用后偶尔会出现画面倒置或者切换失败实测不要依赖切换摄像头的API关键场景干脆返回重开。html5-qrcode还有一个Html5QrcodeScanner子类它会直接生成包含“开始/停止”按钮的完整UI开箱即用。但它的样式是内置的想改按钮文案和扫码框样式比较费劲需要盖上去。所以如果你要跟现有设计系统保持一致我更推荐用Html5Qrcode核心类加自定义UI。5. 实战中的高频问题与对应处理方案这两套方案我都跑过生产环境这里把最常遇到的问题和排查思路整理出来遇到直接对照。5.1 摄像头打不开HTTPS、权限与容器限制H5调用摄像头的最硬性条件是HTTPS 或 localhost。Chrome和Safari都明确规定非安全环境下getUserMedia直接不可用。所以如果测试环境是内网IP的HTTP地址摄像头必然打不开先去搞定HTTPS再说。第二个常见坑是WebView容器。同一个页面在浏览器里能扫码嵌到Android原生WebView里就黑屏大概率是宿主App没有拦截并授权WebView的权限请求。原生WebView里要跑摄像头宿主得在WebChromeClient里实现onPermissionRequest回调并返回grant应用本身还要声明CAMERA权限。这个问题的排查特征是网页代码没报错但getUserMedia的Promise一直pending或直接reject。在微信、钉钉、企业微信这类内置浏览器里跑H5扫码授权弹窗的表现各不相同但总体可用。要注意的是这些浏览器对“用户拒绝授权后再次请求授权”的策略差异很大有些浏览器拒绝一次后就不再弹窗页面也无法判断用户到底有没有拒绝。所以在扫码页最外层放一个“如果扫不了请尝试手动输入”的兜底入口这个几乎是我做H5扫码项目的必选项。5.2 识别率低二维码清晰度与扫描频率的平衡如果你发现二维码明明在画面里就是扫不出来多半不是库的问题而是输入给解码器的画布尺寸和二维码在画面中的像素比例不匹配。jsQR的解码质量直接取决于二维码在整张画布中占了多少像素所以画布尺寸不能一味改小。我的调试经验是把Canvas尺寸保持在video.videoWidth和video.videoHeight的原始分辨率不做缩放。这样二维码在画面中的像素密度最高识别距离最远。省性能的办法是用间隔扫描来控制频率而不是缩小画布。如果你把画布降到一个很小的尺寸识别距离会肉眼可见地变短这在扫码场景里是非常差的体验。另外在UI层面加上“请将二维码放入框内并保持稳定”的提示文案非常有用看似老土实际上很多扫码失败都是用户手持不稳、二维码没在画面中间造成的。5.3 扫码后的资源清理与路由体验摄像头资源不释放这个坑我前面已经重点强调过这里再说一个实际体验问题扫码成功后页面跳转如果前一个页面的stream没有被正确清理用户返回时会发现摄像头重新唤起甚至出现摄像头被占用、无法再次扫码的问题。我习惯的做法是扫码成功拿到结果后先把扫描循环停掉再关摄像头最后再执行跳转逻辑。顺序不能反因为你还没停掉循环的话跳转过程中可能又有一帧被解析出来触发重复处理。状态标志位和stream清理逻辑建议放在同一个方法里前后顺序固定死避免一次漏调。5.4 内置浏览器的页面返回与跳转差异热词里提到的“微信打开H5怎么去掉自带的返回条”这个问题在扫码场景里一样会遇到。微信内置浏览器的UI是它自己控制的前端无法直接移除或修改返回栏。能做的事情只有一个在业务路由层做文章把扫码前和扫码后的页面尽量组织成同一个H5应用内的路由跳转减少对浏览器返回键的依赖。还有一点值得注意在小程序web-view里加载H5摄像头权限的支持非常有限尤其在iOS上经常调不起来摄像头。如果你在小程序生态里需要扫码优先选小程序原生的wx.scanCodeAPI而不是让H5去做扫码。H5扫码方案的主力场景是独立的移动端网页或者是企业微信、钉钉等App内置的工作台页面。6. 最后再说说选型的一些个人体会跑完这几个项目我自己的选型逻辑基本定型如果扫码场景只是内部系统的一个入口几乎没有定制化需求直接用html5-qrcode它把摄像头、UI、识别这个闭环都处理好了短平快解决问题如果扫码页是用户面对的核心功能有视觉要求或者你需要把识别能力和自己已有的Canvas渲染流程结合起来那jsQR是那个更干净的底层零件。另外如果你只需要识别一张现成的图片比如用户从相册上传的二维码截图那jsQR会更直接因为它本身就是吃ImageData的解码器不需要启动摄像头。而html5-qrcode在这个场景下还得走它内置的文件扫描逻辑反而多一层封装。最后给一个屡试不爽的小技巧无论用哪个库扫码页都放一个“手动输入”的备选入口。摄像头权限一旦被系统禁掉页面再智能也救不回来这个时候一个输入框加确认按钮能让你的功能不至于彻底瘫痪。这个兜底设计我在好几个项目里都被它救过。本文还有配套的精品资源点击获取
返回列表