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();
};
}, []);
这种写法的问题:
- 代码重复:每个用扫码的页面都要写一遍
init和stop。 - 生命周期混乱:如果从 PageA 跳转到 PageB,PageA 还没来得及
stop,PageB 又去init,硬件可能会崩溃。 - 难以维护:如果哪天硬件 SDK 变了,你要改几十个文件。
二、 理想的架构(各司其职)
为了解决上面的问题,我们设计了一套 5层架构。每一层都有明确的职责,绝不越权。
🏗️ 架构图解
graph TD
A[Page 业务页面] -->|使用| B(Hook 钩子函数)
B -->|消费| C{Context 全局上下文}
C -->|持有| D[Manager 管理器]
D -->|调用| E[Native 原生模块]
🎭 角色扮演说明
-
Page (业务页面) —— “老板”
- 职责:只管下达命令(“我要监听 RFID”),不关心具体怎么做。
- 例子:
TaskRFidScan.js
-
Hook (钩子函数) —— “秘书”
- 职责:负责帮老板安排琐事(注册监听、页面关闭时自动取消监听)。
- 例子:
useRFID,useScan
-
Context (全局上下文) —— “总管”
- 职责:掌控全局状态,确保整个 App 只有一个硬件管理器实例,防止冲突。
- 例子:
RFIDContext,ScanContext
-
Manager (管理器) —— “技术员”
- 职责:干实事的。处理原生指令、防抖、数据格式化。
- 例子:
RFIDManager,PhysicalKeyScanManager
-
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)中,页面跳转并不等于组件销毁。
- 当你从 Page A 跳转到 Page B 时,Page A 只是被压入栈底,它并没有被卸载(Unmount)。
- 此时,Page A 的
useEffect清理函数不会执行。 - 结果:Page A 的监听器还在运行,Page B 的监听器也开启了。
- 灾难后果:你扫一个码,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>;
};
四、 成果总结
通过这次重构,我们获得了什么?
- 极简的业务代码:页面里再也看不到
NativeModules这种底层代码了,全是业务逻辑。 - 自动化的生命周期:
- 页面加载 -> 自动开启硬件。
- 页面离开 -> 自动关闭硬件。
- 多个页面同时存在 -> Manager 统一管理,不会重复初始化。
- 更强的健壮性:即使新手写代码,也不会因为忘记关闭硬件而导致 Bug。
- 易于测试和模拟:因为逻辑分层了,我们可以在 Manager 层 Mock 数据,方便在模拟器上开发。
五、 写给小白的建议
- 不要害怕重构:一开始为了赶进度写出“面条代码”很正常,但当发现逻辑重复时,就是重构的好时机。
- Context 是好东西:对于这种“全局单例”的硬件资源(打印机、扫描头、定位),用 Context 管理是最合适的。
- Hook 是逻辑复用的利器:把复杂的生命周期封装在 Hook 里,让组件保持纯净。
本文档基于 Mx Phone 项目重构实践。