React Native 物理按键扫码监听终极方案:从冲突到完美共存

React Native 物理按键扫码监听终极方案:从冲突到完美共存

写给所有被 PDA 扫码折磨的开发者,以及未来的自己。

如果你正在开发 PDA(手持终端)应用,并且遇到了"全局监听和页面监听打架"、"扫码结果在这个页面能收到,在那个页面就收不到"的问题,那么这篇文章就是为你准备的。

1. 遇到的问题

在开发仓库管理系统(WMS)或类似 PDA 应用时,物理扫码键是最常用的交互方式。我们通常会有两种需求:

  1. 全局监听:不管在哪个页面,我都要记录扫码历史,或者做一些全局的日志记录。
  2. 页面监听:在具体的业务页面(比如入库单、盘点单),我需要拿到扫码结果去请求接口、查询商品。

最初的痛点 : 当我们使用原生的 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 为多播模式,我们完美实现了:

  1. 全局不掉线:ScanContext 里的历史记录永远在记录。
  2. 页面互不扰:A 页面监听扫码,不会影响 B 页面;离开 A 页面自动停止监听。
  3. 上下文可追踪:清楚地知道当前这一次扫码是为了哪个业务对象。

这就是 PDA 物理按键扫码的"终极解决方案"。🚀

相关推荐
墨狂之逸才9 小时前
React Native 状态管理大比拼:Event Bus 还是 Context?小白一看就懂!
react native
爱滑雪的码农9 小时前
React Native 完整开发全流程(从零到上线)
javascript·react native·react.js
沐言人生10 小时前
ReactNative 源码分析12——Native View创建流程onBatchComplete
android·react native
沐言人生2 天前
ReactNative 源码分析11——Native View创建流程setChildren和manageChildren
android·react native
沐言人生3 天前
ReactNative 源码分析10——Native View创建流程createView
android·react native
坏小虎3 天前
【聊天列表组件选型建议】FlashList、FlatList、LegendList三种列表组件
javascript·react native·react.js
sealaugh324 天前
react native(学习笔记第五课) 英语打卡微应用(4)- frontend的列表展示
笔记·学习·react native
沐言人生5 天前
ReactNative 源码分析9——Native View初始化
android·react native
接着奏乐接着舞5 天前
react native expo打包
javascript·react native·react.js
jxm_csdn7 天前
Expo Go 本地命令行编译 apk(Ubutnu22.04)
react native