1. 从一个真实需求说起:为什么要死磕连续扫码
去年接了一个仓储盘点的小程序项目,需求方开口第一句话就是:“我要能一直扫,扫完一个自动接着扫下一个,中间不要让我点任何按钮。”听起来很简单对吧?微信小程序官方提供了wx.scanCodeAPI,调一次扫一个,扫完再调一次不就行了?
实际动手之后才发现,这里面的坑比想象中多得多。wx.scanCode每次调用都会拉起一个全屏的原生扫码界面,扫完之后界面关闭,回到小程序页面,然后你才能再次调用。这个“拉起-关闭-再拉起”的过程,在安卓上大概有 300-500ms 的闪烁间隙,在 iOS 上更明显,用户会看到屏幕反复闪白,体验非常割裂。而且每次调用都会有一次明显的相机启动延迟,连续扫十几个码下来,操作员直接跟我说“眼睛都要闪瞎了”。
后来我把目光转向了camera组件。微信小程序从基础库 2.7.0 开始,camera组件支持了mode="scanCode"模式,可以在组件内部直接完成扫码识别,不需要反复拉起原生界面。这意味着你可以把 camera 组件嵌在页面里,配合bindscancode事件,实现真正的“连续扫码”——扫完一个码,界面不关闭,直接接着扫下一个。
但事情没有这么简单。官方文档对camera组件的scanCode模式描述非常简略,很多关键细节需要自己踩坑才能摸清楚。比如:扫到同一个码会不会重复触发?连续扫码时如何避免重复录入?不同机型上 camera 组件的表现差异有多大?页面隐藏后相机资源怎么释放?这些问题在官方文档里几乎找不到答案。
这篇文章就是把我这段时间踩过的坑、试过的方案、总结出来的经验完整地分享出来。如果你也在做微信小程序的扫码功能,尤其是需要连续扫码的场景(仓储盘点、图书录入、资产盘点、门票核销等),这篇文章应该能帮你省下不少时间。我会从方案选型开始讲,然后深入 camera 组件的核心机制,再给出完整的实操代码和避坑指南,最后整理一份常见问题速查表。内容比较长,建议先收藏再慢慢看。
2. 方案选型:scanCode API 还是 camera 组件
2.1 两种扫码方式的本质区别
在微信小程序里做扫码,摆在面前的有两条路:一是调用wx.scanCodeAPI,二是使用camera组件的scanCode模式。很多人一开始都会用wx.scanCode,因为它简单,一行代码就能调起来。但如果你要做连续扫码,这个方案很快就会暴露出问题。
wx.scanCode的本质是调用微信客户端原生的扫码界面。你调用它,微信就弹出一个全屏的原生页面,这个页面不属于你的小程序,你无法控制它的 UI,也无法在它上面叠加任何东西。扫完之后原生页面关闭,结果通过回调返回给你的小程序。整个过程是“你的小程序 → 原生扫码页 → 你的小程序”这样一个来回切换的过程。
camera组件则完全不同。它是嵌入在你小程序页面里的一个原生组件,相机画面直接显示在你指定的区域里。当设置mode="scanCode"时,组件内部会自动识别画面中的二维码/条形码,识别到之后通过bindscancode事件把结果抛给你。整个过程你的页面始终在前台,相机画面不会中断,用户看到的是一个连续的取景框,体验上更接近“专业扫码枪”的感觉。
2.2 连续扫码场景下的性能对比
我分别在安卓和 iOS 上做了对比测试,用同一台设备连续扫 20 个二维码,记录总耗时和用户可感知的闪烁次数。测试机型:安卓端是某骁龙 870 机型,iOS 端是 iPhone 13。
| 对比维度 | wx.scanCode API | camera 组件 scanCode 模式 |
|---|---|---|
| 单次扫码平均耗时 | 800-1200ms | 200-400ms |
| 连续扫 20 个码总耗时 | 约 20-25 秒 | 约 6-8 秒 |
| 界面闪烁 | 每次都有明显闪烁 | 无闪烁,画面连续 |
| 相机启动延迟 | 每次调用都有 | 仅首次有 |
| 自定义 UI | 不支持 | 完全支持 |
| 连续扫码体验 | 差,需要反复等待 | 好,接近无缝 |
| 最低基础库要求 | 1.0.0 | 2.7.0 |
从表格可以清楚看到,camera组件在连续扫码场景下的优势是压倒性的。总耗时差了 3 倍以上,而且没有闪烁,用户体验完全不是一个级别。当然代价是基础库要求更高(2.7.0),以及你需要自己处理相机权限、页面生命周期等细节。
2.3 什么场景该选哪个方案
不是说wx.scanCode就一无是处。如果你的场景是“偶尔扫一次”,比如用户点击按钮扫一个码就完事,那用wx.scanCode完全没问题,代码简单,兼容性好,不需要处理相机权限的复杂逻辑。
但如果你符合以下任何一个特征,就应该认真考虑camera组件方案:
- 需要连续扫多个码,中间不希望用户有多余操作
- 对扫码速度有要求,比如高峰期需要快速核销
- 需要自定义扫码界面的 UI,比如加个手电筒按钮、加个已扫列表
- 需要在扫码的同时显示其他信息,比如当前盘点进度
我个人的建议是:只要你的场景涉及连续扫码,直接上camera组件方案,不要犹豫。前期多花一两个小时处理细节,后期省下的是无数次的用户投诉。
3. camera 组件连续扫码的核心机制拆解
3.1 bindscancode 事件的触发逻辑
camera组件的bindscancode事件是整个连续扫码的核心。当mode="scanCode"时,组件会自动识别相机画面中的码,识别成功就触发一次bindscancode,回调参数里包含扫码结果。
这里有一个非常关键的细节:同一个码在画面中持续存在时,bindscancode 会不会重复触发?官方文档没有明确说明,我实测的结果是:会重复触发,但触发频率不固定。在安卓上,同一个码大约每 500-800ms 会触发一次;在 iOS 上,大约每 300-500ms 触发一次。这意味着如果你不做去重处理,同一个码会被连续录入多次。
这个机制的设计初衷可能是为了“容错”——万一第一次识别结果不完整,后续还能补上。但对于连续扫码场景来说,这就是一个必须处理的陷阱。解决方案后面会详细讲。
3.2 相机资源的生命周期管理
camera组件是原生组件,相机资源是独占的。这意味着:
- 当页面隐藏时(比如用户按了 Home 键,或者跳转到其他页面),相机应该被释放,否则会一直占用摄像头,导致其他应用无法使用相机
- 当页面显示时,相机需要重新初始化
- 如果页面上有多个 camera 组件,只有最后一个生效
微信小程序提供了页面生命周期函数onHide和onShow,以及组件级别的wx.createCameraContext来管理相机。但实际使用中,我发现单纯依赖页面生命周期还不够,因为 camera 组件的初始化是异步的,页面onShow触发时相机可能还没准备好。
一个比较稳妥的做法是:用一个data字段控制 camera 组件的渲染与销毁。页面隐藏时把字段设为false,组件被销毁,相机释放;页面显示时设为true,组件重新创建。这样虽然会有一点初始化延迟,但能确保相机资源被正确释放。
3.3 连续扫码的去重策略
前面提到同一个码会重复触发bindscancode,所以去重是必须的。去重的核心思路是:记录最近一次扫码的结果和时间戳,如果新结果和上一次相同,且时间间隔小于某个阈值,就忽略。
但这里有个问题:如果用户真的需要连续扫两个相同的码呢?比如盘点时有两个相同的资产标签。这种情况下,简单的“结果相同就忽略”会误杀。所以去重策略需要更精细一些。
我的做法是:维护一个“已扫码集合”,每扫到一个码,先检查是否在集合中。如果不在,加入集合并触发业务逻辑;如果在,检查距离上次扫码的时间间隔,如果超过一定时间(比如 3 秒),认为是用户有意重复扫,允许通过;如果小于 3 秒,认为是重复触发,忽略。
这个策略在实际使用中效果不错,既避免了重复触发,又不会误杀合理的重复扫码需求。
4. 完整实操:从零实现一个连续扫码页面
4.1 页面结构与基础配置
先来看页面的 WXML 结构。核心就是一个camera组件,加上一些辅助 UI。
<view class="scan-container"> <!-- 相机区域 --> <camera wx:if="{{cameraVisible}}" class="camera-area" mode="scanCode" device-position="back" flash="{{flashOn ? 'torch' : 'off'}}" bindscancode="onScanCode" binderror="onCameraError" > <!-- 扫码框覆盖层 --> <cover-view class="scan-frame"> <cover-view class="scan-corner top-left"></cover-view> <cover-view class="scan-corner top-right"></cover-view> <cover-view class="scan-corner bottom-left"></cover-view> <cover-view class="scan-corner bottom-right"></cover-view> </cover-view> </camera> <!-- 相机不可用时的提示 --> <view wx:else class="camera-placeholder"> <text>相机初始化中...</text> </view> <!-- 底部操作栏 --> <view class="bottom-bar"> <view class="scan-count">已扫 {{scanList.length}} 个</view> <view class="btn-group"> <button class="btn-flash" bindtap="toggleFlash"> {{flashOn ? '关闭闪光灯' : '打开闪光灯'}} </button> <button class="btn-finish" bindtap="finishScan">完成</button> </view> </view> <!-- 已扫列表 --> <scroll-view class="scan-list" scroll-y> <view wx:for="{{scanList}}" wx:key="index" class="scan-item"> <text class="scan-index">{{index + 1}}</text> <text class="scan-result">{{item}}</text> </view> </scroll-view> </view>这里有几个关键点需要注意:
camera组件用wx:if控制显示与隐藏,而不是hidden。因为hidden只是隐藏了 UI,相机资源并没有释放。用wx:if才能真正销毁组件、释放相机。mode="scanCode"是开启扫码模式的关键属性。device-position="back"指定使用后置摄像头,扫码场景基本都用后置。flash属性控制闪光灯,torch是常亮模式,适合扫码补光。bindscancode是扫码结果的回调。binderror是相机出错的回调,必须处理,否则出错时用户看不到任何提示。
4.2 核心逻辑:扫码事件处理与去重
接下来是 JS 部分的逻辑。核心是onScanCode事件处理函数,以及去重逻辑。
Page({ data: { cameraVisible: true, flashOn: false, scanList: [], lastScanResult: '', lastScanTime: 0, scannedSet: {}, // 用于去重的集合 }, onLoad() { // 页面加载时检查相机权限 this.checkCameraAuth(); }, onShow() { // 页面显示时重新渲染 camera 组件 this.setData({ cameraVisible: true }); }, onHide() { // 页面隐藏时销毁 camera 组件,释放相机 this.setData({ cameraVisible: false }); }, onUnload() { // 页面卸载时确保相机释放 this.setData({ cameraVisible: false }); }, // 检查相机权限 checkCameraAuth() { wx.getSetting({ success: (res) => { if (!res.authSetting['scope.camera']) { wx.authorize({ scope: 'scope.camera', success: () => { console.log('相机权限已授权'); }, fail: () => { wx.showModal({ title: '需要相机权限', content: '扫码功能需要使用相机,请在设置中开启', confirmText: '去设置', success: (modalRes) => { if (modalRes.confirm) { wx.openSetting(); } } }); } }); } } }); }, // 扫码结果处理 onScanCode(e) { const { result } = e.detail; const now = Date.now(); const DEDUP_INTERVAL = 3000; // 3秒内相同结果视为重复 // 去重逻辑 if (result === this.data.lastScanResult) { if (now - this.data.lastScanTime < DEDUP_INTERVAL) { // 3秒内相同结果,忽略 return; } } // 更新最近扫码记录 this.setData({ lastScanResult: result, lastScanTime: now, }); // 检查是否已经扫过 if (this.data.scannedSet[result]) { // 已经扫过,给出提示但不重复添加 wx.showToast({ title: '该码已扫描过', icon: 'none', duration: 1000, }); return; } // 新码,加入列表 const newList = [...this.data.scanList, result]; const newSet = { ...this.data.scannedSet, [result]: true }; this.setData({ scanList: newList, scannedSet: newSet, }); // 震动反馈 wx.vibrateShort({ type: 'medium' }); // 提示音(可选) // this.playBeep(); }, // 相机错误处理 onCameraError(e) { console.error('相机错误:', e.detail); wx.showToast({ title: '相机启动失败,请检查权限', icon: 'none', duration: 2000, }); }, // 切换闪光灯 toggleFlash() { this.setData({ flashOn: !this.data.flashOn }); }, // 完成扫码 finishScan() { const { scanList } = this.data; if (scanList.length === 0) { wx.showToast({ title: '还没有扫描任何码', icon: 'none' }); return; } // 这里可以把 scanList 提交到后端 console.log('扫码结果:', scanList); wx.showModal({ title: '扫码完成', content: `共扫描 ${scanList.length} 个码,是否提交?`, success: (res) => { if (res.confirm) { // 提交逻辑 this.submitScanResults(scanList); } } }); }, // 提交扫码结果 submitScanResults(list) { wx.showLoading({ title: '提交中...' }); // 模拟提交 setTimeout(() => { wx.hideLoading(); wx.showToast({ title: '提交成功', icon: 'success' }); // 重置状态 this.setData({ scanList: [], scannedSet: {}, lastScanResult: '', lastScanTime: 0, }); }, 1000); }, });这段代码有几个关键设计点需要展开说明。
去重逻辑的双层设计。第一层是“时间窗口去重”,用lastScanResult和lastScanTime判断是否在 3 秒内重复触发。第二层是“集合去重”,用scannedSet记录所有已扫过的码,防止同一个码在间隔较长时间后被再次扫入。两层配合,既能防止连续触发导致的重复,又能防止用户无意中重复扫描同一个码。
震动反馈的重要性。在连续扫码场景中,用户往往不会一直盯着屏幕看,可能在看商品或者看货架。这时候声音和震动反馈就非常重要。wx.vibrateShort是微信提供的短震动 API,在扫码成功时触发,用户能通过触感确认扫码成功,不需要看屏幕。这个细节在实际使用中能大幅提升效率。
页面生命周期的处理。onHide和onUnload里都把cameraVisible设为false,确保相机资源被释放。onShow里重新设为true,让相机重新初始化。这里有一个小问题:页面从隐藏到显示时,camera 组件重新创建需要一点时间,用户会看到短暂的“相机初始化中”提示。这个延迟在安卓上大约 200-300ms,iOS 上大约 100-200ms,基本可以接受。
4.3 样式处理与 UI 细节
camera 组件是原生组件,层级最高,普通的view无法覆盖在它上面。所以扫码框、提示文字这些覆盖层必须用cover-view和cover-image。
.scan-container { display: flex; flex-direction: column; height: 100vh; background: #000; } .camera-area { flex: 1; width: 100%; position: relative; } .scan-frame { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 500rpx; height: 500rpx; } .scan-corner { position: absolute; width: 60rpx; height: 60rpx; border-color: #00ff00; border-style: solid; } .top-left { top: 0; left: 0; border-width: 4rpx 0 0 4rpx; } .top-right { top: 0; right: 0; border-width: 4rpx 4rpx 0 0; } .bottom-left { bottom: 0; left: 0; border-width: 0 0 4rpx 4rpx; } .bottom-right { bottom: 0; right: 0; border-width: 0 4rpx 4rpx 0; } .bottom-bar { display: flex; justify-content: space-between; align-items: center; padding: 20rpx 30rpx; background: #1a1a1a; } .scan-count { color: #fff; font-size: 28rpx; } .btn-group { display: flex; gap: 20rpx; } .btn-flash, .btn-finish { font-size: 26rpx; padding: 10rpx 30rpx; border-radius: 40rpx; } .btn-flash { background: #333; color: #fff; } .btn-finish { background: #07c160; color: #fff; } .scan-list { height: 300rpx; background: #111; } .scan-item { display: flex; align-items: center; padding: 16rpx 30rpx; border-bottom: 1rpx solid #222; } .scan-index { width: 50rpx; color: #07c160; font-size: 26rpx; } .scan-result { flex: 1; color: #ccc; font-size: 26rpx; word-break: break-all; }样式方面有几个经验点。扫码框用四个角的边框来画,比画一个完整的矩形框更清爽,也不会遮挡太多相机画面。底部操作栏用深色背景,和相机画面形成对比,按钮用微信绿(#07c160)作为主操作色,符合用户习惯。已扫列表放在底部,高度固定,可以滚动,用户扫完一个码可以快速确认结果。
5. 那些官方文档不会告诉你的坑
5.1 安卓与 iOS 的行为差异
这是最让人头疼的部分。camera组件在安卓和 iOS 上的表现差异比想象中大得多。
识别速度差异。iOS 的识别速度明显快于安卓。同一个二维码,iPhone 基本是“对准即识别”,安卓有时候需要稍微停留一下。这个差异在扫密集排列的条码时特别明显。我的应对策略是在 UI 上给用户一个“正在识别”的视觉反馈,比如扫码框呼吸灯效果,让用户知道系统在工作,不要急着移开。
重复触发频率差异。前面提到过,iOS 上同一个码的重复触发间隔大约是 300-500ms,安卓上是 500-800ms。这意味着去重的时间窗口不能设得太短,否则安卓上会漏掉一些重复触发。我最终把去重窗口设为 3 秒,这个值在两端都能很好地工作。
闪光灯行为差异。在 iOS 上,flash="torch"会打开常亮补光灯,效果很好。但在部分安卓机型上,torch模式可能不生效,或者亮度很低。如果闪光灯对你的场景很重要,建议在代码里加一个检测逻辑,如果torch不生效,降级为flash="on"模式(虽然这个模式在扫码时体验不好,但至少能补光)。
相机初始化时间差异。iOS 上 camera 组件初始化大约 100-200ms,安卓上 200-500ms 不等,低端安卓机可能超过 1 秒。所以“相机初始化中”的提示是必要的,不要让用户面对一个黑屏。
5.2 基础库版本与兼容性处理
camera组件的scanCode模式要求基础库 2.7.0 及以上。虽然现在大部分用户的微信版本都远高于这个,但仍然需要做兼容处理。
我的做法是在页面加载时检查基础库版本:
onLoad() { const systemInfo = wx.getSystemInfoSync(); const SDKVersion = systemInfo.SDKVersion; const compareVersion = (v1, v2) => { const arr1 = v1.split('.').map(Number); const arr2 = v2.split('.').map(Number); const len = Math.max(arr1.length, arr2.length); while (arr1.length < len) arr1.push(0); while (arr2.length < len) arr2.push(0); for (let i = 0; i < len; i++) { if (arr1[i] > arr2[i]) return 1; if (arr1[i] < arr2[i]) return -1; } return 0; }; if (compareVersion(SDKVersion, '2.7.0') < 0) { wx.showModal({ title: '版本过低', content: '当前微信版本不支持连续扫码功能,请升级微信后再试', showCancel: false, }); return; } }这段版本比较函数是通用的,建议放在工具函数里复用。如果版本低于 2.7.0,直接提示用户升级,不要尝试降级方案,因为降级到wx.scanCode的体验落差太大,不如不做。
5.3 相机权限被拒绝后的处理
相机权限是敏感权限,用户可能拒绝。如果用户拒绝了,camera组件会触发binderror,但错误信息比较模糊。更好的做法是主动检查权限状态。
wx.getSetting({ success: (res) => { if (res.authSetting['scope.camera'] === false) { // 用户之前拒绝过,引导去设置页开启 wx.showModal({ title: '相机权限未开启', content: '扫码需要使用相机,请在设置中开启相机权限', confirmText: '去设置', success: (modalRes) => { if (modalRes.confirm) { wx.openSetting({ success: (settingRes) => { if (settingRes.authSetting['scope.camera']) { // 用户开启了权限,重新初始化相机 this.setData({ cameraVisible: true }); } } }); } } }); } } });这里有一个细节:wx.openSetting只能由用户点击触发,不能在代码里自动调用。所以必须通过showModal引导用户点击“去设置”按钮,才能打开设置页。这个流程虽然多了一步,但符合微信的规范,不会被审核拒绝。
5.4 连续扫码时的性能优化
连续扫几十个码之后,页面可能会变得卡顿。原因主要有两个:一是setData频繁调用导致数据传输量大,二是已扫列表越来越长,渲染压力大。
优化方案:
- 合并 setData 调用。扫码事件里不要多次调用
setData,把需要更新的数据合并成一次调用。比如scanList和scannedSet一起更新。 - 限制列表渲染长度。已扫列表不需要显示全部,只显示最近 20 条即可。用
scanList.slice(-20)来截取。 - 使用虚拟列表。如果确实需要显示全部,考虑用
recycle-view等虚拟列表组件,但实现复杂度较高,一般场景用截取就够了。 - 避免在扫码回调里做复杂计算。扫码回调应该尽量轻量,复杂的业务逻辑(比如网络请求)应该异步处理,不要阻塞扫码事件。
我实测下来,做了这些优化之后,连续扫 100 个码页面依然流畅,没有明显卡顿。
6. 常见问题速查表与排查思路
6.1 扫码相关高频问题
| 问题现象 | 可能原因 | 排查思路 | 解决方案 |
|---|---|---|---|
| camera 组件不显示 | 基础库低于 2.7.0 | 检查 SDKVersion | 提示用户升级微信 |
| 扫码无反应 | 相机权限被拒绝 | 检查 authSetting | 引导用户去设置页开启 |
| 同一个码重复录入 | 未做去重处理 | 检查 onScanCode 逻辑 | 加时间窗口+集合去重 |
| 页面隐藏后相机仍占用 | 未销毁 camera 组件 | 检查 onHide 逻辑 | 用 wx:if 控制组件销毁 |
| 安卓上闪光灯不亮 | 机型兼容性问题 | 真机测试 | 降级为 flash="on" |
| 扫码识别慢 | 相机对焦问题 | 检查 device-position | 确保使用后置摄像头 |
| 连续扫码后页面卡顿 | setData 频繁调用 | 检查 setData 次数 | 合并 setData,限制列表长度 |
| iOS 上扫码框被遮挡 | cover-view 层级问题 | 检查 cover-view 使用 | 确保覆盖层用 cover-view |
| 扫码结果乱码 | 编码问题 | 检查二维码编码格式 | 用 decodeURIComponent 处理 |
| 相机启动失败 | 其他应用占用相机 | 检查后台应用 | 提示用户关闭其他相机应用 |
6.2 几个容易被忽略的细节
扫码结果的编码处理。有些二维码的内容是 URL 编码的,直接显示会是乱码。建议在拿到结果后做一次decodeURIComponent,但要注意捕获异常,因为不是所有结果都是编码过的。
let result = e.detail.result; try { result = decodeURIComponent(result); } catch (err) { // 解码失败,使用原始结果 console.warn('解码失败,使用原始结果:', err); }扫码结果为空的情况。极少数情况下,bindscancode会触发但result为空字符串。这种情况直接忽略即可,不要加入列表。
连续扫码的节流。虽然 camera 组件本身有识别间隔,但在业务层面加一个节流会更稳妥。比如设置一个 500ms 的节流窗口,窗口内的扫码事件直接丢弃。这样可以进一步降低重复触发的概率。
页面跳转时的处理。如果扫码过程中用户点击了某个按钮跳转到其他页面,要确保在跳转前把cameraVisible设为false,否则相机可能在新页面加载后仍然被占用。
6.3 真机调试 vs 开发者工具
这里必须强调:camera 组件的很多行为在开发者工具里和真机上完全不一样。开发者工具里 camera 组件可能根本不显示,或者显示的是电脑摄像头画面,扫码识别逻辑也可能不触发。所以 camera 相关的功能,必须用真机调试。
真机调试的步骤:
- 在开发者工具里点击“真机调试”
- 用手机微信扫描弹出的二维码
- 在手机上进行扫码测试
- 通过开发者工具的调试面板查看日志和错误
如果真机调试时发现 camera 组件不显示,先检查手机是否给了微信相机权限。安卓上还要检查是否被其他应用占用了相机。
7. 一些进阶玩法与扩展思路
7.1 扫码结果实时校验
在仓储盘点场景中,扫到的码需要和系统里的数据进行比对。可以在扫码回调里加一个校验逻辑,如果扫到的码不在预期列表中,给出不同的提示音和震动模式。
onScanCode(e) { const { result } = e.detail; // ... 去重逻辑 ... // 校验逻辑 const isValid = this.checkCodeValidity(result); if (isValid) { wx.vibrateShort({ type: 'medium' }); // 正常提示音 } else { wx.vibrateLong(); wx.showToast({ title: '无效码', icon: 'none' }); // 错误提示音 } }这种即时反馈能让操作员立刻知道扫的码对不对,不需要事后核对,效率提升明显。
7.2 批量扫码后的数据提交
连续扫码会产生一批数据,这些数据通常需要提交到后端。提交时要注意:
- 数据量大时分批提交,避免单次请求体过大
- 提交失败时要保留本地数据,支持重试
- 提交过程中要防止用户重复提交
我的做法是用一个submitting标志位控制,提交时禁用“完成”按钮,提交成功后再重置状态。
7.3 扫码历史记录与断点续扫
如果扫码过程中小程序被意外关闭(比如用户接了个电话),已扫的数据会丢失。对于需要扫几百个码的场景,这是不可接受的。
解决方案是把已扫列表实时存入wx.setStorageSync,页面加载时从缓存恢复。这样即使小程序被关闭,重新打开后还能继续扫。
// 每次扫码后保存 wx.setStorageSync('scan_list', this.data.scanList); // 页面加载时恢复 onLoad() { const savedList = wx.getStorageSync('scan_list') || []; if (savedList.length > 0) { wx.showModal({ title: '发现未完成的扫码记录', content: `上次扫描了 ${savedList.length} 个码,是否继续?`, success: (res) => { if (res.confirm) { this.setData({ scanList: savedList }); } else { wx.removeStorageSync('scan_list'); } } }); } }这个功能在实际使用中非常实用,尤其是盘点场景,操作员可能扫到一半需要去做别的事,断点续扫能避免重复劳动。
8. 我个人在实际项目中的几点体会
做这个连续扫码功能前前后后花了大概两周时间,其中大部分时间不是在写代码,而是在真机上测试和调整。有几点体会比较深。
第一,不要相信开发者工具里的表现。camera 组件在开发者工具里基本没法用,所有测试都必须上真机。而且至少要准备一台安卓和一台 iOS,因为两端差异真的很大。
第二,去重逻辑要比想象中复杂。一开始我以为简单判断一下就行,后来发现要考虑时间窗口、集合去重、用户有意重复扫等多种情况。最终的去重逻辑改了三四版才稳定下来。
第三,用户体验的细节决定成败。震动反馈、提示音、扫码框动画、已扫列表的实时更新,这些细节看起来不起眼,但在连续扫码场景中,每一个都能明显影响操作效率。尤其是震动反馈,操作员不用看屏幕就能知道扫码成功,这个体验提升是巨大的。
第四,异常处理要全面。相机权限被拒、相机启动失败、扫码结果为空、页面隐藏后相机未释放,这些异常情况在开发阶段可能不会遇到,但上线后一定会出现。提前处理好这些异常,能省下很多客服成本。
最后分享一个小技巧:如果扫码场景的光线条件不好,除了开闪光灯,还可以在扫码框周围加一圈半透明的白色边框,利用屏幕自身的光来补光。这个技巧在扫反光材质的条码时特别有用,能明显提升识别率。