React Native for OpenHarmony 三方库集成实战:设备巡检
React Native for OpenHarmony 三方库集成实战:设备巡检
验证日期: 2026-09-26
受测宿主:RN能力库 0.3.1
一、应用背景
设备诊断页往往要同时展示设备名称、系统版本、电量、运营商、区域、开机时长和应用版本。它们来自不同的 JS API、TurboModule 和系统服务,返回类型也不一致:有的同步返回,有的返回 Promise,有的在没有 SIM 卡时返回 null。
巡检是诊断流程,不是全量成功的交易。电量读取失败不应让页面连设备型号也不显示;但“没有 SIM 卡”和“运营商系统服务失败”又不能都写成“不可用”。因此本文把每个字段的状态保留下来,再由页面决定如何展示。

图 1:真机返回设备、系统、电量、区域和运行时长等巡检数据。
二、应用目标
- 并发读取七类设备和应用信息;
- 单项失败时保留其他字段和错误原因;
- 将同步 getter、异步 Promise 和 RNOH 原生模块转换为统一快照;
- 防止重复刷新和旧请求覆盖新结果;
- 使用系统设置页交叉核对应用名称和版本。
三、三方库与版本
| 三方库 | 锁定版本 | 适配 TAG | 职责 |
|---|---|---|---|
expo-device | 57.0.2 | 57.0.2-ohos-1.0.0 | 设备和系统字段 |
expo-battery | 57.0.3 | 57.0.3-ohos-1.0.0 | 电源状态 |
expo-cellular | 57.0.2 | 57.0.2-ohos-1.0.0 | 运营商名称 |
expo-localization | 57.0.2 | 57.0.2-ohos-1.0.0 | 语言和地区 |
react-native-device-name | 1.0.0 | 1.0.0-ohos-1.0.0 | 设备名称 |
react-native-device-uptime | 1.0.0 | 1.0.0-ohos-1.0.0 | 开机运行时长 |
react-native-app-info | 0.0.6 | 0.0.6-ohos-1.0.0 | 应用名称和版本 |
以上版本、源仓库和 TAG 于 2026-09-26 核对。受测运行时为 React Native 0.84.1、React 19.2.3、RNOH 0.84.3、DevEco Studio/SDK 26.0.0。
四、应用方式
安装包和自动链接完成后,在 HarmonyOS 工程内安装 HAR 并生成 bundle:
npm install
./node_modules/.bin/react-native link-harmony
cd harmony && ohpm install --all && cd ..
npm run bundle:harmony
受测宿主使用 local-packages/ 中与上表版本一致的 .tgz。采集函数把同步和异步调用拆开,再用 Promise.allSettled 保留单项结果:
import * as Battery from 'expo-battery';
import * as Cellular from 'expo-cellular';
import * as Device from 'expo-device';
import {getLocales} from 'expo-localization';
import AppInfo from 'react-native-app-info';
import DeviceName from 'react-native-device-name';
import DeviceUptime from 'react-native-device-uptime';
export async function inspectDevice() {
const [battery, carrier, name, uptime] = await Promise.allSettled([
Battery.getPowerStateAsync(),
Cellular.getCarrierNameAsync(),
DeviceName.getDeviceName(),
DeviceUptime.getUptime(),
]);
const locale = getLocales()[0];
return {
device: name.status === 'fulfilled'
? name.value
: Device.modelName || Device.deviceName || '未知设备',
system: [Device.osName, Device.osVersion].filter(Boolean).join(' '),
battery: battery.status === 'fulfilled'
? Math.round(battery.value.batteryLevel * 100)
: null,
carrier: carrier.status === 'fulfilled'
? carrier.value || null
: undefined,
locale: locale?.languageTag || null,
uptimeMs: uptime.status === 'fulfilled'
? Number(uptime.value)
: null,
app: {
name: AppInfo.getInfoDisplayName(),
version: AppInfo.getInfoShortVersion(),
},
};
}
getLocales()、Device 字段和 AppInfo getter 是同步读取;电量、运营商、设备名称和运行时长是异步调用。react-native-device-uptime 返回字符串形式的毫秒值,转换前要做有限数检查。carrier 为 null 表示业务上没有运营商,undefined 则表示调用失败,页面不能把两者混成一个文案。
五、工程实现
用字段状态而不是一串展示文本。
巡检快照会被页面、工单和日志同时消费,应该保留字段级状态:
type FieldStatus = 'ok' | 'unavailable' | 'unsupported' | 'error';
type Field<T> = {
status: FieldStatus;
value: T | null;
source: string;
errorCode?: string;
};
type DeviceInspection = {
schemaVersion: 1;
collectedAt: string;
device: Field<string>;
system: Field<string>;
battery: Field<number>;
carrier: Field<string>;
locale: Field<string>;
uptimeMs: Field<number>;
app: Field<{name: string; version: string}>;
};
电量越界、运行时长不是有限数、系统版本为空都应在适配层标为 error,不能把 NaN 或 undefined 交给渲染层。页面展示“未插卡”时应来自 carrier.value === null 的业务状态,而不是根据错误字符串猜测。
用请求序号处理刷新竞态。
设备服务通常没有统一的取消接口。用户快速点击两次刷新时,可以让新请求取代旧请求,并在写回 React state 前比较序号:
let requestId = 0;
async function refreshSnapshot() {
const current = ++requestId;
setStatus('loading');
try {
const snapshot = await inspectDevice();
if (current !== requestId) return;
setSnapshot(normalizeSnapshot(snapshot));
setStatus('ready');
} catch (error) {
if (current !== requestId) return;
setStatus('error');
setError(toPublicError(error));
}
}
请求序号只能过滤旧结果,不能取消已经进入系统服务的调用。如果底层库提供取消能力,应在页面失焦或新请求开始时一并调用;否则至少保证组件卸载后不再更新已不存在的 UI。
缓存和字段时效。
设备型号、应用包名和应用版本在一次进程中基本稳定,可以缓存;电量、充电状态和运营商会变化,回到前台或用户手动刷新时应重新采集;运行时长持续增长,不应永久使用旧值。快照至少保存 collectedAt 和 schemaVersion,首屏可以先展示缓存,再按字段刷新。
type CachedSnapshot = {
schemaVersion: 1;
collectedAt: string;
values: DeviceInspection;
};
function isStale(snapshot: CachedSnapshot, staleAfterMs: number) {
return Date.now() - Date.parse(snapshot.collectedAt) > staleAfterMs;
}
缓存只用于首屏和无网诊断参考,不覆盖当前系统读取。上报或复制诊断报告前使用字段白名单,不记录完整设备标识和原始异常对象。
把三方库调用放在适配器里。 页面不应该直接知道某个字段来自哪个包。每个适配器负责一次调用、类型校验和错误码转换,领域层再把结果包装成 Field<T>:
async function readBattery(): Promise<Field<number>> {
try {
const state = await Battery.getPowerStateAsync();
const level = Number(state.batteryLevel);
if (!Number.isFinite(level) || level < 0 || level > 1) {
return {status: 'error', value: null, source: 'expo-battery', errorCode: 'range'};
}
return {
status: 'ok',
value: Math.round(level * 100),
source: 'expo-battery',
};
} catch {
return {
status: 'error',
value: null,
source: 'expo-battery',
errorCode: 'read-failed',
};
}
}
function normalizeCarrier(value: string | null | undefined): Field<string> {
if (value === null) {
return {status: 'unavailable', value: null, source: 'expo-cellular'};
}
if (typeof value !== 'string' || value.length === 0) {
return {status: 'error', value: null, source: 'expo-cellular', errorCode: 'invalid'};
}
return {status: 'ok', value, source: 'expo-cellular'};
}
适配器不应在失败时返回“未知设备”这类默认值,否则页面无法区分系统不支持和真实设备名称。只有领域层知道产品文案后,才将 unavailable 映射为“未插卡”或“系统不提供”。
按字段时效刷新前台数据。 AppState 回到 active 时,不必无条件重新读取七个字段。先计算离开时间,再只刷新已经过期的电量、运营商和运行时长;设备名称和应用版本可以继续使用当前快照:
import {AppState, useEffect} from 'react-native';
useEffect(() => {
let backgroundAt: number | null = null;
const subscription = AppState.addEventListener('change', next => {
if (next === 'background') {
backgroundAt = Date.now();
return;
}
if (next === 'active' && backgroundAt !== null) {
const elapsed = Date.now() - backgroundAt;
if (elapsed > 60_000) void refreshVolatileFields();
backgroundAt = null;
}
});
return () => subscription.remove();
}, []);
前台回调可能连续触发,refreshVolatileFields 仍需复用请求序号和进行中的 Promise,不能在每个事件里启动一套全量采集。
导出诊断报告时重新脱敏。 页面显示值、日志字段和客服复制文本的用途不同。导出器使用白名单并固定 schema,避免新增字段后把设备唯一标识、原始异常或网络令牌带出:
function toSupportReport(snapshot: DeviceInspection) {
return {
schemaVersion: snapshot.schemaVersion,
collectedAt: snapshot.collectedAt,
system: snapshot.system.value,
app: snapshot.app.value,
battery: snapshot.battery.value,
carrier: snapshot.carrier.status === 'ok' ? snapshot.carrier.value : null,
fieldStatus: {
battery: snapshot.battery.status,
carrier: snapshot.carrier.status,
uptime: snapshot.uptimeMs.status,
},
};
}
报告创建时生成 inspectionId,日志只使用这个 ID 关联字段耗时和错误码。复制“摘要”和“详细诊断”应提供不同字段集合,不能把内部异常堆栈直接放进剪贴板。
给页面提供稳定的服务门面。 页面只调用 collect()、refresh() 和 getCached(),不直接导入七个库。门面负责请求序号、缓存和结构化快照,后续替换某个上游库时,表单和工单页面不需要一起修改:
export class DeviceInspectionService {
private request = 0;
private cached: CachedSnapshot | null = null;
private inFlight: Promise<DeviceInspection> | null = null;
async collect(): Promise<DeviceInspection> {
if (this.inFlight) return this.inFlight;
const request = ++this.request;
this.inFlight = (async () => {
const raw = await inspectDevice();
if (request !== this.request) throw new Error('stale-inspection');
const snapshot = normalizeSnapshot(raw);
this.cached = {
schemaVersion: 1,
collectedAt: new Date().toISOString(),
values: snapshot,
};
return snapshot;
})();
try {
return await this.inFlight;
} finally {
this.inFlight = null;
}
}
getCached() {
return this.cached;
}
cancel() {
++this.request;
}
}
复用进行中的 Promise 可以减少用户连续刷新造成的系统调用,但 cancel() 仍然只是让旧结果失效;如果原生模块提供取消接口,门面应把取消动作一起转发。服务门面还可以集中记录每个字段的耗时和失败率,页面不需要在渲染过程中写日志。
分层排查和测试。
页面显示“不可用”时按三层排查:先从 Metro source map 确认实际加载的包版本,再检查 link-harmony 生成的 Package Factory、HAR 和 ohpm 依赖,最后检查 OpenHarmony 系统服务返回值。HAP 能启动只能说明基础 RNOH 正常,不能证明每个 TurboModule 已注册。
单元测试覆盖全部成功、单项 Promise 拒绝、运营商返回 null、电量越界和两次刷新乱序;集成测试确认 JS 入口、HAR 和自动链接;真机测试核对系统真实值。三层结果分别记录,避免把 UI 降级误判成原生模块失败。
测试断言应同时检查字段值和来源。例如 app.name 来自 react-native-app-info,系统版本来自 expo-device,不能因为两个字段碰巧都是字符串就认为映射正确。报告中保留 source 和 collectedAt 后,升级某个包时可以快速定位是返回值变化、自动链接问题还是页面格式化问题。
it('keeps a missing carrier different from a rejected carrier call', () => {
expect(normalizeCarrier(null).status).toBe('unavailable');
expect(normalizeCarrier(undefined).status).toBe('error');
expect(normalizeCarrier('China Mobile').value).toBe('China Mobile');
});
图 2:系统应用设置页显示 RN能力库 0.3.1,用于交叉核对 react-native-app-info。
六、真机验证
| 验证项 | 实测结果 | 结论 |
|---|---|---|
| HAP 安装与启动 | RN能力库 0.3.1 可启动 | 通过 |
| 设备和系统 | 返回华为畅享 90 Pro Max 与 OpenHarmony-7.0.0.105 | 通过 |
| 电量、运营商、区域、运行时长 | 汇总为结构化巡检摘要 | 通过 |
| 应用信息 | 与系统设置页显示一致 | 通过 |
| 多库接入 | 七个库可在同一宿主调用 | 通过 |
受测设备为 6UMBB26319007180,HAP SHA-256:a0f079982142ef5eb836f439af5e71c8f31f33c4988e80ab2a7575bee64b1f03。系统设置截图与巡检页面来自同一受测 HAP。
图 3:expo-device 库级真机验证中的设备信息场景,截图来自 2026-09-14 的独立包验证。
图 4:expo-device 库级真机验证中的系统信息场景。
图 5:expo-device 库级真机验证中的边界场景。
本次覆盖一台手机和一个系统版本,未覆盖无 SIM 卡、飞行模式、低电量保护、平板和多模块 HAP。发布前应针对这些情况验证 null、权限拒绝、系统不支持和旧缓存四类降级结果,不能仅凭“所有字段都有值”判断功能完整。
七、参考链接
- expo-device
- expo-battery
- expo-cellular
- expo-localization
- react-native-device-name
- react-native-device-uptime
- react-native-app-info
- RN能力库
欢迎加入 RN for OpenHarmony 社区。
更多推荐



所有评论(0)