阅读视图

发现新文章,点击刷新页面。

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 只移除指定的回调,而不是清空所有。
// 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)。
  • 提供全局状态。
// 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),我们在实现时使用了局部变量锁定的技巧,确保清理函数总是清理当前周期创建的那个监听器。

// 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):回调里会把当时的上下文带回来给你。
// src/hooks/useContextualPhysicalKeyScan.js

const setContext = useCallback((context) => {
  contextRef.current = context; // 存起来
  // 可以设置个超时,比如30秒后自动清除,防止误操作
}, []);

const handleScanResult = useCallback((result) => {
  // 触发回调时,把上下文也传出去
  onScan(result, contextRef.current);
}, []);

4. 如何使用?(小白看这里)

场景一:我就想在页面里拿扫码结果

直接用 usePhysicalKeyScan

import usePhysicalKeyScan from '@/hooks/usePhysicalKeyScan';

const MyPage = () => {
  usePhysicalKeyScan({
    onScan: (result) => {
      alert(`扫到了:${result.code}`);
      // 这里调用接口查询...
    }
  });

  return <View>...</View>;
};

场景二:我有好几个输入框/列表项,我要区分扫给谁

useContextualPhysicalKeyScan

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 物理按键扫码的“终极解决方案”。🚀

❌