简介:面向H5页面与uni-app开发者的扫码功能实现包,整合jsQR与html5-qrcode两种解码方案,帮助前端工程师在浏览器中快速完成摄像头扫码、图片识别与跨端集成。压缩包共43个文件,以JavaScript脚本、TypeScript类型定义、Vue页面组件及JSON工程配置为主,另有HTML示例页、PNG测试图片和说明文档,整体仅488KB,目录结构清晰,便于按模块取用。目前已有3586人学习下载,适合需要快速落地扫码能力的Web与跨平台项目。资源内含可直接运行的uni-app项目骨架,包含manifest.json、pages.json、App.vue等工程文件,以及两种解码库的调用示例、常用工具函数和页面逻辑;配套的静态二维码图片可用来验证解析结果。通过对照该包,可系统掌握视频流获取、逐帧图像抽取、二维码定位识别、错误容错及HTTPS环境适配等完整流程,同时也可作为在现有工程中引入H5扫码功能的参考起点。
1. h5 扫码不是吊起摄像头就完事:jsQR 与 html5-qrcode 到底解决什么问题
在 H5 里做扫码,最容易遇到的黑匣子就是“摄像头已经亮了,二维码也怼在镜头前了,但结果一直是 undefined”。很多人以为 h5 扫码就是打开摄像头,其实摄像头只负责取景,二维码解析是另一套算法。h5 扫码这个需求要拆成“拿视频流”和“从图像帧里解码”两件事。jsQR 是一个纯前端二维码解码库,给它一张图像的像素数组,它返回二维码内容;html5-qrcode 则把摄像头调用、扫码框、连续识别这些环节打成包,底层识别用的同样是 jsQR。两者的关系,类似发动机和整车。这篇文章按落地顺序讲:先讲 jsQR 怎么搭一个最小扫码器,再讲 html5-qrcode 怎么少写代码,最后把微信、企业微信、App 内嵌 WebView 里的高频坑说清楚。适合正在做 H5 扫码、不想被手机型号和容器兼容性反复折磨的开发者。
2. 自己写一个最小 h5 扫码器:jsQR 的摄像头、画布与解码循环
2.1 getUserMedia 打开摄像头:分辨率、环境与前置摄像头的选法
在写 jsQR 之前,得先把摄像头打开。浏览器侧负责这件事的是navigator.mediaDevices.getUserMedia,它只在安全上下文里可用:线上必须 HTTPS,本地可以把 localhost 当成例外。我习惯优先让系统挑后置摄像头,因为扫码场景里,用户多数是去扫别人出示的二维码,前置摄像头拍出来是镜像,二维码方向一错,识别率就下来了。
async function openCamera(videoEl) { const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } } }); videoEl.srcObject = stream; await videoEl.play(); return stream; }facingMode: { ideal: 'environment' }是让浏览器优先选择后置摄像头。ideal表期望,不强制;设备没有后置时也会退回前置。改成{ exact: 'environment' }会强制抛OverconstrainedError,我一般不用 exact。width/height同样给 ideal,720p 而不是 1080p 是因为解码耗时会随像素量上涨,二维码识别并不需要 4K 画面,分辨率太高反而容易掉帧。
另一个容易被忽略的点:videoEl.play()在部分 Android WebView 里可能一直 pending。如果视频画面出不来,先检查 video 有没有playsinline属性,这个坑在第 4 章会再展开。
每次扫码结束后记得把摄像头停掉,否则指示灯常亮,用户会很慌。停止的方式是遍历轨道调用stop():
function stopCamera(stream) { stream.getTracks().forEach(track => track.stop()); }这个函数虽然简单,但真正会写的人不多。很多扫码页从 A 页面跳到 B 页面后,摄像头一直开着,再回来时浏览器报NotReadableError,就是因为上一个页面的流没有被释放。
2.2 把视频帧画到 canvas:为什么必须用 canvas 而不是直接截图
jsQR 不认视频元素,它认的是ImageData对象,也就是一帧画面展开后的 RGBA 像素数组。所以要想让 jsQR 工作,必须把视频当前帧画到一个 canvas 上,再getImageData取像素。有人会偷懒用video.toDataURL()先转 base64,再走一遍图片解析,这在单次截图上没问题,但做成每秒 10 帧的连续扫码,性能和内存都扛不住。
const scanCanvas = document.createElement('canvas'); const scanCtx = scanCanvas.getContext('2d', { willReadFrequently: true }); function captureFrame(videoEl) { const vw = videoEl.videoWidth; const vh = videoEl.videoHeight; scanCanvas.width = vw; scanCanvas.height = vh; scanCtx.drawImage(videoEl, 0, 0, vw, vh); return scanCtx.getImageData(0, 0, vw, vh); }drawImage(videoEl, 0, 0, vw, vh)会把 video 当前帧完整画到 canvas。这里有个关键参数:videoWidth和videoHeight是视频源的真实分辨率,不是 video 元素的 CSS 尺寸。如果元素设置了width="100%"的样式,video.videoWidth仍旧是原始分辨率。如果 canvas 尺寸小于视频尺寸,drawImage 会自动缩放,这个缩放是免费的,等于帮 jsQR 降采样。
我一般会把 canvas 宽度压到视频宽度的一半,比如源 1280 就画到 640,解码耗时会明显下降。scanCtx.getContext('2d', { willReadFrequently: true })里的willReadFrequently值得单独说:它告诉浏览器这个上下文会被高频调用getImageData,浏览器会为它选择更适合回读像素的存储方式。不加这个参数,在某些浏览器上连续取像素会慢得明显。
还有一个边界是 canvas 的宽高比不能和视频不一致,否则二维码会被拉伸变形,jsQR 的定位图形检测会失败。如果扫码框不是满屏,只截取框内区域,那就在 drawImage 的第 2 到第 5 个参数里设定源区域,这样还能顺便减少背景干扰,我最后一章会再讲。
2.3 调用 jsQR 解码:返回结果、失败判断与两个关键参数
图像数据准备好后,就可以交给 jsQR。函数签名是jsQR(imageData, width, height, options)。第一个参数必须是Uint8ClampedArray或直接传ImageData.data;width 和 height 要和 imageData 的宽高一致,填错会出现解码定位错误或者直接返回 null。
import jsQR from 'jsqr'; function decodeFrame(imageData) { const result = jsQR( imageData.data, imageData.width, imageData.height, { inversionAttempts: 'attemptBoth', greyScale: false } ); return result ? result.data : null; }识别成功返回result.data,内容可能是 URL、JSON 或普通文本;识别失败返回null,不是抛异常,所以不用 try/catch 包这一层。options 里我调的最多是inversionAttempts。扫码时常见两种反向画面:一种是黑底白字的二维码,一种是屏幕或镜头反光导致明暗反转,attemptBoth会让 jsQR 尝试正常和反色两种识别路径,成功率更高,但耗时也更高。如果你明确只扫自己生成的白底黑码,比如内部系统的固定码,可以设成dontInvert,解码速度会有可感知的提升。
greyScale默认是 false,我保持默认。有的教程会建议先手动灰度再传给 jsQR,实际上 jsQR 内部会做自适应二值化,提前转灰度可能丢失二维码边缘的对比度。
2.4 连续扫码的 requestAnimationFrame 循环与节流:别让手机变成暖手宝
实时扫码是一个持续动作,摄像头每秒送来 30 帧,但不该每帧都去调 jsQR。我之前为了追求“秒识别”,把 fps 调高,结果是识别没快多少,手机先烫了,iOS 上还出现热降频,识别反而更慢。现在的做法是:用 requestAnimationFrame 保持视频预览的流畅,但用一个时间戳门槛控制实际解码频率。
let scanning = true; let lastDecodeTime = 0; let activeStream = null; function loop(timestamp) { if (!scanning) return; if (timestamp - lastDecodeTime > 200) { const imageData = captureFrame(videoEl); const text = decodeFrame(imageData); if (text) { scanning = false; handleSuccess(text); stopCamera(activeStream); return; } lastDecodeTime = timestamp; } requestAnimationFrame(loop); } requestAnimationFrame(loop);timestamp - lastDecodeTime > 200就是节流:每 200ms 解码一次,对应 5fps 的解码频率。200ms 是我在多数中端 Android 上的折中值。如果页面里只有扫码一个任务,可以缩到 150ms;如果扫码的同时还要上传轨迹、渲染列表,放到 300ms 更安全。scanning = false放在成功回调之前,是为了防止用户按住手机时同一个二维码被连续识别多次。真实业务里扫码成功跳转前,最好也加一个 500ms 的结果防抖:第一次弹窗用户没点,第二次又识别到同一个码,容易重复提交。
到这里,最小扫码器已经能用了:打开摄像头、取帧、解码、循环、停止。剩下的大多数问题不是代码逻辑,而是手机摄像头和环境光,这部分的坑我在第 4 章集中写。
3. 用 html5-qrcode 少写一半代码:npm 依赖、扫码区配置与 Vue 生命周期
3.1 html5-qrcode 和 jsQR 的分工:库内部还是用 jsQR 解码
如果你不想自己管理摄像头、canvas、requestAnimationFrame 和节流,html5-qrcode 是更务实的方案。它是一个 npm 包,内置了摄像头选择、扫码框、失败提示、文件识别这些完整逻辑。很多人以为 html5-qrcode 和 jsQR 是并列的两种选择,其实不是:html5-qrcode 的二维码识别核心用的还是 jsQR,它把摄像头权限、视频流、帧提取和解码循环都封装好了。换句话说,先用 jsQR 自己写一遍,是为了理解它的边界;用 html5-qrcode 是为了让代码量和维护成本下来。
安装方式就是常规的 npm 依赖:
npm install html5-qrcode这个包自带 TypeScript 类型,不需要额外安装 @types 包。引入路径上注意,浏览器环境直接使用Html5Qrcode和Html5QrcodeScanner两个类,避免手滑写成import html5Qrcode from 'html5-qrcode',默认导入不是类。
3.2 最小调用:Html5QrcodeScanner 与 Html5Qrcode 两种模式
这个库提供两套 API,名字容易混:Html5QrcodeScanner自带一套扫码 UI,包括摄像头选择下拉框、扫码区域和“更换摄像头”按钮,适合快速上线;Html5Qrcode是底层类,不带 UI,适合要自己画扫码框、做个性化界面的情况。我大部分业务场景用的是 Scanner,因为扫码页的设计通常由 UI 同学先行,调用代码最少。
import { Html5QrcodeScanner, Html5QrcodeSupportedFormats } from 'html5-qrcode'; const scanner = new Html5QrcodeScanner( 'qr-reader', { fps: 10, qrbox: { width: 250, height: 250 } } ); scanner.render( (decodedText) => { console.log('识别到:', decodedText); scanner.clear(); }, (error) => { // jsQR 每帧解码失败都会走这里,别把错误吐给用户 } );这里的fps不是摄像头本身帧率,而是 html5-qrcode 每隔多久从视频流里抽一帧去解码,10 对应每 100ms 一帧。qrbox是扫码区域,宽高给的是实际像素,不是比例。render的第一个参数是成功回调,第二个是每帧失败回调。注意:jsQR 识别失败非常频繁,尤其当前几帧画面里没有二维码时,失败回调可能每秒执行几十次,千万别在里面做弹窗、上报或者 toast。
Html5Qrcode的用法更接近自己管理摄像头:
const html5QrCode = new Html5Qrcode('qr-reader'); await html5QrCode.start( { facingMode: 'environment' }, { fps: 10, qrbox: 250 }, (decodedText) => console.log(decodedText) ); // 页面离开时一定要 stop,否则摄像头不会关闭 await html5QrCode.stop();start的第一参数可以传摄像头 id 字符串,也可以传{ facingMode: 'environment' }这种约束。它返回 Promise,摄像头权限被拒时会 reject,记得用 try/catch 包住,不然页面上会留下一个未处理的 Promise 错误。
3.3 扫码框、二维码格式白名单、每秒帧数的参数设置
scanner 的配置项比想象中多,但真正值得调的也就几个。formatsToSupport用来限定只识别二维码,避免相机被一维码带偏;rememberLastUsedCamera可以让用户第二次进入时继续用上次选的摄像头;showTorchButtonIfSupported是手电筒开关,适合夜间扫屏幕码的场景。
const config = { fps: 10, qrbox: { width: 240, height: 240 }, aspectRatio: 1.0, formatsToSupport: [Html5QrcodeSupportedFormats.QR_CODE], rememberLastUsedCamera: true, showTorchButtonIfSupported: true };几个常见参数的作用整理如下:
| 参数 | 常见值 | 说明 |
|---|---|---|
| fps | 10 | 每秒解码帧数,不是摄像头帧率 |
| qrbox | { width: 240, height: 240 } | 扫码识别区域,像素单位 |
| aspectRatio | 1.0 | 视频画面宽高比,避免二维码变形 |
| formatsToSupport | [QR_CODE] | 只识别二维码,不去追一维码 |
| rememberLastUsedCamera | true | 下次打开记住上次摄像头 |
| showTorchButtonIfSupported | true | 手电筒开关,仅支持的设备出现 |
aspectRatio我建议设成 1.0 或者和扫码框接近的比例,这样视频画面不会因为拉伸导致二维码变形。qrbox是识别区域的像素尺寸,不是整个视频的尺寸;它越小,解码时越省 CPU,但也要求用户把二维码放到框内。实际扫码场景里,用户往往习惯离远一点扫,框太小时识别失败率会上升,我的经验是宽度至少占屏幕宽度的 70%。
3.4 在 Vue 组件里封装:start/stop 与 beforeDestroy 的生命周期管理
html5-qrcode 在 Vue 项目里最常见的坑是重复初始化。Scanner 一旦渲染到某个 DOM 节点,就不可再对同一个节点调用第二次 render,否则会报错。原因在于路由切换时组件销毁,但摄像头和扫描循环没跟着停。解决方法是把 scanner 实例存在组件作用域里,在路由组件卸载时主动 clear 或 stop。
<script setup> import { onMounted, onBeforeUnmount } from 'vue'; import { Html5QrcodeScanner } from 'html5-qrcode'; let scanner = null; onMounted(() => { scanner = new Html5QrcodeScanner('qr-reader', { fps: 10, qrbox: { width: 250, height: 250 } }); scanner.render(onScanSuccess, onScanFailure); }); onBeforeUnmount(() => { if (scanner) { scanner.clear(); scanner = null; } }); function onScanSuccess(text) { // 跳转前避免连续回调多次 scanner.clear(); console.log(text); } function onScanFailure() {} </script>clear()和stop()有一点区别:Scanner 实例用clear(),Html5Qrcode 实例用stop()。如果用的是 Vue 2 的 Options API,就把清理逻辑放到beforeDestroy里;Vue 3 用onBeforeUnmount或onUnmounted。还有一个容易漏的:scanner.clear()之后,同一组件如果再次进入 mounted,需要重新 new Scanner,不能复用旧实例。
4. h5 扫码翻车避坑指南:权限、对焦、双镜头与误码率排查
4.1 权限被拒或 getUserMedia 不存在:先检查是不是 HTTPS 和 iframe 权限
现象:navigator.mediaDevices是 undefined,或者getUserMedia一直返回NotAllowedError;还有一种是在微信里点扫码,摄像头权限弹窗一闪而过,页面拿到 no permission。
原因:浏览器摄像头 API 只在安全上下文里开放,http 页面访问非 localhost 域名时直接禁用;iframe 嵌套时还需要在 iframe 标签上声明 allow 属性;部分 Android WebView 如果不实现onPermissionRequest,H5 拿不到授权。
解决:线上页面强制 HTTPS,调试用 localhost;如果业务要嵌到别的平台,让客户端开发在 WebView 配置里允许摄像头权限;iframe 场景在标签上加allow="camera"。我一般会在 getUserMedia 之前先做环境判断:
if (!navigator.mediaDevices?.getUserMedia) { showError('当前浏览器不支持摄像头,请使用 HTTPS 或最新版浏览器'); }不要等到catch里才提示,undefined 的情况根本没有 Promise 可 reject。
4.2 二维码怼在镜头前也扫不出来:光线、反光和对焦是三大玄学
现象:摄像头能看到画面,二维码也清晰,但 jsQR 持续返回 null;尤其是拿手机去扫另一个手机屏幕,屏幕上出现彩色条纹,然后识别失败。
原因:手机屏幕刷新和摄像头传感器之间存在频闪;环境光过强或过暗都会让二维码二值化失败;摄像头对焦没有锁定,二维码边缘发虚。
解决:把扫码框放在屏幕中央,提示用户距离控制在 10-30cm;夜间开启手电筒或者引导用户调节环境亮度;如果场景是扫屏幕码,可以在页面上增加一个“切换摄像头”或“打开手电筒”按钮。html5-qrcode 的showTorchButtonIfSupported只对支持的设备有效,不要指望所有 Android 都有。自己写 jsQR 循环时,如果连续识别失败超过几秒,就在 UI 上给“光线不足或距离太近”的提示,不要盲目调大 fps,帧率再高也解决不了对焦问题。
4.3 iOS 和安卓 WebView 表现不一致:有些设备 getUserMedia 一直 pending
现象:同一个 H5 页面,Android Chrome 扫码正常,iOS Safari 也正常,但嵌入 App 的 WKWebView 后摄像头打开了,画面却卡住,或者一直黑屏;部分 iOS 上点击开始扫码后,页面直接全屏播放视频。
原因:iOS WebView 对媒体播放有额外要求:页面里的 video 要加playsinline属性,不然调用play()时会进入全屏播放;WKWebView 还需要在 App 的 Info.plist 里声明NSCameraUsageDescription。另外,getUserMedia在 WebView 里如果不在用户手势触发下调用,授权回调可能一直不回来。
解决:给 video 元素加playsinline和muted;确认 App 的 WKWebView 配置允许媒体权限;扫码入口按钮的点击事件里再调用getUserMedia,不要在页面加载时就自动打开摄像头。如果 App 是别人家做的,H5 无法控制客户端,那就退而求其次,在页面上加一个“调起系统扫码”的降级方案,走微信 JSSDK 或原生 bridge。
4.4 误码率高、识别到错误内容:inversionAttempts、灰度、扫码区域调优
现象:识别结果偶发不对,明明扫的是 A 二维码,返回的却是上一帧 B 二维码的内容;或者同一个二维码,时灵时不灵。
原因:jsQR 每帧都从当前画面解一次码,用户移动手机时,画面里同时出现两个二维码,jsQR 不保证返回哪个;如果解码循环没有加冷却,识别成功后没有立刻停,下一帧可能又触发一次回调。另外,inversionAttempts开成attemptBoth后,反色尝试遇到低对比度画面时,可能把噪声误识别成码。
解决:识别成功回调里先置scanning = false,再处理业务;给结果加一个最短响应间隔,比如 500ms 内的重复结果直接忽略;如果业务允许,只识别框内区域,减小背景干扰。用 jsQR 时可以在 drawImage 时只截取扫码框范围的图像,而不是全画面,这样误码率会明显下降。还有一种做法是把inversionAttempts设成dontInvert,前提是你确定二维码一定是白底黑码,比如自己生成的码,识别速度会快不少。
5. 把扫码页装进微信/企业微信/App 内嵌 H5:容器适配与结果处理
5.1 微信/企业微信内打开扫码页:JSSDK 的扫一扫和 WebRTC 如何选
如果页面只在微信内使用,还有一个更省事的方案:微信 JSSDK 的wx.scanQRCode,它直接调起微信原生扫码,识别速度和成功率都比网页摄像头好。但它的问题是只能在微信内置浏览器里用,而且 JSSDK 签名配置本身也是一道门槛。企业微信客服场景里打开的 H5,签名逻辑和企业微信的agentConfig绑定,不像普通公众号那样只配一个wx.config就完事,很多团队在这里卡住。
如果不想绑死微信,或者产品要求同一个 H5 在微信、普通浏览器、App 内都能访问,那还是用 html5-qrcode 的方案。我的选择依据是:目标用户全部在微信内,用 JSSDK;存在混合分发,用 html5-qrcode 加降级。判断当前是不是微信环境,常规做法是看navigator.userAgent里有没有MicroMessenger,但企业微信的 UA 里同时有wxwork和MicroMessenger,要分开处理。
5.2 App 内嵌 WebView 的摄像头授权与 WebRTC 白名单
很多 App 内嵌 H5 后,摄像头打开失败,第一反应是 H5 代码有问题,其实问题经常在客户端 WebView 配置。iOS 的 WKWebView 必须先在 Info.plist 里加NSCameraUsageDescription,否则 getUserMedia 直接拒绝;Android WebView 需要在 WebChromeClient 里重写onPermissionRequest,并且 grant 之后还要允许页面继续使用摄像头。单纯让 H5 上 HTTPS 是不够的。
如果你控制不了客户端,至少要在 H5 端把错误类型区分开:NotAllowedError是权限问题,NotFoundError是设备没有摄像头,NotReadableError是摄像头被其他程序占用或权限冲突。弹给用户的话术完全不同。“摄像头正在被占用”在 Android 上很常见,比如用户刚用微信扫过码,再切到 App 里扫,摄像头没释放干净,就会出现这个错。
5.3 uniapp 封装 H5 指向多个域名时的摄像头权限坑
有同学问过“uniapp 封装 h5 如何指向 2 个域名”,这个场景和扫码结合时有一个隐蔽问题:浏览器把摄像头授权存在“源”上。在 A 域名授权后,跳到 B 域名,B 源没有授权记录,又要重新弹一次权限框;如果 B 域名是 http,那直接在源层面就没有摄像头能力。所以 uniapp 的 H5 如果分发给多个域名,一定要保证所有入口都是 HTTPS,并且主动告诉用户第一次进入时需要重新授权。
另一个坑是 uniapp 的chooseImage和真正的摄像头权限不是一回事。很多 uniapp 页面用<input type="file" capture>只能拿到相册或拍照结果,这不是持续的视频流扫码。要在 uniapp H5 里做实时扫码,仍然得写自己的navigator.mediaDevices.getUserMedia或引 html5-qrcode,uniapp 组件层帮不上忙,只有 App 端走原生插件才另说。
5.4 扫码结果处理:跳转、上传、图片 blob 与键盘联动
扫码成功后的处理比想象中更影响体验。如果结果是 URL,直接location.href跳转会丢失当前扫码页的历史记录,用户按返回键可能直接退出;我一般用location.replace或在新窗口打开,给用户一个停留在原页面的选择。如果结果是 JSON 字符串,别急着JSON.parse,先try/catch,因为二维码里的 JSON 经常带不可见字符或被二次编码。
有的业务需要把扫码连带的二维码图片保存到服务器,这时把相机当前帧截出来转成 blob 上传是常见做法。h5 里的 blob 文件是能上传的,前提是 canvas 来源没有被污染,getUserMedia 的视频帧不会污染 canvas,可以放心toBlob:
scanCanvas.toBlob(async (blob) => { if (!blob) return; const formData = new FormData(); formData.append('file', blob, 'scan.png'); formData.append('code', decodedText); await fetch('/api/upload', { method: 'POST', body: formData }); }, 'image/png');扫码完成后如果需要跳到页面上某个输入框并自动弹出键盘,注意 App 内 WebView 的软键盘会挤压视口,扫码页面高度要用position: fixed或动态计算visualViewport,否则键盘弹起来时扫码框被顶出屏幕,用户以为页面崩了。企业微信里做客服聊天场景时,扫码结果通常是发给客服的一条消息,多模态格式要注意图片大小,扫码截图 PNG 可能几百 KB,改成 JPEG 质量 0.8 会明显降低上传耗时。
6. 把 jsQR 用得更顺手:相册识别、连续扫码降功耗和一个小工具验证
6.1 用 jsQR 识别相册里的二维码图片:让扫码页多一个降级入口
实时扫码偶尔会被权限、光线卡住,给页面加一个“从相册选择二维码”的入口很实用。流程是用<input type="file" accept="image/*">拿到图片,读入 Image 对象,绘制到 canvas 后再用同样的 jsQR 调用。
async function decodeFromFile(file) { const url = URL.createObjectURL(file); const img = new Image(); await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; img.src = url; }); const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const frame = ctx.getImageData(0, 0, canvas.width, canvas.height); const result = jsQR(frame.data, frame.width, frame.height); URL.revokeObjectURL(url); return result?.data ?? null; }注意大图要先压缩,否则一张 4000x3000 的图片,getImageData会拿到 4800 万个像素值,解码可能卡一两秒。可以在drawImage时直接把图片画到一个宽度不超过 1000 的 canvas 上,解码速度能快很多。
6.2 连续扫码降功耗:把解码间隔从 50ms 调到 300ms
实时扫码的功耗主要来自三块:摄像头常开、视频帧持续绘制、jsQR 持续解码。前两块省不掉,第三块是唯一能让手机不烫的调节空间。fps 不是越高越好:10fps 和 20fps 在识别率上的差距,远小于发热降频带来的差距。如果用户在室内稳定环境下扫码,把解码间隔放到 300ms,一秒钟只解 3 次,手机温度会明显降下来,识别率反而稳定。
6.3 用 performance.now 验证解码耗时:别凭感觉调参数
调参不能靠肉眼,我习惯把单帧解码耗时打出来,用数据决定到底该降分辨率还是降 fps。
const start = performance.now(); const code = jsQR(frame.data, frame.width, frame.height, { inversionAttempts: 'dontInvert' }); const spent = performance.now() - start; console.log('jsQR 解码耗时:', spent.toFixed(1), 'ms');如果spent经常超过 80ms,优先减小 canvas 尺寸,而不是降 fps;如果spent稳定在 30ms 以内,可以尝试把inversionAttempts切回attemptBoth提升反色码识别率。我之前有一次上线后用户反馈扫码页烫手,最后发现是 fps 设了 30,每帧全分辨率解码。改成 10fps 加半分辨率画布之后,识别率反而提升,因为手机不再降频。这个教训让我后来每次调扫码参数,都会先测单帧耗时,而不是凭感觉堆配置。希望帮到你。
本文还有配套的精品资源,点击获取