鸿蒙端 SDK 创建、单元测试、发布与依赖完整指南

15 阅读9分钟

鸿蒙端 SDK 创建、单元测试、发布与依赖完整指南

本文档介绍从零创建鸿蒙(OpenHarmony/HarmonyOS)SDK、编写单元测试、发布到官方三方库中心仓,并在项目中依赖使用的完整流程。涉及 C 层(Native)开发时,提供基于源码(BUILD.gn)和应用层(CMake)两种方式的完整实现示例。


一、SDK 创建

1.1 项目结构

鸿蒙 SDK 通常以 HAR(Harmony Archive) 形式发布。根据是否包含 C/C++ 原生代码、是否包含页面,结构有所不同。

1.1.1 纯 ArkTS 结构(无 C 层、无页面)
my_sdk/
├── oh-package.json5          # 包配置(必填)
├── build-profile.json5       # 构建配置
├── src/main/
│   ├── module.json5          # 模块配置
│   └── ets/
│       ├── index.ets         # 入口,导出对外 API
│       ├── utils/             # 工具函数
│       └── ...
├── README.md
├── CHANGELOG.md
└── LICENSE
1.1.2 含页面 + C/C++ 的完整结构
my_sdk/
├── oh-package.json5
├── build-profile.json5
├── src/main/
│   ├── module.json5
│   ├── ets/                   # ArkTS 源码
│   │   ├── index.ets         # 入口:import 页面 + export API
│   │   ├── pages/             # 页面(@Entry 路由页)
│   │   │   ├── MainPage.ets   # 主页面
│   │   │   ├── DetailPage.ets
│   │   │   ├── components/    # 页面内可复用组件
│   │   │   │   ├── Toolbar.ets
│   │   │   │   └── BottomPanel.ets
│   │   │   └── utils/         # 页面相关工具
│   │   │       ├── OptionsParser.ets
│   │   │       └── BoundsUtils.ets
│   │   ├── ui/                # 通用 UI(Canvas 绘制、自定义 View)
│   │   │   └── OverlayPainter.ets
│   │   ├── crop/              # 业务核心(或 domain/)
│   │   │   ├── CropOptions.ets
│   │   │   ├── CropTask.ets
│   │   │   └── ResultHandler.ets
│   │   └── utils/             # 通用工具
│   │       └── HttpUtils.ets
│   └── cpp/                   # C/C++ 原生代码(可选)
│       ├── CMakeLists.txt
│       ├── napi_init.cpp
│       └── types/             # NAPI 类型声明
│           └── libentry/
│               ├── oh-package.json5   # name: "libentry.so"
│               └── index.d.ts
├── ohosTest/                  # 单元测试
│   └── ets/test/
├── README.md
├── CHANGELOG.md
└── LICENSE
1.1.3 目录职责说明
目录职责示例
pages/@Entry 的路由页面,负责页面编排与生命周期CropEntryPageMainPage
pages/components/页面内可复用 UI 组件(@ComponentCropToolbarCropBottomPanel
pages/utils/页面相关解析、计算、辅助逻辑CropOptionsParserCropBoundsUtils
ui/通用 UI 绘制、Canvas、自定义 ViewCropOverlayRulerWidget
crop/domain/业务核心、数据模型、任务执行CropOptionsImageCropTask
utils/通用工具(网络、文件、格式等)HttpDownloadUtils
cpp/C/C++ 原生实现,通过 NAPI 暴露给 ArkTSnapi_init.cpp
1.1.4 页面相关调整

新增页面

  1. pages/ 下新建 XxxPage.ets,使用 @Entry({ routeName: 'XxxPage' }) 装饰:
    @Entry({ routeName: 'XxxPage' })
    @Component
    struct XxxPage {
      build() {
        Column() { /* ... */ }
      }
    }
    
  2. index.ets 中增加 import './pages/XxxPage'(若 index.ets 在 src/main/ets/ 下)或 import './src/main/ets/pages/XxxPage'(若 index.ets 在包根),使路由能注册该页面。
  3. 调用方通过 router.pushNamedRoute({ name: 'XxxPage' })router.pushUrl() 跳转。

修改页面布局

  • 页面根节点一般为 ColumnStackRow,按需调整子组件顺序和 layoutWeight
  • 使用 @State 控制 UI 状态,@Prop 在父子组件间传参。
  • 链式写法注意 ArkTS 的 ASI:} 后的 .width() 等需与 } 同一行或确保不被解析为独立语句。

调整页面层级

  • 将可复用部分抽到 pages/components/@Component export struct Xxx,在页面中 Xxx({ ... }) 使用。
  • 将通用绘制逻辑抽到 ui/,如 CropOverlayPainter 负责 Canvas 绘制。

路由与参数传递

  • 使用 AppStorage.setOrCreate('key', value) 存数据,页面通过 @StorageLink('key') 读取。
  • 或使用 router.pushUrl({ url: 'pages/XxxPage', params: { id: 1 } }),在目标页 router.getParams() 获取。url 格式需与 module.json5 中配置的 routes 一致。
1.1.5 C/C++ 结构说明

当 SDK 包含 Native 时,需在 src/main/ 下增加 cpp/

cpp/
├── CMakeLists.txt       # 构建配置
├── napi_init.cpp        # NAPI 注册与实现
└── types/               # 供 ArkTS 调用的类型声明
    └── libentry/        # 目录名随意,oh-package.json5 中 name 填 "libentry.so"
        ├── oh-package.json5   # name: "libentry.so", types: "./index.d.ts"
        └── index.d.ts         # 声明 C 层暴露的接口

主模块 oh-package.json5dependencies 中需添加:

"libentry.so": "file:./src/main/cpp/types/libentry"

build-profile.json5 中配置 externalNativeOptions 指向 CMakeLists.txt。详见第七章「C 层开发」。

1.2 oh-package.json5 配置

{
  "name": "my_sdk",                    // 包名,发布后用于依赖
  "version": "1.0.0",                 // 语义化版本
  "description": "SDK 功能描述",
  "main": "src/main/ets/index.ets",    // 入口文件
  "author": "your_name",
  "license": "Apache-2.0",
  "repository": "https://gitee.com/xxx/my_sdk",
  "dependencies": {}                    // 依赖的其他 ohpm 包
}

必填项nameversionmainlicense

1.3 module.json5 配置

{
  "module": {
    "name": "my_sdk",
    "type": "har",
    "deviceTypes": ["default", "tablet"]
  }
}
  • type: "har" 表示构建为 HAR 静态共享包
  • deviceTypes 指定支持的设备类型

1.4 构建 HAR

在项目根目录执行:

# 安装依赖
ohpm install

# 构建 HAR
hvigorw assembleHar

构建产物位于 build/default/outputs/default/ 目录,生成 .har 文件。


二、单元测试

2.1 测试类型

类型目录运行环境适用场景
Local Testtest/本地 JVM纯逻辑、工具函数、不依赖设备
Instrument TestohosTest/设备/模拟器需系统 API、UI、生命周期

2.2 创建测试目录

在 DevEco Studio 中:

  1. 右键项目 → New → Directory → 输入 ohosTest(Instrument Test)或 test(Local Test)
  2. ohosTesttest 下创建 ets/test/ 子目录

或手动创建:

ohosTest/          # Instrument Test(设备/模拟器)
└── ets/
    └── test/
        └── MySdkTest.ets

test/              # Local Test(本地 JVM,可选)
└── ets/
    └── test/
        └── MyUtilTest.ets

Instrument Test 更常用,HAR 包通常使用 ohosTest

2.3 使用 Hypium 框架

oh-package.json5devDependencies 中添加:

{
  "devDependencies": {
    "@ohos/hypium": "1.0.x"
  }
}

2.4 编写测试用例

// ohosTest/ets/test/MySdkTest.ets
import { describe, it, expect } from '@ohos/hypium';
import { MyUtil } from '../../src/main/ets/MyUtil';  // 路径以实际目录层级为准

export default function test() {
  describe('MyUtil 测试', function () {
    it('add 应返回两数之和', 0, async () => {
      const result = MyUtil.add(1, 2);
      expect(result).assertEqual(3);
    });

    it('formatPath 应正确处理路径', 0, async () => {
      const path = MyUtil.formatPath('/a/b/c');
      expect(path).assertContain('a');
    });
  });
}

2.5 运行测试

  • DevEco Studio:右键测试文件 → Run 'MySdkTest'
  • 命令行
hvigorw test

2.6 常用断言

断言说明
expect(x).assertEqual(y)相等
expect(x).assertTrue()为 true
expect(x).assertContain(y)包含
expect(x).assertNotNull()非空
expect(x).assertDeepEquals(y)深度相等

三、发布到官方库(OpenHarmony 三方库中心仓)

3.1 注册与准备

  1. 打开 OpenHarmony 三方库中心仓
  2. 使用 Gitee 账号登录并完成实名认证
  3. 进入「个人中心」完成发布者信息

3.2 生成密钥

# 创建目录
mkdir -p ~/.ssh_ohpm

# 生成 RSA 密钥对
ssh-keygen -m PEM -t RSA -b 4096 -f ~/.ssh_ohpm/mykey -N ""

~/.ssh_ohpm/mykey.pub 公钥内容上传到中心仓个人中心的「公钥管理」。

3.3 配置 ohpm

# 设置私钥路径
ohpm config set key_path ~/.ssh_ohpm/mykey

# 设置发布 ID(个人中心获取)
ohpm config set publish_id <your_publish_id>

# 设置发布仓库地址
ohpm config set publish_registry https://ohpm.openharmony.cn/ohpm

3.4 准备发布文件

确保项目根目录包含:

文件说明
oh-package.json5包配置
README.md使用说明、API 介绍、示例
CHANGELOG.md版本变更记录
LICENSE开源协议(如 Apache-2.0)

3.5 发布命令

# 进入 HAR 所在目录(或包含 oh-package.json5 的目录)
cd /path/to/my_sdk

# 发布
ohpm publish .

3.6 常见问题

问题处理
Missing file "oh-package.json5"确保在包含 oh-package.json5 的包根目录执行 ohpm publish .
签名失败检查 key_path、公钥是否已上传
版本冲突修改 oh-package.json5 中的 version 后重新发布
Node 版本建议使用 Node 18+,执行 node -v 检查

四、在项目中依赖使用

4.1 配置依赖

在项目根目录的 oh-package.json5dependencies 中声明:

{
  "dependencies": {
    "my_sdk": "1.0.0"
  }
}

依赖版本号支持语义化范围,如 "1.0.x""^1.0.0"

4.2 安装依赖

ohpm install

4.3 导入使用

// 按包名导入
import { MyUtil, MyClass } from 'my_sdk';

// 使用
const result = MyUtil.doSomething();
const obj = new MyClass();

4.4 依赖私有仓或特定源

在项目根目录创建或编辑 oh-package.json5

{
  "dependencies": {
    "my_sdk": "1.0.0"
  },
  "devDependencies": {},
  "registry": "https://ohpm.openharmony.cn/ohpm"
}

或通过环境变量:

export OHPM_REGISTRY=https://ohpm.openharmony.cn/ohpm
ohpm install

五、Flutter 插件中的鸿蒙 SDK 结构示例

image_cropper 为例,其鸿蒙端结构:

image_cropper/ohos/
├── oh-package.json5           # 主包配置
├── build-profile.json5
├── src/
│   ├── main/
│   │   ├── module.json5
│   │   └── ets/
│   │       └── components/
│   │           └── plugin/
│   │               └── ImageCropperPlugin.ets
│   └── lib/
│       └── image_crop_ohos/    # 子库(HAR)
│           ├── oh-package.json5
│           ├── build-profile.json5
│           ├── index.ets       # 导出入口
│           └── src/main/ets/
│               ├── crop/
│               ├── pages/
│               └── ui/
└── ...

主包通过 dependencies: { "image_crop_ohos": "file:./src/lib/image_crop_ohos" } 引用本地子库。若将 image_crop_ohos 单独发布到中心仓,其他项目可直接依赖:

"dependencies": {
  "image_crop_ohos": "1.0.0"
}

六、流程总览

┌─────────────────┐
│ 1. 创建 SDK 项目  │
│ oh-package.json5 │
│ module.json5     │
└────────┬────────┘
         │
         ├──────────────────────────────────┐
         │ 若需 C 层能力                      │
         ▼                                  │
┌─────────────────┐                         │
│ 1.5 添加 Native  │  CMake / BUILD.gn       │
│ NAPI 模块        │  → libxxx.so            │
└────────┬────────┘                         │
         │                                  │
         ▼                                  ▼
┌─────────────────┐
│ 2. 编写单元测试   │
│ ohosTest/       │
│ @ohos/hypium    │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ 3. 构建 HAR     │
│ hvigorw         │
│ assembleHar     │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ 4. 发布到中心仓  │
│ ohpm publish    │
│ 密钥 + 文档      │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ 5. 项目依赖使用  │
│ ohpm install    │
│ import 'my_sdk' │
└─────────────────┘

七、C 层(Native)开发

当 SDK 需要调用 C/C++ 代码(如高性能计算、移植三方库、系统底层能力)时,需使用 NAPI(Native API) 作为 ArkTS 与 C/C++ 的桥梁,相当于 Android 的 JNI。

7.1 两种开发方式对比

方式适用场景构建系统是否需要 OpenHarmony 源码
方式一:源码 + BUILD.gn系统级、预装应用、设备厂商BUILD.gn✅ 需要
方式二:应用层 + CMake普通应用、HAR 包、发布到中心仓CMake❌ 不需要

7.2 方式一:基于 OpenHarmony 源码(BUILD.gn)

适用于有完整 OpenHarmony 源码、需将 NAPI 编译进系统镜像的场景。

7.2.1 目录结构
mysubsys/                    # 子系统
├── ohos.build
└── hello/                   # 组件
    └── hellonapi/          # 模块
        ├── BUILD.gn
        └── hellonapi.cpp
7.2.2 添加子系统 ohos.build

mysubsys/ohos.build

{
  "subsystem": "mysubsys",
  "parts": {
    "hello": {
      "module_list": [
        "//mysubsys/hello/hellonapi:hellonapi"
      ],
      "inner_kits": [],
      "system_kits": [],
      "test_list": []
    }
  }
}
7.2.3 注册到 build/subsystem_config.json
"mysubsys": {
  "project": "hmf/mysubsys",
  "path": "mysubsys",
  "name": "mysubsys",
  "dir": ""
}
7.2.4 C++ 源码实现 hellonapi.cpp
#include <string>
#include "napi/native_api.h"
#include "napi/native_node_api.h"

// 1. 业务接口实现
static napi_value getHelloString(napi_env env, napi_callback_info info) {
  napi_value result;
  std::string words = "Hello OpenHarmony NAPI";
  NAPI_CALL(env, napi_create_string_utf8(env, words.c_str(), words.length(), &result));
  return result;
}

// 2. 注册对外接口
static napi_value registerFunc(napi_env env, napi_value exports) {
  static napi_property_descriptor desc[] = {
    DECLARE_NAPI_FUNCTION("getHelloString", getHelloString),
  };
  NAPI_CALL(env, napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc));
  return exports;
}

// 3. 定义并注册 NAPI 模块
static napi_module hellonapiModule = {
  .nm_version = 1,
  .nm_flags = 0,
  .nm_filename = nullptr,
  .nm_register_func = registerFunc,
  .nm_modname = "hellonapi",
  .nm_priv = nullptr,
  .reserved = { 0 },
};

extern "C" __attribute__((constructor)) void hellonapiModuleRegister() {
  napi_module_register(&hellonapiModule);
}
7.2.5 BUILD.gn 构建脚本
import("//build/ohos.gni")

ohos_shared_library("hellonapi") {
  include_dirs = [
    "//foundation/arkui/napi/interfaces/kits",
    "//foundation/arkui/napi/interfaces/inner_api",
  ]
  cflags_cc = [ "-Wno-error", "-Wno-unused-function" ]
  sources = [ "hellonapi.cpp" ]
  deps = [ "//foundation/arkui/napi:ace_napi" ]
  relative_install_dir = "module"
  subsystem_name = "mysubsys"
  part_name = "hello"
}
7.2.6 编译与烧录
# 增量编译
./build.sh --product-name rk3568 --ccache --build-target=hellonapi --target-cpu arm64

# 全量编译后烧录镜像
7.2.7 ETS 调用
import hellonapi from '@ohos.hellonapi';

let str = hellonapi.getHelloString();

需在 SDK 目录下提供 @ohos.hellonapi.d.ts 声明:

declare namespace hellonapi {
  function getHelloString(): string;
}
export default hellonapi;

7.3 方式二:基于 DevEco Studio + CMake(应用层)

适用于普通应用开发者,无需 OpenHarmony 源码,在 DevEco Studio 中创建 Native C++ 模块。

7.3.1 项目结构
entry/
├── src/
│   └── main/
│       ├── cpp/                    # C++ 源码
│       │   ├── CMakeLists.txt
│       │   ├── napi_init.cpp
│       │   └── types/              # 类型声明(可选)
│       │       └── libentry/
│       │           ├── oh-package.json5
│       │           └── index.d.ts
│       └── ets/
│           └── ...
├── build-profile.json5
└── oh-package.json5
7.3.2 CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
project(entry)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
include_directories(${NATIVERENDER_ROOT_PATH})

# 生成 libentry.so
add_library(entry SHARED napi_init.cpp)

# 链接 NAPI 库
target_link_libraries(entry PUBLIC libace_napi.z.so)
7.3.3 napi_init.cpp 实现
#include "napi/native_api.h"
#include "napi/native_node_api.h"

static napi_value add(napi_env env, napi_callback_info info) {
  size_t argc = 2;
  napi_value args[2];
  napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

  int32_t a, b;
  napi_get_value_int32(env, args[0], &a);
  napi_get_value_int32(env, args[1], &b);

  napi_value result;
  napi_create_int32(env, a + b, &result);
  return result;
}

static napi_value registerFunc(napi_env env, napi_value exports) {
  napi_property_descriptor desc[] = {
    { "add", nullptr, add, nullptr, nullptr, nullptr, napi_default, nullptr }
  };
  napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
  return exports;
}

static napi_module entryModule = {
  .nm_version = 1,
  .nm_flags = 0,
  .nm_filename = nullptr,
  .nm_register_func = registerFunc,
  .nm_modname = "libentry",
  .nm_priv = nullptr,
  .reserved = { 0 },
};

extern "C" __attribute__((constructor)) void entryModuleRegister() {
  napi_module_register(&entryModule);
}
7.3.4 build-profile.json5 配置

在模块的 build-profile.json5 中配置 Native 编译:

{
  "apiType": "stageMode",
  "buildOption": {
    "externalNativeOptions": {
      "path": "./src/main/cpp/CMakeLists.txt",
      "arguments": "-DOHOS_STL=c++_shared",
      "abiFilters": ["armeabi-v7a", "arm64-v8a"]
    }
  }
}
7.3.5 类型声明与 SO 关联

src/main/cpp/types/libentry/ 下创建:

oh-package.json5

{
  "name": "libentry.so",
  "types": "./index.d.ts",
  "version": "1.0.0",
  "description": "Native NAPI module"
}

index.d.ts

export const add: (a: number, b: number) => number;
7.3.6 主模块 oh-package.json5 引用

entry/oh-package.json5dependencies 中添加:

{
  "dependencies": {
    "libentry.so": "file:./src/main/cpp/types/libentry"
  }
}
7.3.7 ETS 调用
import libentry from 'libentry.so';

let sum = libentry.add(1, 2);  // 3

7.4 调用已有三方 SO

若已有预编译的 .so 文件,无需编写 C++ 源码,只需:

  1. .so 放入 src/main/libs/<abi>/ 目录(如 src/main/libs/arm64-v8a/libmylib.so
  2. 创建类型声明包,在 oh-package.json5name 填 SO 名(如 libmylib.so),types 指向 index.d.ts
  3. 主模块 oh-package.json5dependencies 中添加:"libmylib.so": "file:./src/main/cpp/types/libmylib"
  4. build-profile.json5externalNativeOptions 中配置 abiFilters,或通过 CMake 的 add_library(IMPORTED) 引入预编译 so(具体以 DevEco 文档为准)

目录示例

src/main/
├── libs/
│   ├── arm64-v8a/
│   │   └── libmylib.so
│   └── armeabi-v7a/
│       └── libmylib.so
└── cpp/types/libmylib/
    ├── oh-package.json5
    └── index.d.ts

types/libmylib/oh-package.json5

{
  "name": "libmylib.so",
  "types": "./index.d.ts",
  "version": "1.0.0"
}

index.d.ts

export const nativeMethod: (param: string) => number;

主模块 oh-package.json5

{
  "dependencies": {
    "libmylib.so": "file:./src/main/cpp/types/libmylib"
  }
}

7.5 NAPI 常用类型转换

ETS 类型C/C++ 获取C/C++ 返回
numbernapi_get_value_int32 / napi_get_value_doublenapi_create_int32 / napi_create_double
stringnapi_get_value_string_utf8napi_create_string_utf8
booleannapi_get_value_boolnapi_get_boolean(创建 true/false 的 napi_value)
ArrayBuffernapi_get_arraybuffer_infonapi_create_arraybuffer
对象napi_get_propertynapi_create_object

7.6 C 层流程总览

┌─────────────────────────────────────────────────────────────┐
│ C++ 实现 (napi_init.cpp)                                      │
│  - 实现业务逻辑 napi_value xxx(napi_env, napi_callback_info) │
│  - registerFunc 注册接口                                     │
│  - napi_module_register 注册模块                             │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│ 构建 (CMake / BUILD.gn)                                      │
│  - add_library(entry SHARED ...)                              │
│  - target_link_libraries(entry libace_napi.z.so)             │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│ 产物 libentry.so + index.d.ts                                │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│ ETS: import libentry from 'libentry.so'                       │
│      libentry.add(1, 2)                                       │
└─────────────────────────────────────────────────────────────┘

八、参考链接