react-native-device-name 做的事很小:读一个"用户能看到的设备型号名"。埋点上报、设备列表、客服排查、多端登录设备管理这类场景会用到它。

它的公开 API 只有一个方法,但它是必须做原生适配的那类库——因为它的 JavaScript 只是一层壳。上游的入口文件只有三行:

import { NativeModules } from 'react-native';
const { DeviceName } = NativeModules;
export default DeviceName;

真正的实现在上游的 ios/DeviceName.m 和 android/src/main/java/com/reactlibrary/DeviceNameModule.java 里。没有鸿蒙实现,所以在鸿蒙上这个原生模块根本注册不出来。

本文讲清四件事:怎么快速判断一个库"必须有原生实现"、适配要补什么、一个返回"设备相关非固定值"的接口该怎么验证、以及交付包里几处可以补的文档缺口。

环境准备:本文不重复环境搭建步骤。RNOH(React Native for OpenHarmony)开发环境的完整配置见官方开发者指南:
https://atomgit.com/CPF-RN/docs/blob/main/开发者指南/02-搭建准备/环境初始化.md

在这里插入图片描述


一、先说结论:这是一个「JS 只是壳」的库

项结论
需要原生适配吗✅ 需要
适配要补什么一个鸿蒙原生模块(HAR + ArkTS TurboModule)
补的体量ArkTS 侧 8 行;HAR 源码工程 9 个文件、预编译产物 2.6 KB
需要权限吗❌ 不需要
公开 API只有 1 个:getDeviceName(): Promise<string>

判断依据很直接:看 package.json 有没有 harmony.autolinking。

"harmony": {
  "alias": "react-native-device-name",
  "autolinking": {
    "ohPackageName": "@react-native-ohos/react-native-device-name",
    "etsPackageClassName": "DeviceNamePackage",
    "cppPackageClassName": "DeviceNamePackage",
    "cmakeLibraryTargetName": "rnoh_device_name"
  }
}

二、判定过程:怎么确认必须做原生适配

选库阶段就能判断,不用先装进来。

第一步:看 package.json 有没有 harmony 字段

npm view <包名> harmony --json

有 harmony.autolinking(ohPackageName / etsPackageClassName / cppPackageClassName / cmakeLibraryTargetName 那四个名字)的,一定是带原生实现的包,适配时要做 link-harmony + ohpm + hvigor 三件事。

没有这个字段的可能是纯 JS 库,也可能像本例一样——上游没做鸿蒙适配,所以字段是后加的。所以要继续看第二步。

第二步:看上游包里有哪些平台的实现

$ npm pack react-native-device-name@1.0.0
$ tar -xzf react-native-device-name-1.0.0.tgz
$ ls package
index.js  package.json  README.md
android/  ios/  react-native-device-name.podspec

android/ 和 ios/ 两个目录都在,但没有 harmony/ —— 这就是"必须补一个鸿蒙实现"的直接证据。

第三步:读入口文件,看它到底在做什么

// package/index.js —— 上游的全部 JS
import { NativeModules } from 'react-native';
const { DeviceName } = NativeModules;
export default DeviceName;

入口里没有任何算法,只是在取一个原生模块。 这类库的"本体"是原生实现,JS 那层换个平台就得靠新的原生模块顶上去。

反过来,如果入口里是完整的业务逻辑(字符串处理、编解码、状态机),只是偶尔 Platform.OS === 'ios' 分叉一下——那通常是"纯 JS 库 + 平台差异",优先考虑在 JS 侧解决,不一定要写原生。

三、适配实现:取值链与协作方式

交付版新增的 harmony/device_name/ 结构:

harmony/device_name/
├── Index.ets                                   # 导出 DeviceNamePackage
├── oh-package.json5                            # 声明包名 @react-native-ohos/react-native-device-name
├── build-profile.json5
└── src/main/
    ├── module.json5
    ├── cpp/                                    # CAPI 架构下的 C++ 侧(只是个注册壳)
    │   ├── CMakeLists.txt
    │   ├── DeviceNamePackage.h                 # Package + TurboModule 工厂 + methodMap
    │   └── DeviceNamePackage.cpp
    └── ets/
        ├── DeviceNamePackage.ets               # 把 TurboModule 交给 RNOH
        └── DeviceNameTurboModule.ts            # ★ 全部实现逻辑

ArkTS 侧的全部实现只有一行取值逻辑:

import {UITurboModule} from '@rnoh/react-native-openharmony/ts';
import deviceInfo from '@ohos.deviceInfo';

export class DeviceNameTurboModule extends UITurboModule {
  async getDeviceName(): Promise<string> {
    return deviceInfo.marketName || deviceInfo.productModel || 'OpenHarmony device';
  }
}

取值链:marketName → productModel → 兜底文本 'OpenHarmony device'。

  • marketName 是面向用户的市场名(真机上通常是 “HUAWEI Mate 60” 这类),业务展示应该优先用它;
  • productModel 是产品型号,在模拟器或未配置市场名的设备上更可能有值;
  • 最后一层是兜底,保证接口永远返回非空字符串——因为契约是 Promise<string>、调用方通常直接展示。

@ohos.deviceInfo 读的是系统公开参数,不需要任何权限。

C++ 侧只是个注册壳,一行业务逻辑都没有:

class DeviceName : public ArkTSTurboModule {
 public:
  DeviceName(const ArkTSTurboModule::Context ctx, const std::string& name) : ArkTSTurboModule(ctx, name) {
    methodMap_ = {
      ARK_ASYNC_METHOD_METADATA(getDeviceName, 0),
    };
  }
};

ARK_ASYNC_METHOD_METADATA(getDeviceName, 0) —— 方法名、0 个参数、返回 Promise。方法名或参数个数写错,表现是"编译过了但调用报方法不存在"。

JS 侧的改动:把 undefined 隐患换成"早失败"

交付版的 src/index.ts 相比上游换了取模块的方式:

import {TurboModuleRegistry, type TurboModule} from 'react-native';

export interface Spec extends TurboModule {
  getDeviceName(): Promise<string>;
}

export default TurboModuleRegistry.getEnforcing<Spec>('DeviceName');

上游是 const { DeviceName } = NativeModules——模块没注册时得到 undefined,要等到第一次调用才炸。交付版用 TurboModuleRegistry.getEnforcing,模块缺失时在导入阶段就抛出明确错误。

这个改动很小,但对排障很有用:接入出错时你会在启动日志里立刻看到,而不是在业务跑到某个分支时才报 Cannot read property 'getDeviceName' of undefined。

四、接入宿主:三处改动面(外加一处自动生成的)

库本身不能独立运行,必须有一个 RNOH 宿主 App。这里用社区现成的 RNOH084Demo(RNOH 0.84.3 的多库验证宿主),它自带一个 rnAppKey 机制,一个宿主可以挂很多独立测试页:

// harmony/entry/src/main/ets/entryability/EntryAbility.ets
const rnAppKey = want.parameters?.['rnAppKey'] as string | undefined;
AppStorage.setOrCreate('rnAppKey', rnAppKey ?? 'RNOH084Demo');

于是可以用命令行参数切换测试页:

hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey DeviceNameTestApp

接入要改的地方

第一处:package.json。

"react-native-device-name": "file:../react-native-device-name"

第二处:两级 oh-package.json5 都要写 HAR。

"@react-native-ohos/react-native-device-name":
  "file:../node_modules/react-native-device-name/harmony/device_name.har",

harmony/oh-package.json5 管工程级、harmony/entry/oh-package.json5 管模块级,两处都要加。只加一处会出现"能找到包但链接不上"。

这里有个很容易漏的点:跑 link-harmony 时,它只会自动更新工程级那一份,模块级那份要你自己加。

第三处:在 ETS 侧注册 Package。

// harmony/entry/src/main/ets/RNOHPackagesFactory.ets
import type { RNPackageContext, RNOHPackage } from '@rnoh/react-native-openharmony';
import DeviceNamePackage from '@react-native-ohos/react-native-device-name';

export function createRNOHPackages(ctx: RNPackageContext): RNOHPackage[] {
  return [
    new DeviceNamePackage(ctx),
  ];
}

代码写在哪,这里说清楚:手工改动面就是这三个文件(外加 metro.config.js 的 watchFolders)。C++ 侧不用手改——CAPI 架构下 PackageProvider.cpp 会自动消费 autolinking 生成的 RNOHPackagesFactory.h。

那"自动生成的一处"是什么? 执行 link-harmony 时,它会一次性重写这四个文件:

• harmony/entry/src/main/cpp/RNOHPackagesFactory.h   # C++ 侧注册
• harmony/entry/src/main/cpp/autolinking.cmake       # add_subdirectory + 链接
• harmony/entry/src/main/ets/RNOHPackagesFactory.ets # ETS 侧注册
• harmony/oh-package.json5                           # 工程级 HAR 依赖

这四个文件头部都写着 DO NOT modify it manually, your changes WILL be overwritten.——别手改。

接入成功的两个自检信号

$ node_modules\.bin\react-native link-harmony --verbose
[link] react-native-device-name
[skip] @react-native-oh/react-native-harmony
...
info updated 4 file(s), linked 6 libraries, skipped 1 library

[link] react-native-device-name 在列表里,说明 autolinking 认出了它的 harmony.autolinking。打包时 Metro 也会把它列进重定向清单:

[INFO] Redirected imports to 6 harmony-specific third-party package(s):
[INFO] • react-native-device-name → react-native-device-name
…

这两个信号对这个库是"必须出现":有原生实现的包要被解析到它的鸿蒙实现,列表里没有它就说明没接通。

(harmony/entry/src/main/module.json5 未改动——本库不读任何需要权限的信息,不用加 requestPermissions。)

五、构建与运行

# 1) 装 JS 依赖 + 自动链接
npm install
./node_modules/.bin/react-native link-harmony

# 2) 生成调试签名 + 装 ohpm 依赖
cd harmony
devecocli signature generate
ohpm install --all

# 3) 打包 JS bundle(输出到 harmony/entry/src/main/resources/rawfile/)
cd ..
npm run dev

# 4) 编译 HAP
cd harmony
hvigorw --mode module -p product=default -p module=entry@default assembleHap --no-daemon

# 5) 安装 + 启动测试页
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
# 换页参数只在「冷启动」时生效:先 force-stop 再起
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey DeviceNameTestApp

耗时:在已有原生编译缓存的宿主上增量加入这个库,assembleHap 用了 6 分 35 秒;HAP 从 80.09 MB 涨到 80.23 MB(约 +136 KB)。

如果宿主是全新 clone(没有原生编译缓存),首次构建会到 30–40 分钟量级;只改 JS 重新打包也要 6–7 分钟,所以别把 UI 微调留到最后做。

看日志:

hdc shell "hilog -x | grep -i 'TM created'"
hdc shell "hilog -x | grep -i 'device-name-test'"

看界面(读无障碍树,不用截图就能拿到文本):

devecocli ui layout

按文本点击(页面高度会随结果卡片变化,别记固定坐标):

. E:\rnoh-work\click-label.ps1
Click-Label -Pattern '读取设备名并跑全部断言'

六、验证设计:设备相关的非固定值怎么验

这个库只有一个方法,而且返回值不是固定值——它取决于跑在什么设备上。

这就带来一个验证陷阱:如果只断言"返回非空字符串",那么返回 "a"、"随便什么"、"OpenHarmony device" 都会通过——等于没验证。

所以验证要回答两个不同的问题:

问题怎么验
接口形态对不对返回 Promise、类型是 string、非空、无空白/控制字符、长度合理、多次调用一致
返回值对不对与设备系统参数交叉核验(期望值由外部独立读出)

期望值从哪来:用 hdc 独立读系统参数

$ hdc shell param get const.product.marketname
Get parameter "const.product.marketname" fail! errNum is:106!     ← 不存在

$ hdc shell param get const.product.model
emulator

$ hdc shell param get const.product.name
emulator

$ hdc shell param get const.product.brand
HUAWEI

$ hdc shell param get const.product.os.dist.name
HarmonyOS

$ hdc shell param get const.product.os.dist.apiname
26.0.0

$ hdc shell param get const.product.software.version.name
HarmonyOS NEXT Developer Beta1

这一步是整个验证的关键:期望值来自系统本身(param get),不是从被测库拿的。如果库返回一个自己编的字符串,它不可能同时等于 const.product.model 和 const.product.name。

而且这次还额外覆盖到了回退分支:本机 const.product.marketname 不存在,也就是 deviceInfo.marketName 为空,所以实现必然走第二条分支取 productModel → 期望返回值是 "emulator"。

⇒ 一次验证同时确认了两件事:

  1. 值是对的(等于系统参数给出的 productModel);
  2. 回退分支真的生效了(marketName 为空时确实回退,而不是返回空串或抛错)。

七、真机验证

验证环境:Pura X View 模拟器,HarmonyOS 7.0.0(26.0.0) Beta2,API 26,ohos-x64。

TurboModule 注册

RNInstance::TurboModuleProvider  TM created: DeviceName

在这里插入图片描述

断言结果:10 / 10 全部通过

A 组:契约与形态(7 项)

断言结果
返回 Promise(typeof .then === 'function')✅
返回类型是 string✅
非空字符串✅
无首尾空白✅
无控制字符(/[\u0000-\u001f\u007f]/ 不匹配)✅
长度不超过 64✅
连续三次调用结果一致✅

B 组:值正确性(3 项)

断言结果
等于 const.product.model(emulator)✅
等于 const.product.name(emulator)✅
未落到兜底文本 'OpenHarmony device'✅

在这里插入图片描述

原始读数与耗时

[device-name-test] getDeviceName() 连续三次:"emulator" / "emulator" / "emulator"
[device-name-test] 单次耗时:31 ms / 4 ms / 0 ms
[device-name-test] A + B(契约 / 形态 / 值正确性) -> 10/10 全部通过

首次调用 31 ms(含 TurboModule 创建与 ArkTS 侧首次加载 @ohos.deviceInfo),之后 4 ms / 0 ms。这是个"读一次就能缓存"的接口,业务侧没必要反复调用。

能力对照

能力结果
getDeviceName() 返回 Promise✅
返回非空字符串✅
返回值等于设备系统参数中的 productModel✅ 独立交叉核验通过
marketName 为空时的回退分支✅ 本机实测走的就是这条分支
需要权限✅ 不需要(宿主未新增任何 requestPermissions)
TurboModule 注册✅ TM created: DeviceName

八、交付包可补的地方

运行时没有问题,但交付包在可追溯性上有三处缺口,如实记下来:

一、README 里的上游地址是占位符。 README.OpenHarmony_CN.md / README.OpenHarmony.md 写的都是 github_account/react-native-device-name——这不是真实上游地址,读者按 README 找不到源仓库。应该换成上游真实地址。

二、spec.json 没记录上游仓库与基线 commit。 现有一份 spec.json 只有 name / version / module / native / description / methods / rnoh / reactNative / validation,没有 upstream 字段、也没有 upstreamCommit。对交付包来说,“我验的是上游哪一版"是核心信息——缺了它就无法复现,也无法回答"上游发新版后这版适配是否还对应”。

三、契约测试没有真正测到东西。

const test = require('node:test');
const assert = require('node:assert/strict');

test('public contract returns a promise of a non-empty device name', () => {
  assert.equal(typeof 'DeviceName', 'string');
  assert.equal(typeof Promise.resolve('OpenHarmony device').then, 'function');
});

这两条断言的对象是字面量——typeof 'DeviceName' 永远是 'string',typeof Promise.resolve(...).then 永远是 'function'。测试没有 import 本库,也没有调用任何方法,所以无论库是否可用都会通过。这不是验证,只是占位。

根因推测:上游 npm 包本身就是没改过的 RN 库模板——package/package.json 里 "description": "TODO"、"author": {"name": "Your Name", "email": "yourname@email.com"}、repository.url 指向 github_account/...。交付时把上游的占位符照抄进了 README,也没另外补 spec.json 的上游字段。

顺带一个上游打包问题:上游 tarball 92,757 字节里塞满了构建产物——android/build/、android/.gradle/、android/.idea/(其中 gradle_models.ser 56 KB、executionHistory.bin 32 KB)。这是上游 .npmignore 配置的问题,不是交付方引入的,但会让包体积膨胀十几倍。

九、已知限制

一、只读系统公开字段,不读用户自定义名称。 实现取的是 deviceInfo.marketName / deviceInfo.productModel,不是用户在设置里改过的设备名。如果你的业务需要"用户给设备起的名字",这个库不满足——那通常要调设置类接口,而且往往需要权限。

二、不同 ROM 的字段取值可能不同。 const.product.marketname / const.product.model 是否存在、格式如何,取决于 ROM。本次只在一台模拟器上验证,实测 marketName 为空、回退到 productModel。

三、回退到兜底文本的分支未实测。 只有 marketName 与 productModel 同时为空时才会返回 'OpenHarmony device'。模拟器上至少有一个有值,无法构造这个场景——所以"兜底文本"这条路径只有代码层面可见,没有设备侧证据。

四、真机上的返回值很可能与模拟器不同。 真机通常有市场名(如 “HUAWEI Mate 60”),届时返回的会是 marketName 而不是 productModel。业务展示应该允许名称变化,不要把它当作稳定标识符做逻辑判断。

五、返回值不是设备唯一标识。 同一个型号名会出现在成千上万台设备上。它只适合展示,不适合做设备身份——需要唯一标识时应该用别的方案。

六、未测大并发与高频调用。 接口本身没有缓存,每次调用都会走过桥。实测首次 31 ms、后续 0–4 ms;业务侧建议自己缓存结果,不要放在高频渲染路径里。

七、其他 ROM / 真机未验证。 适配方记录的是 OpenHarmony-7.0.0.105;本次在 HarmonyOS 7.0.0(26.0.0) Beta2 模拟器上通过。

十、常见问题

Q:这个库为什么必须做原生适配?
A:它的 JS 入口只有三行——export default NativeModules.DeviceName,本体是原生实现。上游只提供了 ios/DeviceName.m 和 android/.../DeviceNameModule.java,没有鸿蒙实现。不补一个鸿蒙原生模块,NativeModules.DeviceName 就是 undefined,调用必然失败。

Q:怎么快速判断一个库要不要做原生适配?
A:三步。① npm view <包名> harmony --json 看有没有 harmony.autolinking(有 → 一定是带原生实现的适配包);② 上游包里有哪些平台的实现目录(只有 ios/ + android/ 而没有 harmony/ → 要补一个鸿蒙实现);③ 读入口文件——如果里面只有"取原生模块"而没有任何业务逻辑,那它天生依赖原生。

Q:适配一共要改多少东西?
A:ArkTS 侧的实现逻辑只有一行(deviceInfo.marketName || deviceInfo.productModel || 'OpenHarmony device'),加上 Package/CMake/Index 的接线一共 9 个文件,预编译 HAR 只有 2.6 KB。接入宿主时手工改动面是三处:package.json 依赖、模块级 oh-package.json5 加 HAR、RNOHPackagesFactory.ets 注册 Package。

Q:为什么返回的是 "emulator" 而不是设备市场名?
A:因为实现的取值链是 marketName → productModel → 兜底文本,而这台模拟器的 marketName 为空(hdc shell param get const.product.marketname 返回不存在),所以回退到了 productModel(emulator)。真机上通常有市场名,返回值会不同——这也是为什么验证不能只断言"非空"。

Q:怎么验证一个"返回值不固定"的接口?
A:把验证拆成两层:① 形态层——返回 Promise、类型、非空、无空白/控制字符、多次一致;② 值层——找一个外部独立来源作为期望值。本例用的是 hdc shell param get const.product.model,从系统本身读出期望值,而不是从被测库拿。如果库返回的是自己编的字符串,它不可能同时等于两个不同的系统参数。

Q:返回的设备名能当唯一标识用吗?
A:不能。同一个型号名会出现在大量设备上。它只适合展示。需要唯一标识请用别的方案。

Q:能不能读用户自己改的设备名?
A:不能。实现只读 @ohos.deviceInfo 的系统公开字段,不读用户自定义名称,也不需要任何权限。业务如果依赖用户自定义名称,得另找接口。

Q:spec.json 里没有上游 commit,会不会有问题?
A:不影响运行,但影响可追溯性。交付包的价值之一是能回答"我验的是上游哪一版、上游发新版后这版是否还对得上"。缺 upstream / upstreamCommit 就答不了。README 里那个 github_account/... 也是占位符,建议一并补上。

Q:为什么我在模拟器上编译要这么久?
A:宿主已有原生编译缓存时,增量加一个库约 6–7 分钟(hvigor 会把整套流水线走一遍);全新克隆的宿主首次编译要 30–40 分钟。只改 JS 重新打包也是 6–7 分钟。

小结

这个库的适配体量是"一行取值 + 三层接线",但有两件事值得记:

第一件是判定。 一个 RN 三方库要不要做原生适配,看三点就够了:package.json 有没有 harmony.autolinking、上游包里有哪些平台的实现目录、入口文件里有没有真实业务逻辑。本例的入口只有三行、只是在取一个原生模块——这类库的本体在原生侧,换个平台就必须补实现。判断对了,就清楚知道工作量在哪;判断错了(比如把一个"JS 只是壳"的库当纯 JS 用),会在运行时才发现模块是 undefined。

第二件是"非固定返回值"的验证设计。 这个库只有一个方法、返回一个随设备变化的字符串。只断言"非空"等于没验证——任何字符串都能过。所以要把验证拆成两层:形态层查契约,值层找外部独立来源做交叉核验。本例用的是 hdc shell param get:期望值由系统读出,不经由被测库。这样即使库返回一个自造的字符串,也无法同时等于两个不同的系统参数。

而且这次运气不错:本机 marketName 恰好为空,于是实测正好走过了"回退到 productModel"这条分支——一次验证同时确认了"值是对的"和"回退逻辑真的生效"。验证一个带取值链的接口时,先弄清每条分支的触发条件,再挑一个能触发非首条分支的环境,比只测 happy path 有价值得多。

最后如实记了交付包的三处文档缺口:README 里上游地址还是占位符、spec.json 缺上游 commit、契约测试断言的是字面量(没 import 库、没调用方法,怎么跑都会通过)。这三条不影响运行时,但影响"这个交付包能不能被信任和复现"——一个测试如果永远不会失败,它就不是测试。


本篇用到的库

项内容
三方库react-native-device-name(上游 1.0.0 的鸿蒙适配版)
交付仓库https://atomgit.com/oh-react-native/react-native-device-name
适配 TAG1.0.0-ohos-1.0.0
ohpm 包名@react-native-ohos/react-native-device-name
HARharmony/device_name.har(2.6 KB)
原生模块名DeviceName
是否需要权限不需要
宿主工程RNOH084Demo(测试页 rnAppKey = DeviceNameTestApp)
"react-native-device-name": "git+https://atomgit.com/oh-react-native/react-native-device-name.git#1.0.0-ohos-1.0.0"
// harmony/oh-package.json5 与 harmony/entry/oh-package.json5 都要加
"@react-native-ohos/react-native-device-name":
  "file:../node_modules/react-native-device-name/harmony/device_name.har",
import DeviceName from 'react-native-device-name';

const name = await DeviceName.getDeviceName();   // 例如 "emulator" / "HUAWEI Mate 60"
# 换页启动测试页(force-stop 不能省,换页参数只在冷启动生效)
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey DeviceNameTestApp

验证环境

项版本
React Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3
Node.jsv24.14.0
DevEco Studio26.0.0.621
HarmonyOS SDKAPI 26(26.0.0.32)
设备HarmonyOS 7.0.0(26.0.0) Beta2 模拟器 Pura X View(ohos-x64)
本机 const.product.modelemulator
本机 const.product.marketname不存在(errNum 106)
宿主 HAP 产物entry-default-signed.hap(80.23 MB)
本次增量构建assembleHap 6 分 35 秒
验证规模设备侧 10 项断言全通过 + 与系统参数交叉核验

欢迎加入 CPF-RN 鸿蒙社区:https://atomgit.com/CPF-RN

React Native for OpenHarmony 组织:https://atomgit.com/oh-react-native

RN 三方库鸿蒙适配清单:https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview

Logo

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

更多推荐