开源鸿蒙 Cordova 设备信息插件开发详解

目录

  1. 项目背景与概述
  2. 技术架构设计
  3. 开发环境准备
  4. 插件配置文件详解
  5. JavaScript API 层实现
  6. C++ 桥接层实现
  7. ArkTS 原生层实现
  8. 数据流转过程
  9. 关键技术要点
  10. 开发流程总结

项目背景与概述

什么是 Cordova 插件?

Apache Cordova 是一个开源的移动应用开发框架,允许开发者使用 HTML、CSS 和 JavaScript 构建跨平台移动应用。Cordova 通过插件机制提供了访问设备原生功能的能力,使 Web 应用能够调用系统 API。

为什么需要设备信息插件?

在移动应用开发中,获取设备信息是一个常见需求,包括:

  • 设备型号:用于适配不同屏幕尺寸和硬件特性
  • 操作系统版本:用于判断系统功能可用性
  • 设备唯一标识:用于用户识别和数据分析
  • 制造商信息:用于品牌特定的功能适配
  • 虚拟设备检测:用于区分真机和模拟器

cordova-plugin-device 正是为了满足这些需求而开发的核心插件。

HarmonyOS 平台的挑战

HarmonyOS 作为华为推出的分布式操作系统,其架构与 Android/iOS 存在显著差异:

  • 使用 ArkTS(基于 TypeScript)作为主要开发语言
  • 采用全新的 API 体系(如 @kit.BasicServicesKit
  • 需要适配 OpenHarmony 的底层实现

因此,需要为 HarmonyOS 平台重新实现设备信息插件,而不能直接复用 Android/iOS 的代码。


技术架构设计

三层架构模型

本插件采用了经典的三层架构设计:

┌─────────────────────────────────────┐
│   JavaScript API 层 (device.js)    │  ← Web 应用调用接口
├─────────────────────────────────────┤
│   C++ 桥接层 (Device.cpp/h)         │  ← 桥接 JavaScript 和原生代码
├─────────────────────────────────────┤
│   ArkTS 原生层 (GetDeviceInfo.ets)  │  ← 调用 HarmonyOS 系统 API
└─────────────────────────────────────┘

各层职责说明

  1. JavaScript API 层

    • 提供统一的 device 全局对象
    • 处理 Cordova 生命周期事件
    • 封装异步调用逻辑
  2. C++ 桥接层

    • 实现 Cordova 插件接口规范
    • 处理 JSON 数据序列化/反序列化
    • 管理回调上下文和异步通信
  3. ArkTS 原生层

    • 调用 HarmonyOS 系统 API 获取设备信息
    • 处理数据持久化(如字体缩放设置)
    • 返回结构化数据给 C++ 层

开发环境准备

必需工具

  1. HCordova CLI

    npm install -g hcordova
  2. HarmonyOS 开发工具

    • DevEco Studio(HarmonyOS IDE)
    • HarmonyOS SDK
  3. 依赖要求

    • cordova-openharmony >= 2.0.0
    • hcordova >= 1.0.0

项目结构

cordova-plugin-device/
├── plugin.xml              # Cordova 插件配置文件
├── package.json            # NPM 包配置
├── www/
│   └── device.js          # JavaScript API 实现
└── src/
    └── main/
        ├── cpp/
        │   └── Device/
        │       ├── Device.h      # C++ 头文件
        │       └── Device.cpp    # C++ 实现文件
        └── ets/
            └── components/
                └── PluginAction/
                    └── GetDeviceInfo.ets  # ArkTS 原生实现

插件配置文件详解

plugin.xml 核心配置

plugin.xml 是 Cordova 插件的核心配置文件,定义了插件的元数据和平台特定配置:

<?xml version="1.0" encoding="UTF-8"?>
    <plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
    id="cordova-plugin-device"
    version="1.0.0">
<name>device</name>
<description>Cordova device Plugin</description>
<license>Apache 2.0</license>
  <engines>
      <engine name="cordova-openharmony" version=">=2.0.0" />
    </engines>
      <platform name="ohos">
      <!-- 1. 注册插件功能 -->
          <config-file target="src/main/resources/rawfile/config.xml"
          parent="/*"
          modules-targets-name="default">
          <feature name="Device">
          <param name="harmony-package" value="Device" />
        </feature>
      </config-file>
      <!-- 2. 配置 CMake 构建 -->
          <CMakeLists target="src/main/cpp/CMakeLists.txt"
          modules-name="cordova">
        <param target="add_library" value="Device/Device.cpp"/>
      </CMakeLists>
      <!-- 3. 注册 C++ 源文件 -->
          <source-file type="h"
          src="src/main/cpp/Device/Device.h"
          target-dir="src/main/cpp/Device"
          modules-name="cordova"/>
          <source-file type="cpp"
          src="src/main/cpp/Device/Device.cpp"
          target-dir="src/main/cpp/Device"
          modules-name="cordova"/>
        <!-- 4. 注册 ArkTS 源文件 -->
            <source-file type="ets"
            src="src/main/ets/components/PluginAction/GetDeviceInfo.ets"
            target-dir="src/main/ets/components/PluginAction"
            modules-name="cordova"
            runtimeOnly="true"/>
          <!-- 5. 注册 JavaScript 模块 -->
              <js-module src="www/device.js"
              name="device"
              modules-targets-name="default">
            <clobbers target="device" />
          </js-module>
        </platform>
      </plugin>

配置项解析

  1. <config-file>:在应用配置文件中注册插件功能,使 Cordova 框架能够识别并加载插件
  2. <CMakeLists>:配置 C++ 代码的编译规则,将 Device.cpp 添加到构建系统
  3. <source-file>:声明需要复制到目标项目的源文件
  4. <js-module>:注册 JavaScript 模块,clobbers 属性表示将模块导出为全局 device 对象

JavaScript API 层实现

核心代码分析

让我们深入分析 www/device.js 的实现:

var argscheck = require('cordova/argscheck');
var channel = require('cordova/channel');
var utils = require('cordova/utils');
var exec = require('cordova/exec');
var cordova = require('cordova');
// 创建并等待设备信息就绪事件
channel.createSticky('onCordovaInfoReady');
channel.waitForInitialization('onCordovaInfoReady');

Device 构造函数

function Device () {
// 初始化所有设备属性
this.available = false;
this.platform = null;
this.version = null;
this.uuid = null;
this.cordova = null;
this.model = null;
this.manufacturer = null;
this.isVirtual = null;
this.serial = null;
var me = this;
// 监听 Cordova 就绪事件
channel.onCordovaReady.subscribe(function () {
me.getInfo(function (info) {
// 填充设备信息
me.available = true;
me.platform = info.platform;
me.version = info.version;
me.uuid = info.uuid;
me.cordova = cordova.version;  // 使用 Cordova 框架版本
me.model = info.model;
me.isVirtual = info.isVirtual;
me.manufacturer = info.manufacturer || 'unknown';
me.serial = info.serial || 'unknown';
// 触发设备信息就绪事件
channel.onCordovaInfoReady.fire();
}, function (e) {
me.available = false;
utils.alert('[ERROR] Error initializing Cordova: ' + e);
});
});
}

关键设计模式

  1. 事件驱动架构

    • 使用 Cordova 的 channel 机制管理异步初始化
    • createSticky 创建持久化事件,确保后续订阅者也能收到事件
    • waitForInitialization 确保依赖项在初始化完成前不会执行
  2. 异步调用封装

    Device.prototype.getInfo = function (successCallback, errorCallback) {
    argscheck.checkArgs('fF', 'Device.getInfo', arguments);
    exec(successCallback, errorCallback, 'Device', 'getDeviceInfo', []);
    };
    • exec 是 Cordova 提供的桥接函数,用于调用原生代码
    • 参数:(成功回调, 失败回调, 插件类名, 方法名, 参数数组)
  3. 单例模式

    module.exports = new Device();
    • 导出单例实例,确保全局只有一个 device 对象

C++ 桥接层实现

类结构设计

Device.h 定义了插件类的接口:

class Device : public CordovaPlugin{
// 设备信息成员变量
string m_strUuid;
string m_strVersion;
string m_strPlatform;
string m_strModel;
string m_strManufacturer;
string m_strSerial;
string m_strSdkVersion;
// 回调上下文
CallbackContext m_cbc;
CallbackContext m_cbc2;
public:
Device(){
m_strPlatform = "HarmonyOS";
m_strManufacturer = "Huawei";
}
// 核心方法
bool execute(const string& action, cJSON* args, CallbackContext cbc) override;
void initialize(CallbackContext cbc);
bool onArKTsResult(cJSON* args);
void sendResult();
};

插件注册机制

#include "Device.h"
REGISTER_PLUGIN_CLASS(Device)

REGISTER_PLUGIN_CLASS 是一个宏,用于将插件类注册到 Cordova 插件系统中,使得 JavaScript 层可以通过类名找到对应的 C++ 实现。

execute 方法 - 命令分发中心

bool Device::execute(const string& action, cJSON* args, CallbackContext cbc)
{
if(action == "getDeviceInfo") {
m_cbc = cbc;
if(m_strModel == "") {
// 首次调用,需要初始化
initialize(cbc);
return true;
}
// 已初始化,直接返回缓存结果
sendResult();
}
if(action == "onArKTsResult") {
// 处理 ArkTS 层的回调结果
return onArKTsResult(args);
}
// ... 其他功能(字体缩放等)
return true;
}

设计亮点

  • 命令模式:通过 action 字符串分发不同的操作
  • 缓存机制:首次调用后缓存设备信息,后续调用直接返回,提高性能
  • 异步处理:保存回调上下文,等待 ArkTS 层返回结果后再调用

initialize 方法 - 初始化流程

void Device::initialize(CallbackContext cbc)
{
executeArkTs("./PluginAction/GetDeviceInfo/GetDeviceInfo", 0, "", "Device", cbc);
return;
}

executeArkTs 是框架提供的函数,用于调用 ArkTS 层的代码:

  • 第一个参数:ArkTS 模块路径和函数名
  • 第二个参数:参数数量
  • 第三个参数:参数字符串
  • 第四个参数:插件类名(用于回调识别)
  • 第五个参数:回调上下文

onArKTsResult 方法 - 处理 ArkTS 回调

bool Device::onArKTsResult(cJSON* args)
{
string content = cJSON_GetObjectItem(args, "content")->valuestring;
cJSON* json = cJSON_GetObjectItem(args, "result");
if(json != NULL && json->type == cJSON_Array) {
int count = cJSON_GetArraySize(json);
for(int i=0; i<count; i++) {
switch(i) {
case 0: m_strUuid = cJSON_GetArrayItem(json,i)->valuestring; break;
case 1: m_strVersion = cJSON_GetArrayItem(json,i)->valuestring; break;
case 2: m_strPlatform = cJSON_GetArrayItem(json,i)->valuestring; break;
case 3: m_strSdkVersion = cJSON_GetArrayItem(json,i)->valuestring; break;
case 4: m_strSerial = cJSON_GetArrayItem(json,i)->valuestring; break;
case 5: m_strModel = cJSON_GetArrayItem(json,i)->valuestring; break;
}
}
}
// 如果有待处理的回调,发送结果
if(m_cbc.getQueue() != NULL) {
sendResult();
}
return true;
}

关键点

  • 使用 cJSON 库解析 JSON 数据
  • 按数组索引顺序解析设备信息
  • 检查回调队列,如果有等待的回调则立即返回结果

sendResult 方法 - 构建并返回结果

void Device::sendResult()
{
cJSON* json = cJSON_CreateObject();
cJSON_AddStringToObject(json, "version", m_strVersion.c_str());
cJSON_AddStringToObject(json, "platform", m_strPlatform.c_str());
cJSON_AddStringToObject(json, "model", m_strModel.c_str());
cJSON_AddStringToObject(json, "manufacturer", m_strManufacturer.c_str());
// 特殊处理:模拟器检测
if(m_strModel == "emulator") {
cJSON_AddStringToObject(json, "uuid", "emulator123456");
cJSON_AddTrueToObject(json, "isVirtual");
cJSON_AddStringToObject(json, "serial", "emulator123456");
} else {
cJSON_AddStringToObject(json, "uuid", m_strUuid.c_str());
cJSON_AddFalseToObject(json, "isVirtual");
cJSON_AddStringToObject(json, "serial", m_strSerial.c_str());
}
cJSON_AddStringToObject(json, "sdkVersion", m_strSdkVersion.c_str());
m_cbc.success(json);  // 调用成功回调
cJSON_Delete(json);   // 释放内存
}

重要细节

  • 模拟器检测:通过检查 model 是否为 “emulator” 来判断是否为虚拟设备
  • 内存管理:使用 cJSON_Delete 释放 JSON 对象,避免内存泄漏
  • 回调机制:通过 CallbackContext.success() 将结果返回给 JavaScript 层

ArkTS 原生层实现

导入依赖

import { ArkTsAttribute, cordovaWebTagToObjectGlobe, NativeAttribute } from "../PluginGlobal";
import { deviceInfo } from "@kit.BasicServicesKit";
import cordova from 'libcordova.so';
import { common } from "@kit.AbilityKit";
import { preferences } from "@kit.ArkData";
import { MainPage } from "../MainPage";

关键依赖说明

  • @kit.BasicServicesKit:HarmonyOS 基础服务套件,提供设备信息 API
  • libcordova.so:Cordova 原生库,提供与 C++ 层的通信接口
  • @kit.ArkData:数据持久化套件,用于存储用户设置

GetDeviceInfo 函数 - 核心实现

export function GetDeviceInfo(pageIndex:NativeAttribute):void {
let deviceArray:Array<string> = new Array();
  // 按顺序收集设备信息
  deviceArray.push(deviceInfo.ODID);           // [0] UUID/序列号
  deviceArray.push(deviceInfo.versionId);       // [1] 版本 ID
  deviceArray.push(deviceInfo.osFullName);      // [2] 操作系统全名
  deviceArray.push(deviceInfo.sdkApiVersion+""); // [3] SDK 版本
  deviceArray.push(deviceInfo.ODID);           // [4] 序列号(复用 ODID)
  deviceArray.push(deviceInfo.productModel);    // [5] 产品型号
  // 构建结果对象
  let result: ArkTsAttribute = {
  content:"",
  result:deviceArray
  };
  // 通过 Cordova 桥接返回结果给 C++ 层
  cordova.onArkTsResult(
  JSON.stringify(result),
  pageIndex.pageObject,
  pageIndex.pageWebTag
  );
  }

HarmonyOS API 详解

  1. deviceInfo.ODID

    • ODID(Open Device Identifier)是 HarmonyOS 提供的开发者匿名设备标识符
    • 用于替代传统的设备 UUID,保护用户隐私
    • 普通应用无法获取真实的设备 UUID,因此使用 ODID
  2. deviceInfo.versionId

    • 完整的版本标识符,格式为:
      deviceType/manufacture/brand/productSeries/osFullName/productModel/softwareModel/sdkApiVersion/incrementalVersion/buildType
    • 示例:wearable/HUAWEI/HUAWEI/TAS/OpenHarmony-5.0.0.1/TAS-AL00/TAS-AL00/12/default/release:nolog
  3. deviceInfo.osFullName

    • 操作系统全名,格式:OpenHarmony-x.x.x.x
    • 用于标识系统版本
  4. deviceInfo.productModel

    • 产品型号,如:TAS-AL00
    • 用于设备识别和适配

数据流转机制

cordova.onArkTsResult(JSON.stringify(result), pageIndex.pageObject, pageIndex.pageWebTag);

这个调用会:

  1. 将结果序列化为 JSON 字符串
  2. 通过 JNI/FFI 桥接传递给 C++ 层
  3. C++ 层的 onArKTsResult 方法被调用
  4. 解析 JSON 并更新成员变量
  5. 通过回调上下文返回给 JavaScript 层

额外功能:字体缩放

插件还实现了字体缩放功能,展示了如何处理用户设置:

// 设置字体大小
export function SetScaleFont(pageIndex:NativeAttribute) {
// 获取页面对象
let mainPage = cordovaWebTagToObjectGlobe.get(pageIndex.pageWebTag) as MainPage;
let fontScale:number = Number(pageIndex.pageArgs);
// 应用字体缩放
mainPage.textZoomRatio = 100 * fontScale;
// 持久化存储
const context: common.UIAbilityContext = getContext() as common.UIAbilityContext;
let options: preferences.Options = { name: 'cordovaStore' };
let dataPreferences: preferences.Preferences =
preferences.getPreferencesSync(context, options);
dataPreferences.putSync('scaleFont', pageIndex.pageArgs);
dataPreferences.flush();
// 返回结果
let result: ArkTsAttribute = {content:"setScaleFont", result:[]};
cordova.onArkTsResult(JSON.stringify(result), pageIndex.pageObject, pageIndex.pageWebTag);
}
// 获取字体大小
export function GetScaleFont(pageIndex:NativeAttribute) {
const context: common.UIAbilityContext = getContext() as common.UIAbilityContext;
let options: preferences.Options = { name: 'cordovaStore'};
let dataPreferences: preferences.Preferences =
preferences.getPreferencesSync(context, options);
let fontScale:string = dataPreferences.getSync('scaleFont', '1').toString();
let result: ArkTsAttribute = {content:"getScaleFont", result:[fontScale]};
cordova.onArkTsResult(JSON.stringify(result), pageIndex.pageObject, pageIndex.pageWebTag);
}

技术要点

  • 使用 preferences API 进行数据持久化
  • 通过 textZoomRatio 属性控制 WebView 的字体缩放
  • 使用 cordovaWebTagToObjectGlobe 全局映射表管理页面对象

数据流转过程

完整调用链

让我们追踪一次完整的 device.getInfo() 调用:

1. JavaScript 层
   ↓
   document.addEventListener("deviceready", ...)
   ↓
   device.getInfo(successCallback, errorCallback)
   ↓
   exec('Device', 'getDeviceInfo', [])
2. C++ 桥接层
   ↓
   Device::execute("getDeviceInfo", args, cbc)
   ↓
   Device::initialize(cbc)
   ↓
   executeArkTs("./PluginAction/GetDeviceInfo/GetDeviceInfo", ...)
3. ArkTS 原生层
   ↓
   GetDeviceInfo(pageIndex)
   ↓
   deviceInfo.ODID, deviceInfo.versionId, ...
   ↓
   cordova.onArkTsResult(JSON.stringify(result), ...)
4. 回调到 C++ 层
   ↓
   Device::onArKTsResult(args)
   ↓
   解析 JSON,更新成员变量
   ↓
   Device::sendResult()
   ↓
   m_cbc.success(json)
5. 返回到 JavaScript 层
   ↓
   successCallback(info)
   ↓
   更新 device 对象的属性
   ↓
   channel.onCordovaInfoReady.fire()

时序图

JavaScript          C++ Bridge          ArkTS Native
    |                    |                    |
    |--getDeviceInfo()-->|                    |
    |                    |--executeArkTs()--->|
    |                    |                    |--GetDeviceInfo()
    |                    |                    |--deviceInfo.ODID
    |                    |                    |--deviceInfo.versionId
    |                    |<--onArkTsResult()--|
    |                    |--parse JSON-------->|
    |                    |--sendResult()------>|
    |<--success(info)----|                    |
    |--update properties-|                    |
    |--fire event--------|                    |

关键技术要点

1. 异步通信机制

问题:JavaScript 是单线程异步模型,而原生代码调用是同步的,如何协调?

解决方案

  • 使用回调函数处理异步结果
  • C++ 层保存 CallbackContext,等待 ArkTS 层返回后再调用
  • JavaScript 层使用 Promise-like 的回调模式

2. 数据序列化

问题:不同语言层之间如何传递复杂数据结构?

解决方案

  • 统一使用 JSON 格式进行数据交换
  • C++ 层使用 cJSON 库解析和构建 JSON
  • ArkTS 层使用 JSON.stringify()JSON.parse()

3. 内存管理

问题:C++ 需要手动管理内存,如何避免泄漏?

解决方案

  • 使用 cJSON_Delete() 释放 JSON 对象
  • 字符串使用 std::string 自动管理内存
  • 回调上下文由框架管理生命周期

4. 插件注册机制

问题:如何让 Cordova 框架找到并加载插件?

解决方案

  • 使用 REGISTER_PLUGIN_CLASS 宏注册插件类
  • plugin.xml 中声明插件功能
  • 通过类名字符串匹配(“Device”)找到对应实现

5. 模拟器检测

问题:如何区分真机和模拟器?

解决方案

if(m_strModel == "emulator") {
cJSON_AddStringToObject(json, "uuid", "emulator123456");
cJSON_AddTrueToObject(json, "isVirtual");
cJSON_AddStringToObject(json, "serial", "emulator123456");
}

HarmonyOS 模拟器的 productModel 返回 “emulator”,通过检测这个值来判断。

6. 隐私保护

问题:如何获取设备标识符而不侵犯用户隐私?

解决方案

  • 使用 HarmonyOS 提供的 ODID(Open Device Identifier)
  • ODID 是匿名标识符,无法追溯到具体用户
  • 符合 GDPR 等隐私法规要求

开发流程总结

步骤 1:项目初始化

# 创建插件目录结构
mkdir cordova-plugin-device
cd cordova-plugin-device
# 初始化 NPM 包
npm init
# 创建目录结构
mkdir -p www
mkdir -p src/main/cpp/Device
mkdir -p src/main/ets/components/PluginAction

步骤 2:编写 plugin.xml

定义插件元数据、平台配置、源文件注册等。

步骤 3:实现 JavaScript API 层

  • 创建 www/device.js
  • 实现 Device 构造函数
  • 实现 getInfo 方法
  • 处理 Cordova 生命周期事件

步骤 4:实现 C++ 桥接层

  • 创建 Device.h 定义类接口
  • 创建 Device.cpp 实现核心逻辑
  • 实现 execute 方法处理命令分发
  • 实现 onArKTsResult 处理回调
  • 实现 sendResult 构建返回数据

步骤 5:实现 ArkTS 原生层

  • 创建 GetDeviceInfo.ets
  • 导入 HarmonyOS API
  • 实现 GetDeviceInfo 函数
  • 调用 deviceInfo API 获取信息
  • 通过 cordova.onArkTsResult 返回结果

步骤 6:测试与调试

// 在测试应用中
document.addEventListener("deviceready", function() {
console.log("设备型号:", device.model);
console.log("操作系统:", device.platform);
console.log("设备 UUID:", device.uuid);
console.log("系统版本:", device.version);
console.log("制造商:", device.manufacturer);
console.log("是否虚拟设备:", device.isVirtual);
console.log("序列号:", device.serial);
}, false);

步骤 7:打包与发布

# 安装到项目
hcordova plugin add ./cordova-plugin-device
# 构建应用
hcordova build harmonyos
# 发布到 NPM(可选)
npm publish

常见问题与解决方案

Q1: 为什么 uuidserial 返回相同的值?

A: 在 HarmonyOS 中,普通应用无法获取真实的设备 UUID 和序列号,出于隐私保护考虑,两者都返回 ODID。在模拟器中,两者都返回固定的 "emulator123456"

Q2: 如何判断设备是否为模拟器?

A: 检查 device.isVirtual 属性,或检查 device.model 是否为 "emulator"

Q3: 插件初始化失败怎么办?

A:

  1. 确保在 deviceready 事件之后调用
  2. 检查 plugin.xml 配置是否正确
  3. 查看 C++ 和 ArkTS 层的日志输出
  4. 确认 HarmonyOS API 权限已配置

Q4: 如何扩展插件功能?

A:

  1. Device.cppexecute 方法中添加新的 action 分支
  2. 在 ArkTS 层实现对应的函数
  3. 在 JavaScript 层添加新的 API 方法
  4. 更新 plugin.xml 注册新的源文件(如需要)

总结

本文详细介绍了 HarmonyOS Cordova 设备信息插件的开发过程,涵盖了:

  1. 架构设计:三层架构模型,职责清晰
  2. 技术实现:JavaScript、C++、ArkTS 三层协同工作
  3. 数据流转:完整的异步调用链和回调机制
  4. 关键技术:异步通信、数据序列化、内存管理等
  5. 开发流程:从初始化到发布的完整步骤

这个插件展示了如何在 HarmonyOS 平台上开发 Cordova 插件的标准模式,可以作为其他插件开发的参考模板。通过理解这个插件的实现,开发者可以:

  • 掌握 Cordova 插件开发的基本流程
  • 理解跨语言调用的桥接机制
  • 学习 HarmonyOS API 的使用方法
  • 了解异步编程和事件驱动的设计模式

希望本文能够帮助开发者更好地理解和开发 HarmonyOS Cordova 插件!


参考资料

  1. HarmonyOS 设备信息 API 文档
  2. Apache Cordova 插件开发指南
  3. OpenHarmony 应用开发文档
  4. Cordova OpenHarmony 项目