拓冰建站拓冰建站
首页 / 资讯中心 / 正文

微信小程序连续扫码实战:Camera组件与防抖优化方案

微信小程序里做扫码功能很多人第一反应是调wx.scanCode一行代码就能拉起原生扫码界面简单省事。但真把它放到业务场景里跑一圈问题就来了扫完一次界面就关了想连续扫就得反复点按钮扫码结果回调时机不好控制手一抖扫重了相机预览卡顿、识别慢、光线一暗就歇菜。这些坑我在几个零售盘点、仓储入库和票务核销的项目里都踩过最后都绕回到同一个方案上——用Camera组件自己做扫码界面配合scanCode的连续调用和防抖策略把体验拉回来。这篇内容就是把这套方案从头到尾拆一遍。核心关键词是微信小程序、Camera组件、连续扫码、scanCode、防抖优化。适合已经写过小程序基础页面、想把手里的扫码功能从能用做到好用的开发者也适合正在做盘点、核销、出入库这类需要高频扫码场景的产品和前端。我会把为什么这么选、每一步怎么落地、参数怎么调、哪些地方容易翻车都讲清楚代码可以直接抄去改。1. 为什么原生 scanCode 撑不起连续扫码场景1.1 原生扫码的调用模型和它的天花板wx.scanCode的本质是调用一次、拉起一个全屏原生界面、返回一个结果、界面关闭。它是一个一次性的、阻塞式的 API。你调它它就跳出去用户扫完它把结果通过 success 回调还给你然后整个扫码界面消失回到你自己的页面。这个模型在扫一次就够的场景里没问题比如扫个付款码、扫个二维码跳转。但连续扫码场景要的是什么是用户举着手机对着货架或者一摞单据扫完一个紧接着扫下一个中间不要有任何跳转、不要有任何等待、不要有任何手动操作。原生 scanCode 每扫一次都要重新拉起界面这个拉起-关闭-再拉起的循环本身就是体验杀手。更麻烦的是原生界面关闭和你的回调执行之间存在时间差。用户扫完第一个码界面开始关闭动画你的 success 回调这时候才触发你处理完数据想再调一次 scanCode界面可能还没完全关干净就会出现闪烁甚至调用失败。这个时序问题在低端安卓机上尤其明显。1.2 连续扫码真正要解决的三个问题把需求拆开看连续扫码要解决的核心问题其实就三个。第一是界面常驻。相机预览要一直开着用户不需要反复触发扫完一个自动准备扫下一个。这要求扫码界面是一个常驻的页面或者组件而不是一个被反复拉起的原生弹层。第二是结果去重。连续扫码时同一个码很容易被连续识别到多次。相机每秒可能出好几帧识别算法对同一个码会反复命中如果不去重你一次扫码可能往列表里塞进去七八条重复数据。这就是防抖要解决的核心问题。第三是节奏可控。扫完一个码之后要给用户一个明确的反馈震动、提示音、界面高亮然后短暂暂停识别等用户把手机移到下一个码上再恢复。如果识别一直开着用户移动手机的间隙可能扫到旁边的码造成误扫。原生 scanCode 这三个问题一个都解决不了所以必须自己用 Camera 组件搭。1.3 Camera 组件相比原生扫码的能力边界Camera组件是小程序提供的一个原生组件它把相机预览画面嵌到你的页面里你可以控制它的分辨率、闪光灯、前后摄像头还能通过bindscancode事件拿到扫码结果。注意Camera 组件自己就带扫码能力通过设置modescanCode就能开启扫到码会触发bindscancode。这就意味着你不需要自己去接一个第三方的图像识别库Camera 组件内置的扫码能力已经够用了。它的识别速度、对焦能力、对常见一维码二维码的支持都还不错。你要做的是在这个能力之上加一层业务逻辑去重、节流、反馈、状态管理。Camera 组件的边界也要清楚它是原生组件层级最高会盖住普通组件所以你的提示层、按钮层要用cover-view或者cover-image来做普通 view 会被相机画面盖住。这个坑我第一次做的时候踩得很结实界面上放了个普通按钮结果完全点不到。2. Camera 组件扫码模式的接入与页面骨架搭建2.1 页面结构设计相机层、遮罩层、反馈层一个能用的连续扫码页面结构上分三层。最底层是 Camera 组件本身占满整个屏幕或者屏幕的上半部分负责相机预览和扫码识别。中间层是遮罩层用来画扫码框、暗化四周区域引导用户把码对准中间。最上层是反馈层显示已扫数量、最近一条结果、操作按钮比如完成清空。这里有个关键点因为 Camera 是原生组件遮罩层和反馈层如果直接用普通 view在部分机型上会被相机画面盖住。稳妥的做法是遮罩层用cover-view反馈层里需要交互的按钮也用cover-view包一层。不过现在新版本基础库对同层渲染支持好了很多普通 view 在多数机型上也能正常显示在相机之上但为了兼容老设备cover-view还是更保险。页面骨架大概长这样view classscan-page camera classcamera modescanCode device-positionback flashauto resolutionhigh frame-sizelarge bindscancodeonScanCode binderroronCameraError cover-view classscan-mask cover-view classscan-frame/cover-view cover-view classscan-tip将条码放入框内即可自动识别/cover-view /cover-view /camera cover-view classresult-bar cover-view classcount已扫 {{scanCount}} 条/cover-view cover-view classbtn bindtaponFinish完成/cover-view /cover-view /viewmodescanCode是开启扫码模式的关键不设这个属性Camera 就只是预览不会触发扫码事件。device-positionback指定后置摄像头扫码场景基本都用后置。flashauto让闪光灯自动暗光环境下相机会自己补光这个在仓库这种光线不好的地方特别有用。2.2 关键属性逐个说清楚Camera 组件在扫码场景下有几个属性必须调对调错了要么识别慢要么直接不识别。resolution控制相机分辨率。可选low、medium、high。分辨率越高画面越清晰识别小码、远距离码的能力越强但帧率会下降低端机上可能卡顿。我的经验是盘点、核销这类码比较近、比较清晰的场景用medium就够兼顾流畅和识别率如果是扫远处货架上的小码再上high。frame-size控制相机帧数据的大小可选small、medium、large。这个参数影响的是传给识别算法的数据量。large识别能力最强但最吃性能small最省性能但可能识别不到小码。一般和resolution配合两个都往大了调识别强但卡都往小了调流畅但识别弱。我通常用resolutionhigh配frame-sizemedium是个比较平衡的组合。flash控制闪光灯auto是自动on是常开off是关闭。扫码场景建议auto让系统根据环境光自己判断。有些仓库光线特别暗auto可能反应慢可以给用户一个手动切换闪光灯的按钮用off和torch之间切。注意frame-size这个属性在部分基础库版本上表现不一致如果发现识别率异常先检查基础库版本建议在2.10.0以上。2.3 权限申请和相机初始化时机Camera 组件需要用户授权相机权限。第一次进入页面时小程序会自动弹权限申请框用户同意后才能看到画面。如果用户拒绝了binderror会触发你要在回调里给出引导告诉用户去设置里打开权限。这里有个体验细节不要在页面onLoad里就急着渲染 Camera因为权限弹框和相机初始化都需要时间。更好的做法是先渲染一个占位等权限确认后再显示相机。不过 Camera 组件本身会处理这个流程你只要保证binderror有兜底就行。onCameraError(e) { console.error(相机错误, e.detail); wx.showModal({ title: 相机不可用, content: 请检查相机权限是否开启或稍后重试, showCancel: false }); }相机初始化失败的原因常见的有三种用户拒绝授权、相机被其他应用占用、设备本身不支持。这三种都要在binderror里区分处理给用户明确的下一步操作而不是干巴巴报个错。3. 连续扫码的核心scanCode 回调与防抖去重机制3.1 bindscancode 的触发特性bindscancode是 Camera 组件在扫码模式下识别到码时触发的事件回调里能拿到detail.result也就是码的内容。这个事件的触发频率取决于相机帧率和识别算法的命中情况同一个码在画面里停留时可能每隔一两帧就触发一次。我实测过一个码放在画面中央不动bindscancode每秒能触发三到五次。如果你在回调里直接往数组里 push一秒就能塞进去好几条重复数据。这就是为什么防抖是连续扫码的核心没有防抖的连续扫码就是个灾难。还有一个特性要注意bindscancode触发时相机并不会自动暂停识别。也就是说你处理回调的这段时间里相机还在继续识别还在继续触发事件。如果你处理逻辑比较重比如要发网络请求校验事件会堆积造成卡顿甚至数据错乱。3.2 基于时间窗口的防抖策略最直接有效的防抖方式是时间窗口。思路很简单记录上一次成功处理扫码结果的时间戳每次bindscancode触发时先判断距离上次处理是否超过了一个阈值比如 1500 毫秒。没超过就直接忽略超过了才处理。data: { lastScanTime: 0, scanInterval: 1500, scannedList: [], scanCount: 0 }, onScanCode(e) { const now Date.now(); if (now - this.data.lastScanTime this.data.scanInterval) { return; } const result e.detail.result; if (!result) return; this.setData({ lastScanTime: now }); this.handleScanResult(result); }这个 1500 毫秒的阈值不是拍脑袋定的。它要满足两个条件一是足够长让用户把手机从上一个码移到下一个码二是足够短不让用户觉得扫完一个要等半天。我试过 800、1000、1500、2000 几档1500 在大多数场景下手感最好。如果是密集排列的码比如一排货架标签挨得很近可以调到 1000如果是需要用户手动确认的场景可以调到 2000。3.3 内容去重同一个码不能进两次时间窗口能挡住大部分重复但挡不住一种情况用户扫了 A 码过了两秒又扫了一次 A 码。这时候时间窗口已经过了A 码会被当成新结果再进一次。如果业务上不允许同一个码重复录入就需要内容去重。内容去重的做法是维护一个已扫结果的集合每次新结果进来先查集合存在就忽略。handleScanResult(result) { const list this.data.scannedList; const exists list.some(item item.code result); if (exists) { wx.showToast({ title: 该码已扫描, icon: none, duration: 800 }); return; } list.push({ code: result, time: Date.now() }); this.setData({ scannedList: list, scanCount: list.length }); wx.vibrateShort({ type: medium }); }这里用some遍历判断数据量小的时候没问题。如果已扫列表可能上千条some的 O(n) 遍历会拖慢响应这时候应该用一个对象或者 Map 来做 O(1) 的查找。我一般会在data之外维护一个scanSet不参与渲染只用于去重判断。3.4 反馈设计让用户知道扫到了连续扫码最怕的是用户不知道到底扫上没有。相机画面一直在动没有明确的反馈用户会反复对着同一个码扫或者以为没扫上就移开了。反馈要三管齐下视觉、听觉、触觉。视觉上扫到一个码时让扫码框闪一下绿色或者在结果栏顶部弹出一条新记录带个短暂的动画。听觉上用wx.playBackgroundAudio或者更轻量的方式播放一个短促的嘀声。触觉上wx.vibrateShort震一下这个最直接用户手上有感觉。playBeep() { const audio wx.createInnerAudioContext(); audio.src /assets/beep.mp3; audio.play(); audio.onEnded(() audio.destroy()); }音频文件要短控制在 100 毫秒以内太长会拖慢节奏。震动用medium强度light太弱感觉不到heavy太强吓人。4. 性能调优与真机适配的实战经验4.1 低端安卓机上的卡顿治理Camera 组件在低端安卓机上是个性能大户。相机预览本身就要占不少资源再加上扫码识别如果页面里还有复杂的动画或者频繁的setData卡顿是必然的。治理卡顿的第一条原则是减少 setData 频率。连续扫码时每扫一个码就setData更新列表和计数如果扫得快setData会非常频繁。我的做法是把结果先存在一个普通变量里用一个节流函数控制setData的频率比如每 500 毫秒才把最新数据同步到视图层。第二条是控制列表渲染量。已扫列表如果渲染几百条滚动和更新都会卡。界面上只显示最近 10 条完整数据存在内存里最后提交时再全量取。这样视图层的负担就小很多。第三条是避免在扫码回调里做重活。网络请求、复杂计算这些都不要放在onScanCode里同步做应该丢到队列里异步处理回调里只做最轻量的判断和存储。4.2 iOS 和安卓的差异处理iOS 和安卓在 Camera 组件上的表现差异不小有几个点必须分别处理。闪光灯方面iOS 的flashauto响应比较积极暗光下很快补光部分安卓机auto反应迟钝甚至不触发。如果业务场景经常在暗光下扫码建议给用户一个手动开关闪光灯的按钮不要完全依赖auto。对焦方面iOS 的自动对焦又快又准安卓机尤其是中低端的对焦慢、容易失焦。应对办法是在扫码框区域引导用户把码放稳同时可以适当降低对识别速度的预期把防抖阈值调大一点给对焦留时间。权限方面iOS 的权限弹框只能弹一次用户拒绝后必须去系统设置里改安卓部分机型可以再次弹框。所以权限引导文案要写清楚如果误点了拒绝请到设置-隐私-相机里开启别让用户卡在这一步。4.3 相机预览的常见异常与兜底真机上跑相机预览出问题是家常便饭。我整理了几种最常见的异常和对应的兜底方案。异常现象可能原因兜底方案画面全黑权限未授权 / 相机被占用检查权限提示用户关闭其他相机应用画面卡住不动相机初始化失败提供重新加载按钮重新渲染 Camera识别率极低frame-size 太小 / 光线太暗调大 frame-size开启闪光灯扫码回调不触发mode 未设为 scanCode检查属性拼写和基础库版本界面被相机盖住普通 view 层级不够改用 cover-view重新加载这个兜底很实用。做法是给 Camera 组件加一个wx:if控制出问题时把wx:if置 false 再置 true强制重新创建组件。这个操作能解决大部分相机初始化异常。reloadCamera() { this.setData({ cameraVisible: false }); setTimeout(() { this.setData({ cameraVisible: true }); }, 300); }5. 从扫码到业务落地的完整链路5.1 扫码结果的校验与业务处理扫到码只是第一步码的内容能不能用、对应什么业务数据才是关键。实际项目里扫码结果通常要经过几层校验。第一层是格式校验。比如业务规定码必须是 12 位数字那扫到字母或者长度不对的直接判为无效给用户提示这不是有效的商品码。这一层在本地做不消耗网络。第二层是业务校验。把码发给后端查这个码对应的商品、订单、票据是否存在、是否有效、是否已经被扫过。这一层要发请求所以必须异步不能阻塞扫码节奏。我的做法是扫到码先入本地队列并给用户已记录的反馈后台慢慢校验校验失败的再单独标红提示。第三层是重复校验。除了本地去重后端也要做一次去重防止多台设备同时扫同一个码。本地去重是体验优化后端去重是数据准确性的最后防线。5.2 批量提交与断网续传连续扫码场景往往一次要扫几十上百个不可能每扫一个就提交一次。合理的做法是本地累积用户点完成时批量提交。批量提交要考虑断网。仓库、地下室这些地方信号差是常态。我的方案是每次扫码结果都先写入wx.setStorageSync本地缓存提交成功后清除。如果提交时发现断网就提示用户网络异常数据已本地保存恢复网络后可重新提交。下次进入页面时检查本地缓存有未提交的数据就提示用户续传。saveLocal(list) { wx.setStorageSync(pending_scan_list, list); }, async submitAll() { const list this.data.scannedList; if (!list.length) return; try { await request(/api/scan/batch, { list }); wx.removeStorageSync(pending_scan_list); wx.showToast({ title: 提交成功 }); } catch (err) { this.saveLocal(list); wx.showToast({ title: 网络异常已本地保存, icon: none }); } }这个本地缓存 续传的机制在真实项目里救过好几次场。用户扫了一百多个码提交时网断了如果没有本地缓存这一百多个码就白扫了用户得从头再来体验极差。5.3 扫码历史与撤销操作连续扫码难免扫错。用户手快扫到了一个不该扫的码或者扫到了旁边的码需要有办法撤销。所以界面上要能看到已扫列表并且每条能单独删除。已扫列表的展示要克制不要把所有码都铺在界面上那样会挤占相机画面。我的做法是结果栏只显示最近一条和总数点总数展开一个半屏的列表列表里每条带删除按钮。这样既不影响扫码又能随时查看和修正。撤销操作要即时生效删掉之后总数要更新本地缓存也要同步更新。如果已经提交过了撤销就要走后端接口这个复杂度就上来了一般建议在提交前完成所有修正。6. 那些文档里不会写的踩坑记录6.1 cover-view 的样式限制cover-view虽然能盖在相机上但它的样式支持是残缺的。它不支持border-radius的部分写法、不支持box-shadow、不支持复杂的flex布局文字也有限制。我第一次做扫码框的时候想用border画个带圆角的框结果cover-view上圆角死活出不来。解决办法是用cover-image放一张预先做好的扫码框图片或者用四个cover-view拼出边框。扫码框的四个角用图片最省事中间的扫描线动画用cover-view配合animation做。别在cover-view上尝试复杂的 CSS会浪费很多时间。6.2 扫码回调里的 setData 陷阱onScanCode里如果直接setData更新一个长列表在扫码频率高的时候会引发严重的性能问题。我遇到过一次扫到第三十个码的时候界面直接卡死排查半天发现是每次扫码都setData整个列表数据量大了之后每次更新都要重新渲染整个列表。后来改成只setData变化的字段列表用wx:key优化并且限制渲染条数问题就解决了。这个坑的教训是扫码回调里的一切操作都要以轻为原则重活全部异步化。6.3 基础库版本导致的 API 差异Camera 组件的属性在不同基础库版本上支持情况不一样。frame-size是2.10.0才加的resolution更早一些。如果你的小程序要兼容老版本就得做特性检测或者在小程序管理后台把最低基础库版本设高一点。我一般会在app.json里设置requiredBackgroundModes之外还会在项目配置里把最低基础库设到2.10.0这样能用上完整的 Camera 能力也避免了一堆兼容代码。代价是极少数老设备用户可能用不了但扫码场景本身对设备就有要求这个取舍是值得的。6.4 连续扫码的节奏感是调出来的最后说一个偏经验的东西连续扫码的节奏感不是参数堆出来的是调出来的。防抖阈值、震动强度、提示音长短、扫码框闪烁时长这些参数组合在一起决定了用户扫起来是顺滑还是别扭。我的做法是找三五个同事拿真实场景的码让他们实际扫一遍观察他们的动作。如果发现他们扫完一个会犹豫一下才扫下一个说明反馈不够明确或者防抖太长如果他们频繁扫重说明防抖太短。根据观察调参数比对着文档拍脑袋靠谱得多。这套方案我在零售盘点、仓储入库、票务核销几个项目里都跑过从最初的卡顿、重复、误扫到后来的顺滑连续扫中间改了很多版。核心其实就那几件事Camera 常驻、时间窗口防抖、内容去重、三重反馈、本地缓存兜底。把这几点做扎实连续扫码的体验就立住了。剩下的就是根据具体业务场景微调参数多找真人测别自己对着屏幕想当然。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门