在 HarmonyOS 上使用 expo-sharing

发布时间:2026/9/28 21:16:56
在 HarmonyOS 上使用 expo-sharing
给现有的 React Native 应用增加 HarmonyOS 支持理想情况是业务代码一行不改新增一个平台就像安装一个新依赖。为了验证这条路我写了一个 demo 应用功能围绕 expo-sharing 展开从 react-native CLI 初始化的裸工程开始用 expo-harmony 完成接入最终在 Android、iOS、HarmonyOS 三个平台的模拟器上运行。这篇文章记录完整的接入步骤和过程中遇到的问题。先看最终效果。同一份App.tsx没有任何平台分支在三个模拟器上都跑通了生成文件、调起系统分享面板、面板关闭后更新状态的完整流程。AndroidiOSHarmonyOS同一个界面三个平台各自的渲染效果。Expo Harmony 是什么AtomGit 仓库atomgit.com/baoshuo/expo-harmonyGitHub 仓库github.com/renbaoshuo/expo-harmony欢迎给上面这两个仓库点点 Star ~expo-harmony 的目标一句话可以说清让 Expo 驱动的 React Native 应用运行在 HarmonyOS 上。它的底层是 RNOH也就是 React Native 在 OpenHarmony 上的移植实现。RNOH 解决了渲染层和运行时的问题Expo 生态是另一块空白expo-harmony 补的是这一块。项目目前适配 Expo SDK 55 和 RNOH 0.84.1。对应用开发者来说有四点值得了解。一是模块覆盖。常用 Expo 模块基本都有了 HarmonyOS 实现统一发布在expo-harmony/这个 scope 下expo-file-system、expo-sharing、expo-clipboard、expo-image、expo-sqlite、expo-notifications都在列表里完整清单见仓库 README。二是成对安装。业务代码 import 的始终是官方 JS 包比如expo-sharing。HarmonyOS 原生实现由配套的expo-harmony/expo-sharing提供。两个包一起安装版本相互对应。JS 这一层完全不用感知平台。三是两种工作流。一种是 CNGHarmonyOS 原生工程由 app.json 配置生成不需要手工维护。另一种是 bare在已有原生工程里集成harmony/目录自行维护。demo 用的是 bare 方式因为工程本来就手工维护着android/和ios/再加一个手工维护的harmony/顺理成章。四是自动链接。依赖安装完成后执行一条命令模块的原生注册和构建依赖自动生成不需要为每个模块手写 ArkTS 或 C 的注册代码。Demo 应用工程用社区 CLI 初始化纯 RN 模板没有任何 Expo 的东西。npx--yesreact-native-community/clilatest init ExpoSharingDemo\--version0.83.10--pmnpm版本选择 RN 0.83.10。原因在讲两套 React Native 时会说明这里先记住一点iOS/Android 侧的 RN 版本要跟随 Expo SDK 指定的版本SDK 55 对应 0.83 系列。最初用 0.84.1 初始化Android 构建时 expo-modules-core 的 Kotlin 编译报Promise.kt reject overrides nothing因为 Expo SDK 55 的原生代码是针对 RN 0.83.x 的接口写的改回 0.83.10 后编译通过。demo 的功能是本地笔记分享。点击「生成本地笔记文件」用 expo-file-system 在应用缓存目录写一份文本笔记界面显示文件路径、内容预览和Sharing.isAvailableAsync()的检测结果。点击「用系统面板分享这份笔记」调 expo-sharing 拉起系统分享面板分享这个文件。面板关闭后 Promise 结束界面更新状态。全程不联网。expo-sharing 是 Expo 里负责调起系统分享面板的模块。核心方法shareAsync接收一个文件地址调用后弹出系统分享面板用户选择目标或关闭面板后 Promise 完成。它不返回分享结果用户选了什么目标、有没有真的分享出去应用无从得知。模块还提供isAvailableAsync检测设备的分享能力。在 HarmonyOS 上shareAsync只接受本地文件的file://地址不支持 data URIiOS 和 Android 可以直接传 data URI这是接入过程中遇到的第一个平台差异。因此文件需要先写入本地demo 配合使用了同样有鸿蒙适配包的 expo-file-system。核心逻辑如下。import * as Sharing from expo-sharing; import { File, Paths } from expo-file-system; const NOTE_FILE_NAME expo-sharing-demo-note.txt; // 生成笔记文件 const cacheUri Paths.cache.uri.endsWith(/) ? Paths.cache.uri : ${Paths.cache.uri}/; const noteFile new File(${cacheUri}${NOTE_FILE_NAME}); noteFile.create({ overwrite: true, idempotent: true }); noteFile.write(buildNoteContent()); // 分享 await Sharing.shareAsync(noteFile.uri, { mimeType: text/plain, UTI: public.plain-text, dialogTitle: 分享本地笔记, });路径手工拼接而不是用Paths.join这是两处跨端调整之一后文会说明。工程里的 react-native-safe-area-context 保留使用界面用SafeAreaProvider和useSafeAreaInsets处理安全区它在三个平台都有真实用途。安装依赖依赖按四组安装。版本取这次实际使用的组合expo-harmony/*包的 peerDependencies 写明了配套版本安装时需要核对。第一组Expo 官方 JS 包。npminstallexpo55.0.26 expo-modules-core55.0.25\expo-sharing55.0.20 expo-file-system55.0.24第二组HarmonyOS 适配包和上面的版本一一对应。npminstallexpo-harmony/cli55.0.26-harmony.13\expo-harmony/metro-config55.0.26-harmony.4\expo-harmony/expo55.0.26-harmony.3\expo-harmony/expo-modules-core55.0.25-harmony.5\expo-harmony/expo-modules-autolinking55.0.25-harmony.5\expo-harmony/expo-sharing55.0.20-harmony.7\expo-harmony/expo-file-system55.0.24-harmony.5第三组RNOH 运行时。react-harmony是一个 npm alias实际安装的是react19.2.3挂在 react-harmony 这个名字下为什么需要它后面会讲。hermes-compiler必须作为直接依赖安装Release 打包编译 Hermes 字节码时要用。npminstall--save-exact react-native-oh/react-native-harmony0.84.1\react-native-oh/react-native-harmony-cli0.84.1\react-harmonynpm:react19.2.3 hermes-compiler250829098.0.9npminstallreact-native-worklets0.7.4 react-native-ohos/react-native-worklets1.0.0第四组打包配套对齐 expo-harmony bare 示例工程的清单。npminstallexpo/metro-runtime55.0.12 expo/log-box55.0.12\metro0.83.3 metro-config0.83.7npminstall--save-dev babel-preset-expo~55.0.22demo 用到了 safe-area-context 的鸿蒙适配包一并安装。npminstallreact-native-ohos/react-native-safe-area-context5.6.4安装过程中有两个问题需要提前说明。一是 npm 会报 ERESOLVE。RNOH 的 peerDependencies 锁react-native0.84.1项目里 iOS/Android 侧用的是 0.83.10两边冲突。在项目根的.npmrc写入legacy-peer-depstrue即可expo-harmony 官方示例也是这样处理。legacy-peer-depstrue二是 Metro 可能报Cannot find module babel-preset-expo。npm 会把 babel-preset-expo 嵌套装进expo/node_modulesBabel 从项目根解析不到。把它显式安装为根目录的 devDependency 即可解决上面第四组命令里已经带上。配置 Metro让两套 React Native 共存这里需要先交代一个背景。RNOH 和官方 React Native 是两条发布线版本对不齐是常态Expo SDK 锁自己的 react-nativeRNOH 有自己的版本。expo-harmony 的做法是不要求两边一致两套都装进node_modules打包时按平台分流。iOS / AndroidHarmonyOSReact Native0.83.10RNOH 0.84.1基于 RN 0.84.1React19.2.0react-harmony即 react19.2.3原生构建CocoaPods / GradleOHPM HvigorReact 为什么要两份。一个 bundle 里只能有一个 react 实例出现两个会报 hooks 错误。RNOH 0.84 的渲染层配 react 19.2.3Expo SDK 55 锁 react 19.2.0两边不能共用也不能只留一份所以用 npm alias 装出第二份。分流靠 metro.config.js。const{getDefaultConfig}require(expo/metro-config);const{withHarmonyConfig}require(expo-harmony/metro-config);constprojectRoot__dirname;constisHarmonyprocess.env.EXPO_HARMONY1;module.exportswithHarmonyConfig(getDefaultConfig(projectRoot),{enabled:isHarmony,projectRoot,aliases:{react:react-harmony},});enabled由环境变量EXPO_HARMONY控制expo-harmony 的命令会自动设置它。日常 iOS/Android 打包时这个开关关闭打包行为和原来一致。为鸿蒙打包时开关打开import react-native会被换成 RNOHimport react命中 aliases 指到 react-harmony。业务代码一行不用改仍然写import { View } from react-native。注意 Metro 配置要基于expo/metro-config的getDefaultConfig再套withHarmonyConfig。Expo 的 Metro 配置带着模块解析和初始化流程withHarmonyConfig是在这之上接入 RNOH 的 resolver只给 react-native 设一个别名是不够的。接着换 Babel 预设新建 react-native.config.js。// babel.config.jsmodule.exports{presets:[babel-preset-expo]};// react-native.config.jsmodule.exportsrequire(react-native-oh/react-native-harmony-cli/react-native.config.js);react-native.config.js 这行只是把 RNOH 的 CLI 命令注册进来不影响 iOS/Android 原有的 autolinking。最后在 package.json 里加四个脚本。{scripts:{start:harmony:expo-harmony start,run:harmony:expo-harmony run,build:harmony:expo-harmony build,doctor:harmony:expo-harmony doctor}}给 iOS 和 Android 接上 Expo 模块这一步和鸿蒙无关是裸 RN 工程使用 Expo 模块的通用前置。Expo 官方对这件事有文档和install-expo-modules工具我在 RN 0.83 的模板上运行这个工具报了Unable to find compatible Expo SDK version于是参照 Expo 官方 bare 模板手工补了几个文件。遇到同样的报错时按下面的修改即可。Android 侧改两处。settings.gradle接入 expo-gradle-plugin把社区 CLI 的 autolinking 命令换成 Expo 的。pluginManagement { includeBuild(../node_modules/react-native/gradle-plugin) def expoPluginsPath new File( providers.exec { workingDir(rootDir) commandLine(node, --print, require.resolve(expo-modules-autolinking/package.json, { paths: [require.resolve(expo/package.json)] })) }.standardOutput.asText.get().trim(), ../android/expo-gradle-plugin ).absolutePath includeBuild(expoPluginsPath) } plugins { id(com.facebook.react.settings) id(expo-autolinking-settings) } -extensions.configure(com.facebook.react.ReactSettingsExtension){ ex - ex.autolinkLibrariesFromCommand() } extensions.configure(com.facebook.react.ReactSettingsExtension) { ex - ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand) } expoAutolinking.useExpoModules() expoAutolinking.useExpoVersionCatalog()app/build.gradle的 react 块里加三行入口文件交给 expo 解析Release 打包走 Expo CLI这样 Metro 配置在所有平台保持一致。def projectRoot rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() react { entryFile file([node, -e, require(expo/scripts/resolveAppEntry), projectRoot, android, absolute].execute(null, rootDir).text.trim()) cliFile new File([node, --print, require.resolve(expo/cli, { paths: [require.resolve(expo/package.json)] })].execute(null, rootDir).text.trim()) bundleCommand export:embediOS 侧改 Podfile顶部 require expo 的 autolinking 脚本target 里加use_expo_modules!config 命令换成 expo-modules-autolinking 的。require Pod::Executable.execute_command(node, [-p, require.resolve(react-native/scripts/react_native_pods.rb, {paths: [process.argv[1]]}), __dir__]).strip require File.join(File.dirname(node --print require.resolve(expo/package.json)), scripts/autolinking) target ExpoSharingDemo do use_expo_modules! - config use_native_modules! config_command [node, --no-warnings, --eval, require(\expo/bin/autolinking\), expo-modules-autolinking, react-native-config, --json, --platform, ios] config use_native_modules!(config_command)还有一个构建问题。RN 依赖的 glog 0.3.5 在新版 Xcode 下编译不过报unknown type name int32。Podfile 顶部加两个环境变量让 RN 核心依赖走 Expo 模板同款的预编译方案源码编译环节没有了问题随之绕开。ENV[RCT_USE_RN_DEP]||1ENV[RCT_USE_PREBUILT_RNCORE]||1pod install 之后在 Podfile.lock 里确认 ExpoModulesCore、ExpoFileSystem、ExpoSharing 都在。到这里可以先运行一遍 Android 和 iOS确认 Expo 模块在原有平台工作正常再进入鸿蒙的部分。搭建 HarmonyOS 原生工程先交代环境。需要 DevEco Studio 和完整的 HarmonyOS SDKohpm、hvigor、hdc 这些工具都在里面Node 用 20 以上。bare 工程不用 prebuildharmony/原生工程从 expo-harmony 仓库的apps/bare/harmony复制而来官方文档明确允许把示例工程当作起点。复制时排除自动链接的生成物和构建产物这些之后由工具重新生成。# expo-harmony 换成你本地的仓库路径rsync-a\--excludeoh_modules--excludeoh-package-lock.json5\--excludeentry/src/main/cpp/autolinking.cmake\--excludeentry/src/main/cpp/RNOHPackagesFactory.h\--excludeentry/src/main/cpp/rnoh_codegen\--excludeentry/src/main/ets/RNOHPackagesFactory.ets\--excludeentry/src/main/ets/generated\--excludeentry/src/main/resources/rawfile/hermes_bundle.hbc\--exclude.hvigor--excludebuild--exclude.cxx\expo-harmony/apps/bare/harmony/ harmony/复制得到的是一套可直接使用的 RNOH 宿主工程ArkTS 的 Ability、页面、WorkerC 的 CMake 和包注册都在。需要修改的地方不多如下表。文件改什么AppScope/app.json5bundleName 改成com.exposharingdemo.appoh-package.json5name 同步成包名删掉示例里用不到的 expo-battery 依赖build-profile.json5compatibleSdkVersion 改为6.0.1(21)AppScope 下的string.jsonapp_name 改成 Expo Sharing Demoentry 下的string.jsonability label 改成本地笔记分享entry/src/main/ets/pages/Index.etsappKey 从 BareBattery 改成 ExpoSharingDemo有三个容易出错的地方。bundleName 的格式。鸿蒙要求包名至少三段com.exposharingdemo不行hvigor 会报 pattern 校验错误。加一段变成com.exposharingdemo.app即可通过校验。它和 Android 的 applicationId 不要求一致。compatibleSdkVersion。示例工程默认6.0.0(20)而 expo-file-system 的适配包要求最低 API 21构建时会直接报compatibleSdkVersion cannot be lower than the minimum compatible version required by the dependencies。改成6.0.1(21)即可targetSdkVersion 保持不变。appKey 的值要与 app.json 的 name 一致也就是AppRegistry.registerComponent注册的那个名字这里是 ExpoSharingDemo。不一致时页面白屏。另外应用在桌面和任务栏显示的名字由原生资源 string.json 决定改 app.json 的 displayName 对鸿蒙不起作用。自动链接和首次运行依赖和原生工程就位后在应用根目录执行自动链接。npx expo-harmony-autolinkinglink--project-root.--harmony-project-path ./harmonycdharmonyohpminstall--allcd..link 命令解析已安装的 Expo 和 RNOH 模块生成包注册文件RNOHPackagesFactory更新 CMake 配置把各适配包的 HAR 写进 oh-package.json5。ohpm install 负责安装原生依赖首次从 DevEco Studio 或 hvigor 发起构建之前必须先完成这一步。之后新增或删除了原生模块重新构建应用即可。先运行一次诊断。npmrun doctor:harmony它会检查 bare 工程识别、Metro 配置、依赖解析、原生模块清单、DevEco SDK 和工具链。出现 error 时按照输出逐项修改全部通过后再继续。运行使用两个终端。一个启动 Metro注意必须用start:harmony启动的这一份它会带上EXPO_HARMONY1环境变量。为 iOS/Android 启动的那份 Metro 服务不了鸿蒙 bundle解析结果是不对的这一点在实际开发里容易忽略。npmrun start:harmony ----clear另一个终端构建安装。npmrun run:harmony -- --no-bundlerrun:harmony会构建 HAP选择设备或启动模拟器安装并拉起应用端口反向映射也一并处理。不带--no-bundler时它会连 Metro 一起管理分成两个终端是为了分别查看 Metro 和构建的日志。应用启动后界面和 Android、iOS 上的一致。点击「生成本地笔记文件」状态卡片显示文件写进了应用沙箱的 cache 目录路径是file:///data/storage/el2/base/haps/entry/cache/expo-sharing-demo-note.txt分享能力检测为可用。再点击分享按钮系统分享面板弹出。AndroidiOSHarmonyOS三个平台的面板样式不同。Android 显示 Sharing 1 file 和文件名iOS 显示文本文稿和字节数鸿蒙显示文件卡片和大小下面是华为分享、复制、另存为、打印这些目标同机安装的其他支持接收分享的应用也会出现在列表里。面板关闭后 Promise 完成三端行为一致应用只拿到面板关闭这个事实用户选了什么目标、有没有真的分享出去接口不提供。业务代码里的两处跨端调整接入鸿蒙对业务代码的影响只有两处都不是平台分支改完三个平台共用。第一处是文件路径。new File(Paths.cache, note.txt)这种写法在 iOS 和 Android 上正常在鸿蒙上拿到的 uri 会变成 cache 目录本身文件名丢失写入时报Cannot replace a directory with a file。原因是 RNOH 的 URL polyfill 不支持 pathname 写入Paths.join内部走file://URL 分支时取回的还是原值。改成手工拼接缓存目录 URI 和文件名三端行为一致。const cacheUri Paths.cache.uri.endsWith(/) ? Paths.cache.uri : ${Paths.cache.uri}/; const noteFile new File(${cacheUri}${NOTE_FILE_NAME});第二处是时间格式化。RNOH 的 Hermes 没有实现Date.prototype.toLocaleString直接调用显示dateFormat not implemented。笔记里的生成时间改为手工格式化同时保证了三端显示统一。const pad (n: number) String(n).padStart(2, 0); const generatedAt ${now.getFullYear()}-${pad(now.getMonth() 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())};零散的坑safe-area-context 的适配包没有被自动链接。生成的注册文件里只有 Expo 系和 worklets 的包缺SafeAreaViewPackage。排查后发现 RNOH 的链接工具只处理 package.json 里声明了harmony.autolinking的包这个适配包只声明了harmony.alias。解决办法是写一个 postinstall 脚本在每次 npm install 之后给它补上 autolinking 元数据。pkg.harmony.autolinking{ohPackageName:react-native-ohos/react-native-safe-area-context,etsPackageClassName:SafeAreaViewPackage,etsPackageImport:named,cppPackageClassName:SafeAreaViewPackage,cmakeLibraryTargetName:rnoh_safe_area,};几个字段分别告诉链接工具 HAR 包名、ArkTS 和 C 侧导出的类名、导入方式以及 CMake 目标名。类名和目标名要与适配包实际导出的一致按包名推断会链接失败报SafeAreaViewPackage.h file not found。postinstall 挂在 package.json 的 scripts 里重装依赖后依然生效。这个经验可以推广生成的注册文件里少了某个适配包时先看它的 package.json 有没有harmony.autolinking声明。Metro 对文件变更的监听在这个环境下不太可靠。修改代码后应用没有变化时先用--clear重启 Metro 再重启应用再排查别的原因。构建时可能看到一条ERR_EXPO_HARMONY_DUPLICATE_MODULE警告是依赖树里嵌套的react-native0.84.1触发的构建结果不受影响可以忽略。写在最后回顾为鸿蒙接入实际做的事。安装了一批依赖其中鸿蒙专用的一半在接入 iOS/Android 阶段已经装好。写了一份 metro.config.js用别名让两套 React Native 分流。从示例工程复制了 harmony/ 目录改了包名、SDK 版本、应用名和 appKey 四处。执行了一次自动链接。业务代码改了两行兼容写法。没有做的事更能说明问题。没有写一行 ArkTS 业务代码没有平台分支没有为鸿蒙替换任何库原有 iOS 和 Android 的构建流程没有变化。模块移植和工具链这两部分最繁重的工作expo-harmony 已经完成落到应用这一层剩下的主要是配置和少量跨端兼容代码。如果应用本来就在用 Expo 模块或者愿意把部分原生能力换成有适配的 Expo 模块上鸿蒙的改动量就是这篇文章记录的内容。bare 接入的完整文档在仓库的 BareInstallation.md建议动手前通读一遍QuickStart.md 里对两套 React Native 共存机制有更细的解释。