React Native 物理按键扫码监听终极方案:从冲突到完美共存
写给所有被 PDA 扫码折磨的开发者,以及未来的自己。
如果你正在开发 PDA(手持终端)应用,并且遇到了"全局监听和页面监听打架"、"扫码结果在这个页面能收到,在那个页面就收不到"的问题,那么这篇文章就是为你准备的。
1. 遇到的问题
在开发仓库管理系统(WMS)或类似 PDA 应用时,物理扫码键是最常用的交互方式。我们通常会有两种需求:
- 全局监听:不管在哪个页面,我都要记录扫码历史,或者做一些全局的日志记录。
- 页面监听:在具体的业务页面(比如入库单、盘点单),我需要拿到扫码结果去请求接口、查询商品。
最初的痛点 : 当我们使用原生的 DeviceEventEmitter 或者简单的封装时,往往会遇到"单播"的尴尬------一旦我在具体的业务页面开始监听扫码,全局的那个监听器就被"顶"掉了,失效了;或者反过来,全局监听器把事件拦截了,业务页面收不到。
2. 解决方案的核心思想:多播(Multicast)
要解决这个问题,我们需要一个"中间人"(Manager)。
- 以前的模式(单播) :原生事件 -> Manager -> 唯一的监听者(谁最后注册谁就赢)。
- 现在的模式(多播) :原生事件 -> Manager -> 监听者列表(Set) -> 分发给所有注册的人。
这样,无论是全局的 Context,还是具体的页面组件,只要向 Manager 注册了,大家都能收到通知,互不干扰!
3. 代码实现全解析
3.1 底层管理者:PhysicalKeyScanManager
这是最核心的部分。它负责跟原生模块打交道,并维护一个监听者列表。
关键点:
- 使用
Set来存储回调函数,自动去重。 startListening不再覆盖旧的回调,而是add进列表。stopListening只移除指定的回调,而不是清空所有。
javascript
// src/utils/PhysicalKeyScanManager.js
class PhysicalKeyScanManager {
constructor() {
// ...
this.listeners = new Set(); // 核心:存放所有监听者的集合
// ...
}
// 收到原生事件后的处理
_handleScanResult = (result) => {
// ... 包装数据 ...
// 核心:遍历列表,人人有份
this.listeners.forEach(callback => {
if (callback) callback(scanData);
});
};
startListening(callback) {
// 1. 把新来的监听者加入集合
if (callback) this.listeners.add(callback);
// 2. 如果是第一个监听者,才真正去建立原生连接(省资源)
if (!this.scanSubscription) {
this.scanSubscription = this.scanEventEmitter.addListener(
'onScanResult',
this._handleScanResult
);
}
// 3. 返回一个取消函数,方便 useEffect 清理
return () => this.stopListening(callback);
}
stopListening(callback) {
// 1. 只移除这一个监听者
if (callback) this.listeners.delete(callback);
// 2. 如果人走茶凉(列表空了),就把原生连接也断了
if (this.listeners.size === 0 && this.scanSubscription) {
this.scanSubscription.remove();
this.scanSubscription = null;
}
}
}
3.2 全局大管家:ScanContext
我们在 App 的最顶层(App.js)包裹这个 Provider。它的作用是从 App 启动那一刻起,就占一个坑位。
它负责:
- 初始化扫码服务(
autoInit)。 - 记录所有的扫码历史(
history)。 - 提供全局状态。
javascript
// src/context/ScanContext.js
useEffect(() => {
physicalKeyScanManager.autoInit();
// 注册全局监听,因为 Manager 支持多播,这里注册了也不会影响别的页面
const unsubscribe = physicalKeyScanManager.startListening(result => {
console.log('[全局记录] 收到扫码:', result.code);
setHistory(prev => [result, ...prev]);
});
return () => unsubscribe();
}, []);
3.3 页面级的 Hook:usePhysicalKeyScan
这是给普通业务页面用的。它的特点是智能管理生命周期。
- 页面获得焦点时:自动开始监听。
- 页面失去焦点时:自动停止监听。
这样能保证用户不在当前页面时,不会意外触发当前页面的逻辑。
注意 :为了防止 React Hooks 的闭包陷阱导致监听器重复注册(出现收一次码打印两次日志的 Bug),我们在实现时使用了局部变量锁定的技巧,确保清理函数总是清理当前周期创建的那个监听器。
javascript
// src/hooks/usePhysicalKeyScan.js
useFocusEffect(
useCallback(() => {
let unsubscribe = null; // 局部变量,锁定当前周期的监听器
if (autoStart) {
// 页面来了,注册监听,并赋值给局部变量
unsubscribe = physicalKeyScanManager.startListening(handleScanResult);
// 同步到 ref 供外部(如卸载时)使用
unsubscribeRef.current = unsubscribe;
// ...
}
return () => {
// 页面走了,使用局部变量进行清理,精准打击
if (unsubscribe) {
unsubscribe();
}
// ...
};
}, [autoStart])
);
3.4 进阶 Hook:useContextualPhysicalKeyScan
这是最强大的 Hook,专门解决**"我要把这个码扫给谁?"**的问题。
比如在一个物料列表中,点击某一行,然后扫码,把条码填入该行。
- setContext(item):设置当前正在操作的对象(上下文)。
- onScan(result, context):回调里会把当时的上下文带回来给你。
javascript
// src/hooks/useContextualPhysicalKeyScan.js
const setContext = useCallback((context) => {
contextRef.current = context; // 存起来
// 可以设置个超时,比如30秒后自动清除,防止误操作
}, []);
const handleScanResult = useCallback((result) => {
// 触发回调时,把上下文也传出去
onScan(result, contextRef.current);
}, []);
4. 如何使用?(小白看这里)
场景一:我就想在页面里拿扫码结果
直接用 usePhysicalKeyScan。
javascript
import usePhysicalKeyScan from '@/hooks/usePhysicalKeyScan';
const MyPage = () => {
usePhysicalKeyScan({
onScan: (result) => {
alert(`扫到了:${result.code}`);
// 这里调用接口查询...
}
});
return <View>...</View>;
};
场景二:我有好几个输入框/列表项,我要区分扫给谁
用 useContextualPhysicalKeyScan。
javascript
import useContextualPhysicalKeyScan from '@/hooks/useContextualPhysicalKeyScan';
const ListPage = () => {
const { setContext } = useContextualPhysicalKeyScan({
onScan: (result, context) => {
if (context) {
console.log(`把条码 ${result.code} 赋值给商品 ${context.name}`);
// 更新列表数据...
} else {
console.log('没选中商品,扫码无效或作为通用查询');
}
}
});
return (
<View>
{items.map(item => (
<TouchableOpacity
key={item.id}
onPress={() => setContext(item)} // 点击选中,告诉 Hook "接下来扫码是给它的"
>
<Text>{item.name}</Text>
</TouchableOpacity>
))}
</View>
);
};
5. 总结
通过改造 PhysicalKeyScanManager 为多播模式,我们完美实现了:
- 全局不掉线:ScanContext 里的历史记录永远在记录。
- 页面互不扰:A 页面监听扫码,不会影响 B 页面;离开 A 页面自动停止监听。
- 上下文可追踪:清楚地知道当前这一次扫码是为了哪个业务对象。
这就是 PDA 物理按键扫码的"终极解决方案"。🚀