开源鸿蒙 Cordova 设备信息插件开发详解
目录
- 项目背景与概述
- 技术架构设计
- 开发环境准备
- 插件配置文件详解
- [JavaScript API 层实现](#JavaScript API 层实现)
- [C++ 桥接层实现](#C++ 桥接层实现)
- [ArkTS 原生层实现](#ArkTS 原生层实现)
- 数据流转过程
- 关键技术要点
- 开发流程总结
项目背景与概述
什么是 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
└─────────────────────────────────────┘
各层职责说明
-
JavaScript API 层
- 提供统一的
device全局对象 - 处理 Cordova 生命周期事件
- 封装异步调用逻辑
- 提供统一的
-
C++ 桥接层
- 实现 Cordova 插件接口规范
- 处理 JSON 数据序列化/反序列化
- 管理回调上下文和异步通信
-
ArkTS 原生层
- 调用 HarmonyOS 系统 API 获取设备信息
- 处理数据持久化(如字体缩放设置)
- 返回结构化数据给 C++ 层
开发环境准备
必需工具
-
HCordova CLI
bashnpm install -g hcordova -
HarmonyOS 开发工具
- DevEco Studio(HarmonyOS IDE)
- HarmonyOS SDK
-
依赖要求
cordova-openharmony >= 2.0.0hcordova >= 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
<?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>
配置项解析
<config-file>:在应用配置文件中注册插件功能,使 Cordova 框架能够识别并加载插件<CMakeLists>:配置 C++ 代码的编译规则,将 Device.cpp 添加到构建系统<source-file>:声明需要复制到目标项目的源文件<js-module>:注册 JavaScript 模块,clobbers属性表示将模块导出为全局device对象
JavaScript API 层实现
核心代码分析
让我们深入分析 www/device.js 的实现:
javascript
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 构造函数
javascript
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);
});
});
}
关键设计模式
-
事件驱动架构
- 使用 Cordova 的
channel机制管理异步初始化 createSticky创建持久化事件,确保后续订阅者也能收到事件waitForInitialization确保依赖项在初始化完成前不会执行
- 使用 Cordova 的
-
异步调用封装
javascriptDevice.prototype.getInfo = function (successCallback, errorCallback) { argscheck.checkArgs('fF', 'Device.getInfo', arguments); exec(successCallback, errorCallback, 'Device', 'getDeviceInfo', []); };exec是 Cordova 提供的桥接函数,用于调用原生代码- 参数:
(成功回调, 失败回调, 插件类名, 方法名, 参数数组)
-
单例模式
javascriptmodule.exports = new Device();- 导出单例实例,确保全局只有一个
device对象
- 导出单例实例,确保全局只有一个
C++ 桥接层实现
类结构设计
Device.h 定义了插件类的接口:
cpp
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();
};
插件注册机制
cpp
#include "Device.h"
REGISTER_PLUGIN_CLASS(Device)
REGISTER_PLUGIN_CLASS 是一个宏,用于将插件类注册到 Cordova 插件系统中,使得 JavaScript 层可以通过类名找到对应的 C++ 实现。
execute 方法 - 命令分发中心
cpp
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 方法 - 初始化流程
cpp
void Device::initialize(CallbackContext cbc)
{
executeArkTs("./PluginAction/GetDeviceInfo/GetDeviceInfo", 0, "", "Device", cbc);
return;
}
executeArkTs 是框架提供的函数,用于调用 ArkTS 层的代码:
- 第一个参数:ArkTS 模块路径和函数名
- 第二个参数:参数数量
- 第三个参数:参数字符串
- 第四个参数:插件类名(用于回调识别)
- 第五个参数:回调上下文
onArKTsResult 方法 - 处理 ArkTS 回调
cpp
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 方法 - 构建并返回结果
cpp
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 原生层实现
导入依赖
typescript
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 基础服务套件,提供设备信息 APIlibcordova.so:Cordova 原生库,提供与 C++ 层的通信接口@kit.ArkData:数据持久化套件,用于存储用户设置
GetDeviceInfo 函数 - 核心实现
typescript
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 详解
-
deviceInfo.ODID- ODID(Open Device Identifier)是 HarmonyOS 提供的开发者匿名设备标识符
- 用于替代传统的设备 UUID,保护用户隐私
- 普通应用无法获取真实的设备 UUID,因此使用 ODID
-
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
- 完整的版本标识符,格式为:
-
deviceInfo.osFullName- 操作系统全名,格式:
OpenHarmony-x.x.x.x - 用于标识系统版本
- 操作系统全名,格式:
-
deviceInfo.productModel- 产品型号,如:
TAS-AL00 - 用于设备识别和适配
- 产品型号,如:
数据流转机制
typescript
cordova.onArkTsResult(JSON.stringify(result), pageIndex.pageObject, pageIndex.pageWebTag);
这个调用会:
- 将结果序列化为 JSON 字符串
- 通过 JNI/FFI 桥接传递给 C++ 层
- C++ 层的
onArKTsResult方法被调用 - 解析 JSON 并更新成员变量
- 通过回调上下文返回给 JavaScript 层
额外功能:字体缩放
插件还实现了字体缩放功能,展示了如何处理用户设置:
typescript
// 设置字体大小
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);
}
技术要点:
- 使用
preferencesAPI 进行数据持久化 - 通过
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. 模拟器检测
问题:如何区分真机和模拟器?
解决方案:
cpp
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:项目初始化
bash
# 创建插件目录结构
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函数 - 调用
deviceInfoAPI 获取信息 - 通过
cordova.onArkTsResult返回结果
步骤 6:测试与调试
javascript
// 在测试应用中
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:打包与发布
bash
# 安装到项目
hcordova plugin add ./cordova-plugin-device
# 构建应用
hcordova build harmonyos
# 发布到 NPM(可选)
npm publish
常见问题与解决方案
Q1: 为什么 uuid 和 serial 返回相同的值?
A : 在 HarmonyOS 中,普通应用无法获取真实的设备 UUID 和序列号,出于隐私保护考虑,两者都返回 ODID。在模拟器中,两者都返回固定的 "emulator123456"。
Q2: 如何判断设备是否为模拟器?
A : 检查 device.isVirtual 属性,或检查 device.model 是否为 "emulator"。
Q3: 插件初始化失败怎么办?
A:
- 确保在
deviceready事件之后调用 - 检查
plugin.xml配置是否正确 - 查看 C++ 和 ArkTS 层的日志输出
- 确认 HarmonyOS API 权限已配置
Q4: 如何扩展插件功能?
A:
- 在
Device.cpp的execute方法中添加新的 action 分支 - 在 ArkTS 层实现对应的函数
- 在 JavaScript 层添加新的 API 方法
- 更新
plugin.xml注册新的源文件(如需要)
总结
本文详细介绍了 HarmonyOS Cordova 设备信息插件的开发过程,涵盖了:
- 架构设计:三层架构模型,职责清晰
- 技术实现:JavaScript、C++、ArkTS 三层协同工作
- 数据流转:完整的异步调用链和回调机制
- 关键技术:异步通信、数据序列化、内存管理等
- 开发流程:从初始化到发布的完整步骤
这个插件展示了如何在 HarmonyOS 平台上开发 Cordova 插件的标准模式,可以作为其他插件开发的参考模板。通过理解这个插件的实现,开发者可以:
- 掌握 Cordova 插件开发的基本流程
- 理解跨语言调用的桥接机制
- 学习 HarmonyOS API 的使用方法
- 了解异步编程和事件驱动的设计模式
希望本文能够帮助开发者更好地理解和开发 HarmonyOS Cordova 插件!