React Native for OpenHarmony 三方库集成实战:设备巡检

验证日期: 2026-09-26
受测宿主:RN能力库 0.3.1

一、应用背景

设备诊断页往往要同时展示设备名称、系统版本、电量、运营商、区域、开机时长和应用版本。它们来自不同的 JS API、TurboModule 和系统服务,返回类型也不一致:有的同步返回,有的返回 Promise,有的在没有 SIM 卡时返回 null。

巡检是诊断流程,不是全量成功的交易。电量读取失败不应让页面连设备型号也不显示;但“没有 SIM 卡”和“运营商系统服务失败”又不能都写成“不可用”。因此本文把每个字段的状态保留下来,再由页面决定如何展示。
在这里插入图片描述

图 1:真机返回设备、系统、电量、区域和运行时长等巡检数据。

二、应用目标

  • 并发读取七类设备和应用信息;
  • 单项失败时保留其他字段和错误原因;
  • 将同步 getter、异步 Promise 和 RNOH 原生模块转换为统一快照;
  • 防止重复刷新和旧请求覆盖新结果;
  • 使用系统设置页交叉核对应用名称和版本。

三、三方库与版本

三方库锁定版本适配 TAG职责
expo-device57.0.257.0.2-ohos-1.0.0设备和系统字段
expo-battery57.0.357.0.3-ohos-1.0.0电源状态
expo-cellular57.0.257.0.2-ohos-1.0.0运营商名称
expo-localization57.0.257.0.2-ohos-1.0.0语言和地区
react-native-device-name1.0.01.0.0-ohos-1.0.0设备名称
react-native-device-uptime1.0.01.0.0-ohos-1.0.0开机运行时长
react-native-app-info0.0.60.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、权限拒绝、系统不支持和旧缓存四类降级结果,不能仅凭“所有字段都有值”判断功能完整。

七、参考链接

欢迎加入 RN for OpenHarmony 社区。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐