之前在校内为「福uu」做鸿蒙化相关适配的时候就发现,Expo 生态在 HarmonyOS 上的支持几乎为零,反而是裸的 React Native 库倒是有不少第一方、第三方适配。但以 Expo 在 React Native 社区的风靡程度,理论上应该有这么一个项目去做一套很完善、开箱即用的 Expo 适配,于是就有了 Expo Harmony 这个项目。

GitHub 仓库:github.com/renbaoshuo/expo-harmony
AtomGit 上的镜像:atomgit.com/baoshuo/expo-harmony

这篇文章主要介绍在 Expo Harmony 实现过程中的架构考量,关于如何接入这个项目,请参考 仓库 README 中的相关描述。

核心思想 & 优势

  • 能够使开发者以极少的代码改动,复用现有 Expo 项目的业务代码为应用增加 HarmonyOS 支持。
  • 提供常用 Expo 模块的 HarmonyOS 实现,减少开发者自行编写适配代码的工作量。
  • 提供从开发调试到构建打包的完整工具链,支持环境诊断、原生工程生成、HAP 构建、设备安装、应用启动等多项能力。
  • 支持 Expo CNG 和 Bare 两种接入模式,与官方行为对齐。
    • Expo CNG 通过配置生成 HarmonyOS 原生工程,省去人工维护的繁杂流程,也不需要将原生工程在仓库中手动维护,还支持通过 patch-project 自定义可持久化地修改生成后的原生工程。
    • Bare Installation 允许开发者在已有的 RNOH 原生工程中集成 Expo Harmony。
  • 支持 Autolinking,自动链接 Expo 模块和 RNOH 原生模块,无需逐个注册模块、配置构建依赖。
  • 支持 Expo Modules API,可用 ArkTS 编写原生模块和 View 组件,并提供脚手架为已有 Expo 模块补充 HarmonyOS 支持。

这些目标与 Expo 官方的 Core Concept 其实不谋而合,在编写最少的原生代码下实现整个 React Native 应用。并且其实还多了一条 —— 在改动最少的 JS 代码下实现整个 Expo React Native 应用的 HarmonyOS 适配。

实现思路

有了上面的核心思想作为指导,那么项目的实现思路其实也是比较清晰的了。

对于开发者的迁移成本,我们希望是无限接近于零的,以便迅速地完成全部的迁移流程,同时便于后续维护、升级。

那么就需要先研究一下 Expo SDK 的各个模块是如何实现的了。不难发现,几乎绝大多数涉及原生能力的包,都会在 TypeScript 层暴露 React Native 业务侧需要调用的统一接口,然后其内部再通过 Expo Modules API 中的 requireNativeModule("ModuleName") 获取对应的原生模块,最终分别调用 Swift / Kotlin 实现的 iOS、Android 原生能力。

Expo Harmony 延续了这种设计。我们并没有重新实现一套 Expo 的 JS API,而是尽可能直接复用官方实现,并在 HarmonyOS 侧使用 ArkTS 补齐对应的原生模块。只要模块名称、接口和行为与官方实现保持一致,原有 JS 代码依然可以通过 requireNativeModule("ModuleName") 获取模块,从而把 HarmonyOS 与 iOS、Android 之间的平台差异封装在原生实现内部,达到业务侧无需修改代码即可复用同一套 Expo API 的目标。

不过,仅仅实现这些 ArkTS 模块还不够。Expo 的各个原生模块并不是直接暴露给业务代码的,它们依赖 Expo Modules API 提供模块注册、函数调用、事件、Native View、SharedObject、生命周期等一整套运行时能力。因此,Expo Harmony 还实现了一套面向 HarmonyOS,兼容官方 Expo Modules API 的运行时,基于 RNOH 的 TurboModule 和 JSI 接入 React Native 运行时,并在 JS Runtime 中建立与官方 Expo Modules 兼容的模块环境,使 HarmonyOS 实现的模块最终能够被挂载到统一的 globalThis.expo.modules 中。这样,官方 requireNativeModule 的调用链本身也可以继续复用,而不需要为 HarmonyOS 单独增加一套桥接 API。

在模块开发层面,这套实现也尽量延续了官方 Expo Modules 的组织方式。HarmonyOS 模块同样通过统一的模块定义描述名称、常量、函数、事件和 Native View 等能力,再由 expo-modules-core 负责完成注册和调用分发。这样在移植新的 Expo 模块时,开发者主要关注对应 ArkTS 能力的实现即可,而不需要为每个模块重复处理一套 React Native 桥接逻辑。

与此同时,如果每增加一个 Expo 模块都要求开发者手工修改 HarmonyOS 原生工程、导入 HAR 并注册模块,迁移和维护成本依然会非常高。因此项目还实现了 HarmonyOS 版本的 Expo Modules Autolinking。各个模块只需要在配置中声明自己的 HarmonyOS 原生实现,Autolinking 会自动扫描依赖、收集模块信息、生成注册代码,并处理 HAR、RNOH Package 和相关构建依赖,最终将模块统一交给 expo-modules-core 初始化。于是开发者安装对应的 @expo-harmony/expo-* 包后,通常不需要再逐个处理原生模块的链接和注册。

这样,一个 Expo Module 从发现到注册再到调用的核心链路就被打通了,整个 Expo 体系的 HarmonyOS 适配也就有了最核心的基础。


在这个适配的过程中,还衍生出了下面要介绍的 CLI 工具链和 CNG 的支持。

CLI 工具链

前面解决了 Expo 模块如何在 HarmonyOS 上运行的问题,但如果开发者仍然需要手动启动 Metro、调用 Hvigor 构建、使用 HDC 安装应用,再自行处理设备连接、原生工程生成和模块链接,那么整体的开发体验与 Expo 原有的工作流仍然存在较大差距。

因此,Expo Harmony 实现了 @expo-harmony/cli,将 HarmonyOS 开发过程中涉及的各个工具统一组织起来。依旧地,它尽可能复用 Expo 已有的能力,并补充 HarmonyOS 平台特有的流程。例如 Metro 仍然沿用 Expo 的开发服务,prebuild 也直接建立在官方 Expo Prebuild 之上,只是在执行时增加 HarmonyOS 平台以及对应的原生工程模板。

在此基础上,CLI 再将 Expo、RNOH 与 HarmonyOS 原生工具链串联起来,提供 startprebuildbuildrundoctor 等统一命令。以 run 为例,它会依次处理项目状态检查、原生工程准备、设备或模拟器选择、HAP 构建、Metro 启动与端口映射、应用安装和启动等步骤,从而把原本分散在多个工具中的操作收敛成一条完整的开发链路。

CLI 同时承担了不同工具链之间的协调工作。例如在原生工程不存在时自动执行 Prebuild,在已有 CNG 工程与当前配置不一致时检测差异,在 Bare 工程中直接执行 Autolinking,以及通过 doctor 提前检查 RNOH、HarmonyOS SDK、OHPM、Hvigor、HDC 和 Expo Modules 等依赖是否满足要求。

这样一来,开发者不需要理解每个底层工具的调用关系。对于日常开发来说,整体体验依然保持在 Expo 熟悉的命令行工作流中,只不过在 iOS 和 Android 之外增加了 HarmonyOS 这一目标平台。

Continuous Native Generation

除了开发和构建流程之外,另一个需要解决的问题是 HarmonyOS 原生工程本身如何维护。

如果要求开发者在 harmony/ 目录下面长期维护一份完整的原生工程,那么随着 Expo SDK、RNOH、HarmonyOS SDK 以及各个原生模块不断升级,大量生成代码和构建配置都会逐渐成为维护负担。为了尽可能延续 Expo 原有的开发模式,Expo Harmony 同样支持 Continuous Native Generation(CNG),让 HarmonyOS 原生工程也可以从 Expo 配置中按需生成,而不是作为项目的主要配置来源长期手工维护。

为此,项目扩展了 Expo 的 Config Plugin 机制,实现了 HarmonyOS 对应的配置字段和 Base Mods。开发者仍然通过 app.jsonapp.config.js 以及各个模块提供的 Config Plugin 描述应用需要的原生能力,例如包名、权限、资源、系统配置等,Prebuild 阶段再将这些配置转换为 HarmonyOS 原生工程中的具体文件。

@expo-harmony/prebuild-config 则负责把这一过程组织起来。它以预置的 HarmonyOS 工程模板作为基础,根据最终的 Expo 配置生成 AppScope、Entry Module、module.json5、资源文件、Hvigor 配置、CMake 和 RNOH 相关代码,同时执行前面提到的 Expo Modules Autolinking,将当前项目真正使用到的原生模块一起写入生成结果。

这样,harmony/ 目录本质上就变成了应用配置和依赖关系的一份派生结果。增加权限、安装新的 Expo Module,或者升级底层运行时之后,主要修改的仍然是 Expo 配置和依赖,而不是直接维护大量原生工程文件。在任何时候,重新执行 Prebuild 即可生成与当前状态对应的 HarmonyOS 工程。项目还提供了 --clean 用于完整重新生成,以及 --check 用于检测生成结果与当前配置之间是否出现偏差。

Expo Harmony 中对 CNG 的支持并没有停留在只是提供一个创建 HarmonyOS 工程的脚手架的阶段。它依旧保持了 Expo 中「配置描述需求、Config Plugin 描述原生修改、Prebuild 负责生成工程」的工作模式,这样 HarmonyOS 原生工程也能够随着配置和依赖重新生成,从而进一步降低迁移、升级以及长期维护的成本。

结语

Expo Harmony 脱离开了「给每个 Expo 包增加一份单独的 HarmonyOS 实现」的补丁式思路,围绕 Expo 原有的模块体系补齐了从 JS API、Runtime、Autolinking 到 ArkTS 原生实现的完整链路。对于业务侧而言,HarmonyOS 增加了一套与 Swift、Kotlin 并列的底层实现,原有的 Expo 开发方式和 API 使用方式基本保持不变,开发者无需大规模修改业务代码,也无需长期维护一套与 Expo 体系割裂的原生工程,极大地降低了接入成本。

也感谢 expo-harmony-cli 和 expo-harmony-toolkit 等项目在 Expo 鸿蒙化道路上所做出的探索,他们的实践为后续的工程设计提供了不少参考。Expo Harmony 则在这些探索的基础上,进一步将这种适配融入 Expo 原有的模块体系、开发流程和工程管理方式中,把适配从「能够运行」推进到「能够按照 Expo 原本的方式开发和维护」。

现在,使用 Expo Harmony,开发者仍然可以使用熟悉的 Expo API、Config Plugin、Prebuild 和命令行工作流,在尽可能少的改动业务代码的前提下,完成应用的 HarmonyOS 适配。

仓库地址:https://github.com/renbaoshuo/expo-harmony

如果您觉得这个库有帮助到您,请在 GitHub 页面的右上角给这个仓库点亮一个 Star 🌟,谢谢~

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐