React Native 硬件交互最佳实践:从混乱到优雅的架构演进

154 阅读5分钟

React Native 硬件交互最佳实践:从混乱到优雅的架构演进

📌 摘要:你是否在 React Native 开发中遇到过这样的问题:多个页面都要用扫码或 RFID,结果 A 页面开了硬件,B 页面也开,最后导致冲突报错?或者离开页面忘记关闭硬件导致耗电?本文将带你像搭积木一样,构建一套稳健的 Page -> Hook -> Context -> Manager -> Native 分层架构,让你的硬件交互代码既优雅又听话。


一、 为什么要改?(痛点分析)

在重构之前,我们的代码大概长这样:

❌ 之前的“混乱模式”

每个页面都自己去操作原生模块(NativeModules),就像每个部门经理都直接跑到发电机房去拉闸推闸。

// PageA.js (业务页面)
useEffect(() => {
  // 😱 页面自己负责初始化硬件
  NativeModules.UhfModule.init(); 
  NativeModules.UhfModule.start();

  const listener = DeviceEventEmitter.addListener('OnRead', (data) => {
    // 处理数据...
  });

  return () => {
    // 😱 页面关闭时,还得自己记得关掉
    NativeModules.UhfModule.stop();
    NativeModules.UhfModule.uninit();
    listener.remove();
  };
}, []);

这种写法的问题:

  1. 代码重复:每个用扫码的页面都要写一遍 initstop
  2. 生命周期混乱:如果从 PageA 跳转到 PageB,PageA 还没来得及 stop,PageB 又去 init,硬件可能会崩溃。
  3. 难以维护:如果哪天硬件 SDK 变了,你要改几十个文件。

二、 理想的架构(各司其职)

为了解决上面的问题,我们设计了一套 5层架构。每一层都有明确的职责,绝不越权。

🏗️ 架构图解

graph TD
    A[Page 业务页面] -->|使用| B(Hook 钩子函数)
    B -->|消费| C{Context 全局上下文}
    C -->|持有| D[Manager 管理器]
    D -->|调用| E[Native 原生模块]

🎭 角色扮演说明

  1. Page (业务页面) —— “老板”

    • 职责:只管下达命令(“我要监听 RFID”),不关心具体怎么做。
    • 例子TaskRFidScan.js
  2. Hook (钩子函数) —— “秘书”

    • 职责:负责帮老板安排琐事(注册监听、页面关闭时自动取消监听)。
    • 例子useRFID, useScan
  3. Context (全局上下文) —— “总管”

    • 职责:掌控全局状态,确保整个 App 只有一个硬件管理器实例,防止冲突。
    • 例子RFIDContext, ScanContext
  4. Manager (管理器) —— “技术员”

    • 职责:干实事的。处理原生指令、防抖、数据格式化。
    • 例子RFIDManager, PhysicalKeyScanManager
  5. Native (原生模块) —— “发电机”

    • 职责:真正的硬件驱动代码(Android/iOS)。
    • 例子UhfModule.kt

三、 手把手教你实现(以 RFID 为例)

我们按照从底向上的顺序,一步步搭建这套架构。

第一步:编写 Manager(技术员)

Manager 是单例模式,负责封装原生方法的脏活累活。

// src/utils/RFIDManager.js
import { NativeModules, DeviceEventEmitter } from 'react-native';

const { HnaoUhf } = NativeModules;

class RFIDManager {
  constructor() {
    this.listeners = new Set(); // 存放所有的监听者
    this.isScanning = false;
  }

  // 添加监听
  startListening(callback) {
    this.listeners.add(callback);

    // 💡 智能管理:如果是第一个人来监听,我才启动硬件
    if (this.listeners.size === 1) {
      this._startHardware();
    }

    // 返回一个取消订阅的函数
    return () => {
      this.listeners.delete(callback);
      // 💡 智能管理:如果最后一个人走了,我就关闭硬件省电
      if (this.listeners.size === 0) {
        this._stopHardware();
      }
    };
  }

  _startHardware() {
    if (!this.isScanning) {
      HnaoUhf.initAndStart(); // 调用原生
      this.isScanning = true;
      // ...绑定 DeviceEventEmitter
    }
  }

  _stopHardware() {
    if (this.isScanning) {
      HnaoUhf.stop(); // 调用原生
      this.isScanning = false;
    }
  }
}

// 导出单例
export default new RFIDManager();

第二步:创建 Context(总管)

Context 负责把 Manager 的能力暴露给整个 App。

// src/context/RFIDContext.js
import React, { createContext } from 'react';
import rfidManager from '@/utils/RFIDManager';

export const RFIDContext = createContext(null);

export const RFIDProvider = ({ children }) => {
  return (
    // 把 manager 传下去,或者只传一些状态
    <RFIDContext.Provider value={{ rfidManager }}>
      {children}
    </RFIDContext.Provider>
  );
};

别忘了在 App.js 顶层包上它:

// App.js
export default function App() {
  return (
    <RFIDProvider>
      <AppContainer />
    </RFIDProvider>
  );
}

第三步:封装 Hook(秘书)

Hook 让业务页面用起来极其简单。它自动处理了 React 的生命周期。

// src/context/RFIDContext.js (通常和 Context 写在一起)

import { useEffect, useRef } from 'react';

export const useRFID = (onRead) => {
  // 使用 useRef 保证 callback 即使变化也不会导致频繁重连
  const savedCallback = useRef(onRead);
  
  useEffect(() => {
    savedCallback.current = onRead;
  }, [onRead]);

  // 🌟 重点优化:使用 useFocusEffect 确保只有页面可见时才监听
  // 需要引入: import { useFocusEffect } from '@react-navigation/native';
  // 需要引入: import { useCallback } from 'react';
  useFocusEffect(
    useCallback(() => {
      // 1. 页面获得焦点:开启监听
      console.log('页面可见,开启硬件监听');
      const unsubscribe = rfidManager.startListening((epc) => {
        if (savedCallback.current) {
          savedCallback.current(epc);
        }
      });

      // 2. 页面失去焦点(跳转走或切后台):关闭监听
      return () => {
        console.log('页面不可见,关闭硬件监听');
        unsubscribe();
      };
    }, [])
  );
};

⚠️ 关键易错点(踩坑总结)

在实现这个 Hook 时,有一个非常容易踩的坑,也是很多初学者(甚至老手)容易忽视的地方:

不要只依赖 useEffect 的卸载(Unmount)机制!

❌ 错误的写法:
useEffect(() => {
  const unsubscribe = manager.startListening(...);
  return () => unsubscribe(); // 😱 只有组件销毁时才执行
}, []);
为什么是错的?

在 React Navigation 的堆栈导航(Stack Navigation)中,页面跳转并不等于组件销毁

  1. 当你从 Page A 跳转到 Page B 时,Page A 只是被压入栈底,它并没有被卸载(Unmount)
  2. 此时,Page A 的 useEffect 清理函数不会执行
  3. 结果:Page A 的监听器还在运行,Page B 的监听器也开启了。
  4. 灾难后果:你扫一个码,Page A 和 Page B 的回调同时被触发,导致逻辑混乱甚至 App 闪退。
✅ 正确的写法:

必须使用 useFocusEffect,它专门为了处理导航焦点而生。

  • 页面获得焦点(Focus) -> 开启监听
  • 页面失去焦点(Blur) -> 关闭监听

这样能确保永远只有当前用户看到的那个页面在响应硬件事件,实现真正的“所见即所得”。

第四步:业务页面使用(老板)

看!现在的业务页面多么清爽:

// src/pages/assets/TaskRFidScan.js
import { useRFID } from '@/context/RFIDContext';

const TaskRFidScan = () => {
  // ✅ 只需要这一行!
  // 不需要关心什么时候 init,什么时候 stop
  useRFID((epc) => {
    console.log('读到了标签:', epc);
    findItemByRfid(epc);
  });

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

四、 成果总结

通过这次重构,我们获得了什么?

  1. 极简的业务代码:页面里再也看不到 NativeModules 这种底层代码了,全是业务逻辑。
  2. 自动化的生命周期
    • 页面加载 -> 自动开启硬件。
    • 页面离开 -> 自动关闭硬件。
    • 多个页面同时存在 -> Manager 统一管理,不会重复初始化。
  3. 更强的健壮性:即使新手写代码,也不会因为忘记关闭硬件而导致 Bug。
  4. 易于测试和模拟:因为逻辑分层了,我们可以在 Manager 层 Mock 数据,方便在模拟器上开发。

五、 写给小白的建议

  • 不要害怕重构:一开始为了赶进度写出“面条代码”很正常,但当发现逻辑重复时,就是重构的好时机。
  • Context 是好东西:对于这种“全局单例”的硬件资源(打印机、扫描头、定位),用 Context 管理是最合适的。
  • Hook 是逻辑复用的利器:把复杂的生命周期封装在 Hook 里,让组件保持纯净。

本文档基于 Mx Phone 项目重构实践。