设备信息获取:@ionic-native/device 插件在鸿蒙 OpenHarmony 平台的适配实践

使用 hionic CLI 在 React + Capacitor 项目中集成设备信息获取


背景

在移动端开发中,获取设备信息是高频需求——用于兼容性适配(不同屏幕尺寸、系统版本)、用户行为分析(设备型号分布)以及安全校验(设备唯一标识)。

@ionic-native/device 插件理论上提供全局 device 对象,可获取设备型号、操作系统、唯一标识符、制造商等硬件与软件环境信息。但在 hionic(鸿蒙 Capacitor 桥接框架)上,这个插件的适配遇到了不少陷阱,本文将完整记录从安装到最终通过纯 JS 方案拿到设备信息的全过程

项目开源地址:🔗 https://gitcode.com/jianguoxu/hionic


1. 安装插件

hionic plugin add @ionic-native/device

安装流程:npm 安装 → OHOS 检测 → config.xml 注册 → JS 桥接文件生成 → C++/ArkTS 原生代码下载。

日志中会看到类似信息:

> hionic plugin add @ionic-native/device
[i-ohos-plugin]: 目标不存在: /openharmony/cordova
...
[i-ohos-plugin]: copy folder to openharmony/cordova ...

2. 踩坑一:原生代码模块名不匹配

问题

插件的原生代码安装在 openharmony/cordova/ 目录下,但项目使用的模块名为 capacitoropenharmony/capacitor/)。

解决

手动将 C++ 和 ArkTS 代码复制到 capacitor 模块:

cp -r oh-plugins/cordova-plugin-device/src/main/cpp/Device \
      openharmony/capacitor/src/main/cpp/Device

cp -r oh-plugins/cordova-plugin-device/src/main/ets/components/Device \
      openharmony/capacitor/src/main/ets/components/Device

rm -rf openharmony/cordova

确认 CMakeLists.txt 包含 Device/Device.cpp


3. 踩坑二:Cordova 桥 vs Capacitor 桥 — 双桥架构的缝隙

问题

安装完成后,前端 window.device 始终为 undefined。调试发现:

cordovaExists: true
cordovaExecExists: false
cordovaKeys: "define, require, addWindowEventHandler"
capacitorPlugins: "Camera, TsCapacitorPlugin"

cordova.exec 不存在,Device 插件也未出现在 Capacitor.Plugins 中。

根因分析

hionic 的架构存在两条平行的原生桥:

注册宏 JS 调用方式
Cordova 桥 REGISTER_PLUGIN_CLASS(Device) cordova.exec(...)cordova.require('cordova/exec')
Capacitor 桥 REGISTER_CAP_PLUGIN(Device, Device) Capacitor.nativeCallback('Device', 'getDeviceInfo', {}, callback)

@ionic-native/device 的原生端代码(Device.cpp)只注册了 Cordova 桥

// Device.cpp
REGISTER_PLUGIN_CLASS(Device)  // 只注册了 Cordova 桥

@capacitor/camera 注册的是 Capacitor 桥

// Camera.cpp
REGISTER_CAP_PLUGIN(Camera, Camera)                          // Capacitor 桥
REGISTER_PLUGIN_METHOD(Camera, getPhoto, PluginMethod::RETURN_PROMISE)

hionic 的 WebView 通过 Capacitor 桥的 nativeCallback/nativePromise 与原生层通信,Cordova 桥注册的函数对 Capacitor 桥不可见

理论上可行的修复

Device.cpp 加上 Capacitor 桥注册(但装置复杂,见下节):

#include "getcapacitor/Plugin.h"
REGISTER_CAP_PLUGIN(Device, Device)
REGISTER_PLUGIN_METHOD(Device, getDeviceInfo, PluginMethod::RETURN_PROMISE)

4. 踩坑三:Cordova 模块系统不兼容 — 更深的坑

问题

即使尝试用 cordova_plugins.js 通过 cordova.define('Device', ...) 注册 Device 插件,window.device 依然不存在。

根因分析

@capacitor/core/cordova.js 的模块系统工作流程:

cordova.js 加载
  ├─ 创建本地 define/require(闭包内变量)
  ├─ define('cordova', ...)  ← 含 define/require 属性
  ├─ define('cordova/exec', ...)
  ├─ ...
  ├─ 同步执行:
  │   ├─ modulemapper.clobbers('cordova', 'cordova')        ← 只注册不执行
  │   ├─ modulemapper.clobbers('cordova/exec', 'cordova.exec')
  │   ├─ platform.bootstrap() → channel.onNativeReady.fire()
  │   └─ setTimeout(0) → pluginloader.load()
  │                           └─ require('cordova/plugin_list') ← 抛出异常!
  │                              ↑ 因为 cordova_plugins.js 加载时
  │                                window.cordova.define 还不存在
  └─ channel.join([onNativeReady, onPluginsReady])
       ↑ onPluginsReady 永不触发 → join 永不完成
         → modulemapper.mapModules(window) 永不执行
         → window.cordova = require('cordova') 永不执行
         → window.device 永不创建

关键问题有两个:

  1. cordova.define 不在 window.cordova@capacitor/core/cordova.jsdefine 函数定义在 IIFE 闭包内,window.cordova.define 只有通过异步的 modulemapper.mapModules(window) 才会赋值。同步的 <script> 标签加载 cordova_plugins.js 时,window.cordova.define 不存在。

  2. cordova/exec 在 OpenHarmony 上不可用@capacitor/core/cordova.jscapacitorExec 函数只检测 window.androidBridge(Android)和 window.webkit.messageHandlers.bridge(iOS),OpenHarmony 两者都没有。

最终方案:放弃 Cordova 模块系统

既然 Cordova 模块系统在 OpenHarmony 上无法正常工作,直接改用纯 JavaScript 方案获取设备信息:

// cordova_plugins.js — 直接在脚本执行时设置 window.device
(function () {
  var info = {};

  // 平台:通过 Capacitor API 或 User-Agent
  try {
    if (window.Capacitor && typeof window.Capacitor.getPlatform === 'function') {
      info.platform = window.Capacitor.getPlatform();
    }
  } catch (e) {}
  if (!info.platform) {
    var ua = navigator.userAgent || '';
    if (ua.indexOf('HarmonyOS') >= 0) info.platform = 'OpenHarmony';
    else info.platform = 'browser';
  }

  // 型号:从 User-Agent 解析
  var ua = navigator.userAgent || '';
  var modelMatch = ua.match(/; ([^;]+?)(?:;|\))/);
  info.model = modelMatch ? modelMatch[1].trim() : '';
  info.manufacturer = info.model ? info.model.split(' ')[0] : '';

  // UUID(localStorage 持久化)
  try {
    info.uuid = localStorage.getItem('device_uuid');
    if (!info.uuid) {
      info.uuid = 'DEVICE-' + Date.now().toString(36) + '-' + Math.random().toString(36).substr(2, 8);
      localStorage.setItem('device_uuid', info.uuid);
    }
  } catch (e) { info.uuid = 'unknown'; }

  info.version = '';
  info.isVirtual = false;
  info.cordova = '1.0.0';
  info.available = true;

  window.device = info;

  // 触发 deviceready 事件
  try {
    document.dispatchEvent(new Event('deviceready'));
  } catch (e) {}
})();

同时,前端代码也增加浏览器 API 降级逻辑,不依赖 window.device

useEffect(() => {
  var platform = ''
  try { if (window.Capacitor) platform = Capacitor.getPlatform() } catch(e) {}

  var ua = navigator.userAgent || ''
  var modelMatch = ua.match(/; ([^;]+?)(?:;|\))/)
  var model = modelMatch ? modelMatch[1].trim() : 'Unknown'
  var manufacturer = model.indexOf(' ') > 0 ? model.split(' ')[0] : ''

  var info = [
    { label: 'Model', value: model },
    { label: 'Platform', value: platform || 'unknown' },
    { label: 'Manufacturer', value: manufacturer || 'unknown' },
  ]

  // 如果 window.device 已设置(来自 cordova_plugins.js),优先使用
  if (window.device && window.device.available) {
    if (window.device.model) info[0].value = window.device.model
    if (window.device.platform) info[1].value = window.device.platform
    if (window.device.uuid) info.push({ label: 'UUID', value: window.device.uuid })
  }

  setDeviceInfo(info)
}, [])

5. 踩坑四:WebView 加载路径错误

问题

多次部署后设备信息仍然显示不出来。检查 HAP 包内容发现:

resources/rawfile/index.html          ← 我一直在更新的文件(capacitor 模块)
resources/rawfile/www/index.html      ← WebView 实际加载的文件(entry 模块)

Index.ets 中的注释明确写着:

// 默认加载 rawfile/www/index.html

而我之前一直在更新 capacitor/src/main/resources/rawfile/index.html(对应 HAP 中的 resources/rawfile/index.html),WebView 从未读到我的修改。

解决

更新正确的路径:

# 正确路径
openharmony/entry/src/main/resources/rawfile/www/cordova_plugins.js
openharmony/entry/src/main/resources/rawfile/www/index.html
openharmony/entry/src/main/resources/rawfile/www/assets/

# 错误路径(之前一直在改的)
openharmony/capacitor/src/main/resources/rawfile/index.html

6. 踩坑五:Platform 检测错误 - 显示"Android"而非"OpenHarmony"

问题

设备信息终于显示出来了,但 Platform 显示为 Android,而非预期的 OpenHarmony

根因分析

检查 hionic 的 C++ 桥接代码 MessageHandler.cpp

// openharmony/capacitor/src/main/cpp/getcapacitor/MessageHandler.cpp
ArkWeb_ProxyObject proxyObject = {"androidBridge", methodList, 1};

hionic 在 WebView 中注册的 JavaScript 代理对象名为 androidBridge —— 这是为了与标准 Capacitor 的 WebView 检测逻辑兼容。而 native-bridge.js 中的平台检测函数:

const getPlatformId = (win) => {
    if (win?.androidBridge) return 'android';        // ← hionic 匹配这里!
    else if (win?.webkit?.messageHandlers?.bridge) return 'ios';
    else return 'web';
};

因为 window.androidBridge 存在,Capacitor.getPlatform() 返回 'android'

解决

cordova_plugins.js 中,利用 hionic 特有的 TsCapacitorPlugin 作为判断依据:

function isHionic() {
  try {
    return !!(window.Capacitor && window.Capacitor.Plugins && 
              window.Capacitor.Plugins.TsCapacitorPlugin);
  } catch(e) { return false; }
}

if (isHionic() || navigator.userAgent.indexOf('HarmonyOS') >= 0) {
  info.platform = 'OpenHarmony';
} else {
  info.platform = Capacitor.getPlatform();
}

TsCapacitorPlugin 是 hionic 特有的 Capacitor 插件(标准 Capacitor 中没有),因此是判断 OpenHarmony 环境的可靠标志。


7. 完整发布流程

# 1. 构建前端
npm run build

# 2. 复制桥接文件和构建产物到 entry 模块的 rawfile/www/
WWW=openharmony/entry/src/main/resources/rawfile/www
cp cordova_plugins.js "$WWW/"
cp node_modules/@capacitor/core/cordova.js "$WWW/"
cp -r dist/assets/* "$WWW/assets/"

# 3. 构建 HAP(注意 Node.js 版本兼容)
cd openharmony
NODE_HOME=/Applications/DevEco-Studio.app/Contents/tools/node \
  hvigorw assembleApp --no-daemon -p product=default

# 4. 安装运行
hdc install entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.nutpi.MyApp

8. 构建环境注意

DevEco Studio 内置 Node.js v18.20.1,而 Homebrew 安装的 Node.js v26 与 hvigor 存在兼容性问题:

Error Code: 00308018 Unknown Error
The property 'options.recursive' is no longer supported.

原因:Node.js v20+ 移除了 fs.rmdirSync(path, { recursive: true })options.recursive 格式,v26 中直接报错。DevEco Studio 的 hvigor 插件仍使用旧格式。

解决:设置 NODE_HOME 环境变量使用 DevEco 内置 Node:

export NODE_HOME=/Applications/DevEco-Studio.app/Contents/tools/node

9. 三种插件调用对比

插件 类型 调用方式 OpenHarmony 状态
@capacitor/camera Capacitor await import('@capacitor/camera') ✅ 原生 C++ 工作正常
@ionic-native/in-app-browser Cordova window.InAppBrowser.create() ✅ 原生 C++ 工作正常
@ionic-native/device Cordova window.device ⚠️ 原生 C++ 桥不可用,改用纯 JS 方案

10. 要点总结

要点 说明
双桥架构 hionic 同时支持 Cordova 和 Capacitor 桥,但插件只注册一个桥时另一个不可见
Cordova 模块系统不兼容 @capacitor/core/cordova.jsdefine 函数闭包隔离,同步脚本无法使用
cordova.exec 在 OHOS 不可用 只支持 Android/iOS 的原生消息通道
WebView 加载路径 始终确认 entry/src/main/resources/rawfile/www/
Node.js 版本兼容 用 DevEco 内置 Node.js v18 构建
androidBridge 误判 桥接注册为 androidBridgegetPlatform() 返回 'android',需用 TsCapacitorPlugin 二次判断
最终方案 纯 JS 解析 Capacitor.getPlatform() + TsCapacitorPlugin 检测 + navigator.userAgent 降级

参考资源