
刷文档、看视频、导航的时候屏幕自动熄灭是最扫兴的一件事。expo-keep-awake就是解决这个的库——让应用在前台时保持屏幕常亮。它是 Expo 体系里的基础库很多上层库视频播放器、阅读器、地图导航都会间接依赖它。这个库在鸿蒙上没有官方实现所以我做了一版适配。本文把整条链路写清楚从上游同步、鸿蒙实现怎么写、到接入宿主并跑通。环境准备本文不重复环境搭建步骤。RNOHReact Native for OpenHarmony开发环境的完整配置见官方开发者指南https://atomgit.com/CPF-RN/docs/blob/main/开发者指南/02-搭建准备/环境初始化.md一、版本配套四件套必须对齐RNOH 项目有个硬约束RN 版本、RNOH 的 npm 包、RNOH 的 ohpm 包、DevEco SDK 四者必须对齐错一个就是编译报错或者白屏。而且版本矩阵只是通用参考具体库验证过的组合才算数。我这次锁定的组合项版本expo-keep-awake57.0.2与 npm 上游 latest 一致React Native0.84.1React19.2.3react-native-oh/react-native-harmonynpm0.84.3rnoh/react-native-openharmonyohpm0.84.3Compile SDK26.0.0DevEco Studio26.0.0 Release实现方式TurboModule CAPI 架构官方 skill 的版本矩阵里 0.84 系列记的是 RNOH0.84.2我这版用的是0.84.3——以实际验证通过的为准别照着矩阵硬套。二、适配步骤第一步上游同步到 AtomGitexpo-keep-awake不是独立仓库它是expo/expomonorepo 里的一个包packages/expo-keep-awake。我的做法是在oh-react-native组织下建一个独立仓库把上游这个包的源码同步过来并锁死基线 commitupstreamCommit: 9e5319c0f821a27b7924841903abae50e2b41790锁 commit 这一步不能省。上游是 monorepo包目录会跟着主仓一起动不锁基线的话以后想复现这版适配对应上游哪份代码就说不清了。这条信息我写进了仓库的spec.json。第二步本地克隆gitclone https://atomgit.com/oh-react-native/expo-keep-awake.gitcdexpo-keep-awake第三步确定交付分支与版本号适配包和普通库不一样它是要被别的主程按版本引用的所以版本号必须能一眼看出上游版本 鸿蒙实现版本。我用main作开发分支完成后打 TAG 交付gittag57.0.2-ohos-1.0.0命名规则是上游版本-ohos-适配版本。调用方按 TAG 引用就不会被后续改动影响到expo-keep-awake:githttps://atomgit.com/oh-react-native/expo-keep-awake.git#57.0.2-ohos-1.0.0第四步适配实现——新增了什么、为什么这是核心。上游给的是 iOS/Android 实现鸿蒙侧要从零写。新增的第一块是 HAR 工程harmony/keep_awake/harmony/keep_awake/ ├── Index.ets # 导出 ExpoKeepAwakePackage ├── oh-package.json5 # 声明包名 react-native-ohos/expo-keep-awake ├── build-profile.json5 └── src/main/ ├── module.json5 ├── cpp/ # CAPI 架构下的 C 侧 │ ├── CMakeLists.txt │ ├── ExpoKeepAwakePackage.h │ └── ExpoKeepAwakePackage.cpp └── ets/ ├── ExpoKeepAwakePackage.ets # 把 TurboModule 交给 RNOH └── ExpoKeepAwakeTurboModule.ts # ★ 真正的实现第二块是 TurboModule 的实现。上游在 JS 侧声明了三个方法我逐个落到鸿蒙JS 侧方法鸿蒙实现isAvailableAsync()探测当前窗口可不可用activate(tag)把 tag 加入持有集合设置窗口常亮deactivate(tag)从集合移除 tag空了就恢复原标志核心就一句窗口调用awaittarget.setWindowKeepScreenOn(next.size0||original);target是当前应用窗口。这里我特意没有去改系统休眠超时也没有创建后台运行锁——只动自己应用的窗口标志作用域最小不需要额外权限也不会影响全局功耗。上游 Android 实现也是类似的选择。original是接管之前这个窗口的常亮状态。写成next.size 0 || original而不是直接写next.size 0是为了不擅自关掉宿主本来就设好的常亮。这个细节在第四节展开。第三块是package.json里的 autolinking 声明harmony:{alias:expo-keep-awake,autolinking:{ohPackageName:react-native-ohos/expo-keep-awake,etsPackageClassName:ExpoKeepAwakePackage,cppPackageClassName:ExpoKeepAwakePackage,cmakeLibraryTargetName:rnoh_keep_awake}}这四个名字是 RNOH 找到这个包的凭据。少一个或者拼错表现都是编译过了但模块没注册运行时才发现很难查。第五步补全适配仓库所需的额外文件库的 README 和 CHANGELOG 我没动上游原文适配相关的东西单独成文件文件作用README.OpenHarmony.md/README.OpenHarmony_CN.md适配说明能力对照、版本配套、接入方式、已知限制spec.json机器可读的适配规格包名、模块名、方法清单、版本配套、基线 commit、验证结论RN_expo-keep-awake代码检查报告.md代码检查结论、真机状态与截图索引harmony/keep_awake.har预编译产物随包分发装依赖即可拿到spec.json里我记了一份验证数据方便后来人核对validation:{status:pass,date:2026-09-13,systemVersion:OpenHarmony-7.0.0.105,contractTests:6,deviceScenarios:6,screenshots:6,hapSha256:043153d1915b1761f228289bb9af851cf82cfdc71daf66baf364f031b1b8f4c9,releaseTag:57.0.2-ohos-1.0.0}hapSha256是当时产物的哈希。以后有人怀疑你验的那版和现在这版是不是同一份对一下哈希就知道。第六步代码推送gitpush origin maingitpush origin57.0.2-ohos-1.0.0三、这个适配包长什么样克隆下来第一眼会有点意外它没有example/也没有可运行的应用。expo-keep-awake/ ├── package.json # 含 harmony.autolinking ├── spec.json # 适配规格 ├── src/ │ ├── index.ts # JS 侧 API │ └── NativeKeepAwake.ts ├── harmony/ │ ├── keep_awake.har # 预编译产物3.2 KB │ └── keep_awake/ # HAR 源码 └── README.OpenHarmony*.md两个要点它是带原生实现的适配包。和纯 JS 库不同它必须编译原生代码所以不能只npm install就完事还要走 ohpm 和 hvigor。files字段里包含harmony所以从 git 装依赖时能直接拿到 HAR。files:[src,harmony,LICENSE,README.OpenHarmony.md,README.OpenHarmony_CN.md]四、接入宿主只有三处改动面库本身不能独立运行必须有一个 RNOH 宿主 App。社区已有现成的——oh-react-native/RNOH084Demo是RNOH 0.84.3 的多库验证宿主版本和我这版适配完全一致而且自带一个很实用的机制// harmony/entry/src/main/ets/entryability/EntryAbility.etsconstrnAppKeywant.parameters?.[rnAppKey]asstring|undefined;AppStorage.setOrCreate(rnAppKey,rnAppKey??RNOH084Demo);Index.ets里RNApp的appKey取自它于是一个宿主可以挂很多独立测试页用命令行参数切换hdc shell aa start-bcom.rnoh084.demo-aEntryAbility--psrnAppKey KeepAwakeTestApp而且它的 bundle 加载链已经是「Metro 优先 静态 bundle 兜底」调代码不用改原生newAnyJSBundleProvider([newMetroJSBundleProvider(),newFileJSBundleProvider(/data/storage/el2/base/files/bundle.harmony.js),newResourceJSBundleProvider(...,hermes_bundle.hbc),newResourceJSBundleProvider(...,bundle.harmony.js)])接入要改的地方第一处package.json。expo-keep-awake:file:../expo-keep-awake也可以按 README 写的方式装npm install githttps://atomgit.com/oh-react-native/expo-keep-awake.git#57.0.2-ohos-1.0.0再跑./node_modules/.bin/react-native link-harmony。本地file:装的好处是不依赖网络改完直接生效。第二处两级oh-package.json5都要写 HAR。react-native-ohos/expo-keep-awake: file:../node_modules/expo-keep-awake/harmony/keep_awake.har,harmony/oh-package.json5管工程级、harmony/entry/oh-package.json5管模块级两处都要加。只加一处会出现能找到包但链接不上。第三处在 ETS 侧注册 Package。// harmony/entry/src/main/ets/RNOHPackagesFactory.etsimporttype{RNPackageContext,RNOHPackage}fromrnoh/react-native-openharmony;importExpoKeepAwakePackagefromreact-native-ohos/expo-keep-awake;exportfunctioncreateRNOHPackages(ctx:RNPackageContext):RNOHPackage[]{return[newExpoKeepAwakePackage(ctx),];}代码写在哪这里说清楚接入的全部改动面就是这三个文件。C 侧不用手改——CAPI 架构下PackageProvider.cpp会自动消费 autolinking 生成的RNOHPackagesFactory.h。五、实现上的三个设计点点一用 tag 集合管理持有者上游的语义是按标签持有多个业务方各自持有一个 tag谁都不释放就一直常亮。我用Setstring记当前持有者privatetags:SetstringnewSetstring();privatechange(tag:string,activate:boolean):Promisevoid{if(!activate!this.tags.has(tag))return;// ← 释放未知 tag 直接返回constnextnewSetstring(this.tags);if(activate)next.add(tag);elsenext.delete(tag);awaittarget.setWindowKeepScreenOn(next.size0||original);this.tagsnext;// ← 设置成功后才提交}两个细节值得说先复制再改、成功后才提交next是新集合只有setWindowKeepScreenOn成功返回后才this.tags next。原生设置失败时持有者集合不会被改坏调用方可以重试。释放未知 tag 是 no-opif (!activate !this.tags.has(tag)) return;。某个业务方重复释放、或者释放自己从没申请过的 tag都不会误伤别人持有的常亮。这一点我专门测了。点二恢复接管前的原值而不是硬置假awaittarget.setWindowKeepScreenOn(next.size0||original);original是接管之前窗口的常亮状态。只要有任意一个 tag 还持有就保持常亮全部释放了恢复成原来的值而不是硬置为false。如果宿主本来就设了常亮我不会擅自关掉它。模块销毁时同样恢复awaitthis.boundWindow.setWindowKeepScreenOn(this.originalKeepOn);this.tags.clear();窗口已经销毁时只记一条警告不抛错hilog.warn(0x0000,ExpoKeepAwake,Window unavailable during wake-lock cleanup);点三addListener保持不可用exportfunctionaddListener(...):EventSubscription{consterrorObject.assign(newError(ExpoKeepAwake.addListenerForTag is unavailable on harmony),{name:UnavailabilityError,code:ERR_UNAVAILABLE,});throwerror;}上游的addListener监听的是浏览器 wake-lock 的释放事件原生平台本来就没有。我没有假装支持而是和 iOS/Android 一样抛ERR_UNAVAILABLE。这一点很关键调用方按平台写降级逻辑时行为要一致。如果鸿蒙返回一个永不回调的订阅对方会以为监听成功了问题要到很久以后才暴露。六、构建与运行# 1) 装 JS 依赖npminstall# 2) 生成调试签名 装 ohpm 依赖cdharmony devecocli signature generate ohpminstall--all# 3) 打包 JS bundle输出到 harmony/entry/src/main/resources/rawfile/cd..npmrun dev# 4) 编译 HAPcdharmony hvigorw--modemodule-pproductdefault-pmoduleentrydefault assembleHap --no-daemon# 5) 安装 启动测试页hdcinstall-rentry/build/default/outputs/default/entry-default-signed.hap hdc shell aa start-bcom.rnoh084.demo-aEntryAbility--psrnAppKey KeepAwakeTestApp耗时要有心理准备首次原生编译37 分 50 秒其中BuildNativeWithNinja一项就 37 分 6 秒。RNOH 的 C 体量大而且我为模拟器放开了两个 ABIarm64-v8a x86_64。增量构建快得多。还有个容易误判的地方hvigor 打完CompileArkTS那一行之后就不再逐行输出了原生阶段可能几十分钟没有新日志。别以为卡死了——去看进程clang还在跑就是正常的。七、踩坑记录坑一宿主的package-lock.json会拦住你裁剪完宿主依赖后直接npm installnpm error code EMISSINGTARGET npm error Missing target in lock file: ../../private/tmp/lib_react-native-annotated-text锁文件里还记着已删掉的依赖。删package-lock.json和node_modules重装即可。坑二metro.config.js指向不存在的目录Error ENOENT reading contents of ...\react-native-image-pixelmap, skipping. Failed to construct transformer: ENOENT: no such file or directory, stat ...\react-native-document-scanner-pluginwatchFolders只留真正存在的那个watchFolders:[path.resolve(__dirname,../expo-keep-awake)],顺带一个知识点本地file:依赖在 node_modules 里是symlink不把库的真实目录加进watchFoldersmetro 解析不到它的源码。坑三babel.config.js里挂着已删依赖的插件error index.js: Cannot find module react-native-worklets/plugin宿主原来给 reanimated 系库挂了 worklets 插件依赖裁掉了插件也得删。坑四compatibleSdkVersion太低会构建期被拦00306004 Specification Limit Violation Error Message: The projects compatibleSdkVersion: 12 cannot be lower than the minimum compatible version 17 required by the dependencies: react-native-ohos/expo-keep-awake.宿主原来是5.0.0(12)而 HAR 声明的最低兼容版本是 API 17。抬到5.1.0(18)即可。这个报错本身是好事说明 HAR 的module.json5里声明了自己的最低兼容版本依赖方过低会在构建期被直接拦下而不是等到运行时才炸。做适配包时应该在module.json5里如实声明这个版本。坑五模拟器是 x64默认只编 arm64宿主build-profile.json5里没有abiFilters默认只出arm64-v8a装到 ohos-x64 模拟器上直接失败。要显式放开buildOption: { externalNativeOptions: { path: ./src/main/cpp/CMakeLists.txt, abiFilters: [arm64-v8a, x86_64] } }代价是编译量翻倍——这也是首次构建 38 分钟的原因之一。只上真机的话可以去掉x86_64。坑六DEVECO_HOME指向了错的 DevEco这台机器上装了两个 DevEco StudioC:\Program Files\Huawei\DevEco Studio6.1.1.280和E:\dev\DevEco Studio26.0.0.621。环境检测脚本按默认安装路径探测一开始取的是 C 盘那个还把 SDK 连带读成6.1.1.125 / API 24。把DEVECO_HOME指向 E 盘后立刻变成26.0.0.621 SDK 26.0.0.32 / API 26。脚本报的版本不一定是你以为的那个安装配完环境要回读一次确认。八、真机验证验证环境Pura X View模拟器HarmonyOS 7.0.0(26.0.0) Beta2API 26ohos-x64。我写了一个自检台测试页覆盖五个能力可用性探测、按 tag 激活/释放、Hook 挂载释放、addListener边界、事件记录列表。可用性与按 tag 操作[keep-awake-test] isAvailableAsync() - true [keep-awake-test] activateKeepAwakeAsync(reading) - ok [keep-awake-test] activateKeepAwakeAsync(reading) - ok ← 重复激活幂等 [keep-awake-test] activateKeepAwakeAsync(video) - ok [keep-awake-test] deactivateKeepAwake(reading) - ok ← 此时 video 仍持有 [keep-awake-test] deactivateKeepAwake(video) - okHook 的挂载与释放[keep-awake-test] HookHolder mounted - useKeepAwake(hook-tag) [keep-awake-test] HookHolder unmounted - release(hook-tag)addListener边界[keep-awake-test] addListener() - UnavailabilityError / ERR_UNAVAILABLE符合预期鸿蒙不支持 Web wake-lock 事件原生侧确认 TurboModule 真的注册了RNInstance::TurboModuleProvider TM created: ExpoKeepAwake TurboModuleFactory.cpp:54 Creating Turbo Module: ExpoKeepAwake RNOH084Demo -- onCreate rnAppKeyKeepAwakeTestApp能力对照能力结果isAvailableAsync()✅ 返回trueactivateKeepAwakeAsync(tag)✅ 按 tag 持有相同 tag 重复激活幂等deactivateKeepAwake(tag)✅ 释放对应 tag未知 tag 为 no-opuseKeepAwake(tag)✅ 挂载激活、卸载释放addListener(...)✅ 抛UnavailabilityError/ERR_UNAVAILABLE九、已知限制一、常亮只作用于当前窗口。应用退到后台、窗口销毁、或者系统电源策略介入时仍由系统说了算。这个库解决的是前台不被息屏不是设备不睡。二、不修改系统休眠超时也不创建后台运行锁。这是刻意的选择——改系统设置需要更高权限创建后台锁影响全局功耗。只用应用自己的窗口标志作用域最小、不需要额外权限。三、窗口常亮标志本身没有系统侧读数。验证做到了三条证据① JS 侧每个 API 的返回值② 原生 hilog 里的TM created: ExpoKeepAwake证明走的是真实实现不是空壳③ 源码里确实调用setWindowKeepScreenOn。但想读窗口的keepScreenOn位时被挡住了hdc shell hidumper -s WindowManagerService -a -a → 输出 0 字节hdc shell的身份是uid2000(shell)对这个系统服务没有 dump 权限。所以我把这一条也留在spec.json的 limits 里不当成已验证。四、物理息屏时序未测。有全局屏幕超时覆盖生效时没法可靠地验证该睡的时候会不会睡。这条同样是spec.json里如实记录的限制。五、其他 ROM / 设备未验证。适配方记录的是 OpenHarmony-7.0.0.105本次在 HarmonyOS 7.0.0(26.0.0) Beta2 模拟器上通过。不同厂商 ROM 的窗口策略可能不同。六、多个库同时改同一窗口标志时需要业务协调。我会恢复接管前的原值但如果另一个库也在改同一个标志最终状态取决于调用顺序。十、常见问题Q为什么不能直接npm install expo-keep-awakeAnpm 上那个包只有 iOS/Android 实现没有鸿蒙原生代码。本仓库是独立的鸿蒙实现要按git...#57.0.2-ohos-1.0.0或者本地file:的方式装。README 里也写明了这一点。Q装了之后要不要额外依赖expo-modules-coreA不需要。这版是按 RNOH 的 TurboModule autolinking 规范实现的package.json里的harmony字段就是它接入 RNOH 的全部凭据不依赖 Expo 的模块框架。QactivateKeepAwake和activateKeepAwakeAsync用哪个A用异步版。同步版是上游的弃用接口调用时会打console.warn内部还是转发到异步实现。保留它只是为了兼容老代码。Q重复激活同一个 tag 会不会报错A不会是幂等的。Set语义天然去重验证时连续调用两次activateKeepAwakeAsync(reading)都返回ok。Q释放一个我从没申请过的 tag 会怎样A什么都不做。实现里有if (!activate !this.tags.has(tag)) return;避免误伤其他业务方持有的常亮。这和其他平台的语义一致也专门验证过。Q在鸿蒙上addListener为什么直接抛异常A因为它监听的是浏览器 wake-lock 释放事件原生平台没有这个概念。我的选择是和 iOS/Android 保持一致地抛ERR_UNAVAILABLE而不是静默返回一个永不回调的订阅——后者会让调用方以为监听成功了。代码里用到它的话要按平台降级。Q为什么我在模拟器上编译要 38 分钟ARNOH 的 C 原生库体量大而且为了跑 ohos-x64 模拟器放开了两个 ABI编译量翻倍。只上真机的话去掉x86_64会明显缩短同工程二次构建是增量的也快得多。Q怎么确认库真的生效了而不是只是没报错A三条证据一起看① JS 侧每个 API 的返回值② 原生 hilog 里的TM created: ExpoKeepAwake证明 TurboModule 注册成功不是走了空实现③ 读源码确认调用了setWindowKeepScreenOn。想看第四层证据窗口标志位得在真机上观察息屏行为——这条我还没做到见已知限制。Q宿主里为什么会有rnAppKey这种东西A那是RNOH084Demo宿主自带的机制——一个宿主挂多个库的独立测试页用命令行参数切换互不干扰。--ps rnAppKey KeepAwakeTestApp就是启动本库的测试页。做多库验证时非常省事。小结这个库本身很小——核心实现不到 60 行API 只有五个。但适配过程把RNOH 接入的完整链路过了一遍autolinking 的四个身份名ohpm 包名、ETS 包类、C 包类、CMake 目标少一个都是编译过了但模块没注册HAR 的工程级与模块级双重声明缺一处就链接不上版本四件套必须对齐且以实测组合为准HAR 里的compatibleSdkVersion有约束力依赖方版本过低会在构建期被拦下。几个我认为最值得记的设计选择next.size 0 || original是这版实现的核心一行——恢复接管前的状态而不是硬置假宿主本来就常亮时不会被擅自关掉addListener选择抛异常而不是静默——宁可让调用方明确知道这个平台不支持也不给一个假的订阅只在窗口层面做常亮不碰系统休眠超时、不建后台锁把作用域压到最小。适配本身之外这次也更清楚地看到RN 库的能跑起来和能装进宿主是两件事。库自己只有src/harmony/没有example/真正跑起来靠的是宿主。而社区那个RNOH084Demo宿主——自带rnAppKey多测试页切换——把这一步的成本压得很低。本篇用到的库项内容三方库expo-keep-awake上游 57.0.2 的鸿蒙适配版适配仓库https://atomgit.com/oh-react-native/expo-keep-awake适配 TAG57.0.2-ohos-1.0.0ohpm 包名react-native-ohos/expo-keep-awakeHARharmony/keep_awake.har基线 commit9e5319c0f821a27b7924841903abae50e2b41790宿主工程RNOH084Demo测试页rnAppKey KeepAwakeTestAppexpo-keep-awake:githttps://atomgit.com/oh-react-native/expo-keep-awake.git#57.0.2-ohos-1.0.0// harmony/oh-package.json5 与 harmony/entry/oh-package.json5 都要加 react-native-ohos/expo-keep-awake: file:../node_modules/expo-keep-awake/harmony/keep_awake.har,验证环境项版本React Native0.84.1React19.2.3RNOHnpm / ohpmreact-native-oh/react-native-harmony/rnoh/react-native-openharmony0.84.3Node.jsv24.14.0DevEco Studio26.0.0.621HarmonyOS SDKAPI 2626.0.0.32设备HarmonyOS 7.0.0(26.0.0) Beta2 模拟器Pura X Viewohos-x64宿主 HAP 产物entry-default-signed.hap75.4 MB欢迎加入 CPF-RN 鸿蒙社区https://atomgit.com/CPF-RNReact Native for OpenHarmony 组织https://atomgit.com/oh-react-nativeRN 三方库鸿蒙适配清单https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview