最近接了一个项目,客户要求把一套已经跑了两年的 React Native 应用适配到 OpenHarmony 设备上,而且原有的 Redux 状态管理架构不能推倒重写。一开始我以为这只是换个 Android 壳子的事,真正动手才发现,RN 跑在 OpenHarmony 上从构建链到原生桥接完全是另一套逻辑,尤其是 Redux 中间件这块,既要维持原有业务逻辑的连续性,又要适应鸿蒙式的能力边界。这篇文章就把我这段时间折腾下来的经验做个沉淀,围绕 React Native 开发 OpenHarmony 应用这个大背景,单独把 Redux 中间件的开发拆开讲清楚,适合那些已经在 RN 生态里沉淀了业务、现在被迫或者主动要往 OpenHarmony 迁移的团队,也适合想了解鸿蒙应用开发实际落地的人。
1. 为什么是 React Native 而不是重写一套 ArkTS 应用
1.1 鸿蒙应用开发的现状与三条技术路线
拿到 OpenHarmony 适配需求的时候,团队内部先开了一次技术选型会。摆在面前的路基本有三条:直接用 ArkTS 重写一遍,这是官方最推荐的方式;用 Flutter 的 OpenHarmony 分支;再就是保留现有 React Native 代码,通过 RN 的鸿蒙适配层接进去。
重写 ArkTS 的成本不用算也知道,我们这套应用有四十多个业务页面、十几个原生模块调用,完全推倒重来没个半年下不来。Flutter 的方案也很诱人,但团队核心成员都是 React 技术栈,B端后台管理系统里大量复用的小组件、状态管理逻辑都在 RN 生态里沉淀着。所以只要 React Native 能在 OpenHarmony 上稳定跑起来,迁移成本最低的路就是它。这里要澄清一个很多人会混淆的事实:OpenHarmony 操作系统本身的核心是 C/C++ 实现的,应用层支持 ArkTS、TS、JS 等语言,而 RN 代码本质上也是通过 JS 引擎加载的,所以 RN 到 OpenHarmony 并不是"套个安卓模拟器",而是真正的原生桥接。
实际调研下来,社区主流的适配方案是 react-native-harmony 这套框架,它把 RN 的 turbo 模块和 OpenHarmony 的 NAPI 层对接了起来。当前阶段它的版本迭代很快,但稳定性已经能支撑真实业务上线,而不是停留在 demo 阶段。正是这个判断,让我们下决心在这条路上继续往前走。
1.2 RN 适配 OpenHarmony 的进展:什么能跑,什么还有坑
在真正动手前,我建议所有团队先做一个"能力摸底",别急着把整个工程搬过来。拿我们项目的经验来说,RN 基础组件在 react-native-harmony 下的还原度已经相当高,View、Text、ScrollView、FlatList 这些常用组件基本能满足日常开发,核心交互是没问题的。
但有一类能力必须先踩雷再评估:所有依赖原生 SDK 的功能,比如相机、定位、文件存储、生物识别。热词里出现的 camera 就是典型代表。RN 侧调用相机走的是自定义原生模块 + NativeEventEmitter 那套流程,在安卓上写得飞起,到 OpenHarmony 上就得重新看它对 NAPI 的映射方式。这个坑我在后面专门开一章讲,因为它直接影响中间件怎么设计。
另一个常见的动荡点就是"启动白屏"。应用在 OpenHarmony 设备上启动时,首屏很容易出现长时间白屏,根本原因往往不是 RN 代码,而是 JS 引擎初始化、Bundle 加载和原生 View 附着这三者之间的协调问题。这块我也放到后面细说,因为它和中间件的关系远比表面看起来大。
1.3 在鸿蒙场景下选型 Redux 的原因
有人会问,团队既然都准备切 OpenHarmony 了,为什么状态管理还坚持用 Redux,不换成 Zustand 或者 Jotai 这种更轻的方案?原因是原则性的:跨端迁移的核心风险是逻辑不一致,而不是写法不够新潮。Redux 的单一数据源和纯函数 reducer 保证了同样的 action 序列在任何平台上产生同样的 state,这对 B 端应用来说是底线。Zustand 虽然轻,但它在 React 渲染机制上的优化方式和 RN 的跨端桥接层交互时,调试链路反而不如 Redux 清晰。
更关键的是,Redux 的中间件机制给了我一个非常干净的切入点,可以统一处理鸿蒙场景下的异步任务和原生能力调用。在 OpenHarmony 上,RN 层与原生能力的通信延迟、权限校验、错误码体系都和安卓不一样,这些脏活如果散落在业务代码里,后期排查肯定想死。用中间件把它们收拢起来,是我在这篇文章里最想传递的核心思路。
2. 环境搭建与工程接入:RN 进鸿蒙的桥接细节
2.1 需要的开发环境与版本选型
环境搭建这块网上资料不少,但版本坑特别多,我在这里给出一份经过验证的组合。开发机建议用 Windows 或者 macOS 都行,核心工具是 DevEco Studio,这是 OpenHarmony 应用开发的官方 IDE,类似安卓开发里的 Android Studio。设备侧需要一台 OpenHarmony 系统的开发板或者真机,如果暂时没有真机,DevEco Studio 里也有模拟器可以应付前期开发。
RN 侧的依赖我锁定的是 react-native-harmony 兼容的版本:React Native 0.72 系列 + Redux 4.x + redux-thunk。这套组合在我们项目里最稳。注意不要一上来就追最新版 RN,react-native-harmony 的适配通常滞后于 RN 官方版本,版本错配会导致原生编译失败。安装方式就是把模板工程从 GitHub 上拉下来,然后用 npm 安装依赖,最后用 DevEco 打开其 harmony 子工程进行构建。
你们项目如果是老工程迁移,务必先跑通一个最小 Demo:只包含一个 Hello World 页面 + Redux 完整链路。我见过太多团队一上来就把大工程往鸿蒙上搬,结果问题像雪球一样滚,连问题出在哪一层都说不清楚。
2.2 工程结构:RN 代码如何被鸿蒙侧加载
理解工程结构,是理解后面所有中间件问题的基础。一个 RN + OpenHarmony 工程,目录上分为两部分:上面是完整的 RN 工程(package.json、src、node_modules),下面是一个 harmony 子目录,里面是 OpenHarmony 的原生工程(entry 模块、ArkTS 代码、module.json5 配置)。
运行时流程是:设备启动鸿蒙应用入口 -> 原生工程创建一个 RN 宿主页面 -> 加载 JS Bundle -> 在原生页面上渲染 RN 组件。这个"宿主页面"的创建细节很关键,它决定了你能否把 RN 页面嵌入到已有的鸿蒙原生页面里,还是只能整页用 RN 渲染。react-native-harmony 的当前实现里,通过 FragmentContainer 这种方式可以在原生页面里局部挂载 RN 视图,灵活性还不错。
2.3 在 DevEco 中接入 RN 侧工程的注意点
实际操作中,最容易翻车的是两个文件:工程根目录的 build-profile.json5 和 entry 模块下的 module.json5。前者管原生工程编译参数,后者管应用权限和组件声明。
权限声明这块我要重点提醒:你在 RN 侧 AndroidManifest 里习惯性写的一堆权限,在 OpenHarmony 里完全不生效,必须重新在 module.json5 的 requestPermissions 里声明。比如你要用相机,就得申请 ohos.permission.CAMERA;要用位置,就得申请 ohos.permission.LOCATION。这个差异直接影响中间件层面怎么设计原生权限状态,后面第六章我会展开。
另外还有一个隐蔽问题:DevEco 的工程同步默认会清理掉 node_modules 里的某些符号链接,导致 RN 侧依赖在编译期突然找不到。解决办法是每次同步后执行一次 npm install,或者把 node_modules 加入不清理名单。这个小坑花了我整整一天才定位到,写出来希望你们不要浪费这个时间。
3. Redux 中间件在鸿蒙场景下的定位与设计取舍
3.1 中间件到底解决了什么问题
很多人对中间件的理解停留在"处理异步 action"这一层,其实它的本质是一个拦截与转发机制。Redux 的 action 流是单向的:视图 dispatch 一个 action,reducer 收到后生成新 state。中间件插在"dispatch 之后、reducer 之前"这条链路上,可以对 action 做任何干预,包括放行、拦截、改造、延迟、发起副作用。
在这个基础机制上,OpenHarmony 场景给了中间件一个新的使命:成为 RN 侧和鸿蒙侧能力之间的"翻译层"。OpenHarmony 的异步 API 大量采用 promise 与回调两种形态,错误码体系和安卓完全不同,业务侧不该感知这些差异。中间件把原生能力调用封装成一个 action,业务组件只管 dispatch,不用关心底层是走了 NAPI 还是 Metro 桥接。
3.2 鸿蒙场景下的三类典型中间件需求
我梳理了我们项目里必须写的三类中间件:日志中间件、异步请求中间件、原生能力桥接中间件。这三类不是并列关系,而是分层关系。
日志中间件在最外层,负责记录每一次 dispatch 的 action 和目标 state,是所有问题排查的第一现场。异步请求中间件在中间层,接管所有网络请求,统一处理超时、错误码、token 刷新逻辑。原生能力桥接件在最内层,把相机、定位、文件系统这些鸿蒙原生能力封装成同步 fetch 式的调用。为什么按这个顺序叠放,后面第四章我会具体解释。
有一个设计取舍我觉得尤其重要:不要在中间件里维护业务状态。Redux 中间件本质是"过程增强",不是"数据存储"。我见过有同事把登录 token 直接存在中间件闭包里,导致刷新页面后状态丢失,最后又去中间件里做持久化,绕了一大圈。正确做法是异步结果仍然通过 dispatch 新的 action 流回 reducer,中间件只负责"转发与增强"。
3.3 中间件的设计边界:不要在 Redux 里做不该做的事
对 RN + OpenHarmony 这个组合,有一个边界必须划清:中间件适合处理"有明确生命周期"的调用,不适合处理"持续性的原生事件流"。比如相机预览,它是一连串持续产生的帧数据,如果你试图用 Redux action 一帧帧更新 store,性能直接崩。正确做法是相机预览仍然通过原生组件渲染,只在拍照、切换镜头这些离散动作上走 Redux 中间件。
另外,涉及高频 UI 更新的状态也尽量别通过中间件进 store。OpenHarmony 上 RN 的异步桥接开销比安卓要大一些,实时滑动或者动画相关的数值如果每帧都过 Redux,很容易出现白屏和卡顿。中间件的定位应该是"低频、离散、需要跨端协调"的操作,这个意识越早建立,后面的性能问题越少。
4. 实战:日志、异步请求、原生能力桥接三层中间件
4.1 日志中间件:带任务链标识的 dispatch 追踪
先上代码,这是最基础的自定义中间件,直接套 Redux 的标准签名:
const loggerMiddleware = store => next => action => { const prevState = store.getState() console.log('[dispatch]', action.type, JSON.stringify(action.payload)) const result = next(action) console.log('[nextState]', store.getState()) return result }在 OpenHarmony 真机上调试日志很不方便,特别是原生崩溃和 JS 报错混在一起的时候。所以我在这个基础版上做了一点增强:给每个业务请求分配一个 traceId,从 dispatch 开始记录下来,中间件在日志里都会带上这个 traceId,后续原生侧返回错误时也能对上这条链路。这就是把日志中间件做成"任务链追踪器"的思路,排错时效率提升非常明显。
真机调试时可以用鸿蒙的 log 工具过滤输出。日志中间件在开发期保留 console.log,上生产环境时建议用一个 flag 开关直接跳过核心逻辑,避免不必要的字符串序列化开销。字符串拼接在大列表场景下会拖慢 JS 线程,这个优化出厂前一定要做。
4.2 异步请求中间件:把 RN fetch 与鸿蒙网络能力统一
网络请求是刚需。OpenHarmony 有自己基于 socket 的网络 API,RN 的 fetch 在鸿蒙上也能用,但对请求头的处理、代理配置、证书校验和安卓有明显差异。我要把网络请求的全部细节收敛到中间件里。这里用一个简化版 asyncMiddleware 兼容 redux-thunk 的写法:
const asyncMiddleware = store => next => action => { if (typeof action === 'function') { return action(store.dispatch, store.getState) } return next(action) }真正的业务在 thunk 函数里写。比如一个请求用户信息的 thunk,中间件会在合适时机 dispatch loading、success、error 三个 action,业务组件只根据 state 渲染即可。在鸿蒙上有一个细节,请求超时时间不要沿用安卓的默认值,OpenHarmony 的网络栈有时慢一点,建议根据接口重要级分别设 10 秒和 30 秒两档。
还有一个必须注意的坑:并发请求数量。鸿蒙设备上的并发连接数限制和安卓不一样,B端应用容易出现大量请求并发,导致连接池满。中间件里加一个简单的队列就很有必要,超过阈值时把请求排队,前一个完成再发下一个。这种代码看着粗暴,但在真机上实测提升明显。
4.3 原生能力中间件:相机、定位等能力回调如何接入 Redux
这是 OpenHarmony 场景最有特色的一层。以相机为例,RN 侧计划 dispatch 一个 CAPTURE_REQUEST 动作,中间件拦截后调用鸿蒙原生相机模块,原生模块返回 Promise(或者通过事件回调),然后中间件把结果转成 CAPTURE_SUCCESS 或 CAPTURE_FAILED 再 dispatch 回 reducer。下面是核心伪代码:
const nativeBridgeMiddleware = store => next => action => { if (action.type === 'CAMERA_CAPTURE') { const { onDone, onError } = action.payload || {} openHarmonyCapture() .then(res => { store.dispatch({ type: 'CAMERA_SUCCESS', payload: res }) onDone && onDone(res) }) .catch(err => { store.dispatch({ type: 'CAMERA_FAILED', payload: err }) onError && onError(err) }) return } return next(action) }注意这里容易踩一个坑:OpenHarmony 原来的相机权限授权弹窗是系统级的,它和 RN 侧 JS 的 Promise 不是同一个线程模型,有时相机已经关闭了 JS 侧还没拿到回调。所以中间件里一定要处理超时兜底,比如在发起相机调用时启动一个 5 秒的定时器,超时后直接 dispatch 失败 action,避免界面永远停在"正在拍照"的加载态。
定位、文件选择、生物识别这些能力也是同一套模式。我建议把每个原生能力封装成一个独立的 service 模块,中间件严格按照 action.type 前缀路由到对应 service。这样做的好处是:后续鸿蒙系统升级,某个 NAPI 接口变了,只需要改对应 service 文件,中间件和业务代码都不用动。
4.4 中间件组合顺序的讲究
Redux 中间件是洋葱圈模型,先 applyMiddleware 的排在外层。我们的最终顺序是:
store -> loggerMiddleware -> asyncMiddleware -> nativeBridgeMiddleware -> dispatch
这个顺序不是随便排的。logger 在最外层是为了看到完整的 action 流,包括 thunk 函数执行期间异步 dispatch 出来的新 action。async 在 nativeBridge 外层,是为了让 thunk 函数里 dispatch 的 CAPTURE_REQUEST 能先被 nativeBridge 拦截处理,而不是被 async 直接放行到 reducer。简单说:能拦截原生动作的中间件要更靠近 dispatch 核心。
如果你顺序反了,最直观的表现是 action 经常"穿透"中间件直接到了 reducer,该去的原生调用没去,然后出现页面状态更新了但原生设备没动作这种诡异问题。排查这种问题很费劲,提前把顺序定死就能省掉后续大量 debug 时间。
5. 启动白屏与性能瓶颈:中间件不该背的黑锅与排查链路
5.1 启动白屏的根因拆解
热词里出现"react native 启动白屏"不是偶然,我查了一下社区,这个问题在 OpenHarmony + RN 上几乎人人遇到。一开始我以为是代码初始化太慢,后来用 trace 工具测才明白,主要时间花在:JS 引擎冷启动、Bundle 加载、RN 视图和原生页面附着三段。
中间件在这三个阶段里其实完全没有执行,如果白屏了,很大概率是原生侧的加载流程没协调好。我的排查链路是先看原生日志,确认 JS Bundle 是否在预期时间内加载完成;然后看 bundle 的解析耗时;最后一步才排查 Redux 相关代码。如果你一上来就在中间件里打断点,方向就搞反了。
真正有效的优化手段是:把 Bundle 加载提前到应用启动阶段,而不是等到用户进入 RN 页面时才加载;另外关闭 dev mode 的 debug bundle,用 release 包测启动时间。这两步做完,白屏时间通常能缩短一半以上。
5.2 中间件带来性能开销的量化思路
实话讲,Redux 中间件本身的开销非常小,但在鸿蒙设备上容易因为桥接机制被放大。特别是中间件里如果频繁调用 store.getState(),每次 getState 都跨了一层 JS 桥,成本比安卓上高。建议在中间件里避免每次 dispatch 都做全量 state 序列化,日志中间件在开发期开着没事,生产环境一定要裁剪输出。
我有一次遇到页面卡顿,直觉上以为是 camera 模块导致的,结果 trace 出来是日志中间件里 JSON.stringify 了整个 state,而 state 里又挂了特别大的列表数据,每 dispatch 一次就做一次全量字符串化。把这个优化掉之后,帧率立刻恢复正常。所以性能问题定位时,先把中间件开销排除掉,别急着去调原生代码。
5.3 与相机相关的能力卡顿排查
相机在鸿蒙上的接入还有一个特点:相机预览流是在原生层跑的,也是原生渲染,跟 RN 视图无关。如果拍照后界面卡顿,第一嫌疑不是相机本身,而是拍照生成的高分辨率图片在回传 JS 侧时被做成了多余拷贝。中间件里拿到图片数据后,应该先压缩自动转成 base64 或缩略图的路径,再 dispatch 给 reducer,而不是把原始大图直接丢进 store。
阿里和华为内部的实践也验证了这一点。相机图片从原生传回 JS 侧的路径尽量要短,最好直接在原生侧完成图片旋转、压缩、保存文件的三件事,JS 侧只拿一个文件路径字符串。这个路径字符串放在 Redux state 里没有任何压力。
5.4 用 Log 与 Trace 定位中间件链路问题
开发期排查中间件链路,我强烈建议用两条腿走路:一是鸿蒙自带的 HiLog,它能看到原生层的日志;另一个是在中间件里加的 traceId 日志,它能看到 JS 层的 action 流转。两边日志拼在一起,才能还原一条完整的调用链路。
有一次相机拍照失败,原生层日志和 JS 层日志完全对不上,后来才发现是中间件拦截了 action 之后,原生回调返回时 JS 层的 Promise 已经因为超时被中间件吞掉了,后续的 done 回调抛了个未处理异常。这里我的经验是:中间件里所有原生调用的 Promise 都要挂 catch,哪怕你确定调用不会失败也要挂,OpenHarmony 环境下的异常路径比你想的隐蔽得多。
6. 认证与发布:XTS 认证与原生权限审批的实战注意点
6.1 XTS 认证是什么,为何困扰 RN 开发者
应用开发完成后要上架到 OpenHarmony 生态市场,一般绕不开 XTS 认证。它是 OpenHarmony 兼容性测试套件的简称,用来检测应用是否遵守系统的接口规范和安全要求。对原生开发来说,XTS 认证通常只是走个流程,但对 RN 开发者来说却可能变成鬼门关,因为测试会扫描应用调用的系统 API,一旦发现你调用了某类接口但没在 module.json5 里声明对应权限,或者调用了非公开接口,直接不通过。
我们有一个教训:RN 工程里某个原生桥接模块内部偷偷调用了一个非公开 HDI 接口,在开发机上跑得好好的,送去 XTS 认证直接挂掉。最后花了一周时间重写这个桥接模块,改成走公开 API 才过。所以从第一天写原生桥接代码开始,就有意识地只碰公开 API,能省下后期大量返工。
6.2 权限声明与 HDI 接口调用的权限链路
在 OpenHarmony 里,HDI 是硬件驱动接口,上层应用要访问相机、传感器等硬件,走的链路是:应用层 API -> 权限校验 -> HDI 接口。这里有两层权限要留意:一是 module.json5 里必须声明的权限,比如相机权限;二是某些硬件能力还需要用户动态授权。
中间件在这个权限链路里能做的事情是"状态管理"。我建议在 Redux 里维护一个 permissions 切片,程序启动时主动查询当前权限状态,中间件在调用原生能力之前先检查对应权限位,如果未授权就直接 dispatch 一个 PERMISSION_DENIED action,引导用户去设置页。这样把权限判断从前端业务移到中间件一层,所有页面的权限处理逻辑就统一起来了。
6.3 中间件层面的权限状态管理
具体实现上,这个权限状态的更新源有两个:一是应用启动时的初始化查询,二是用户在系统设置里手动改权限后回前台的事件。第二件事尤其容易漏,用户拍完照发现权限被关了,回应用后你还在中间件里傻傻等相机回调,体验很差。
解决方案是在原生侧监听权限变更事件,通过事件桥接回 JS 层,然后中间件拦截该事件并 dispatch 最新的权限 state。把这个逻辑也收拢到权限中间件里,对上层业务完全透明。经过这一套改造,我的适配版本就再也没出现过"权限改了但页面不知道"的问题。
最后说一句个人体会:OpenHarmony 上的 RN 开发,最大的门槛不在 Redux 也不在中间件,而在于你能否接受"每一层都有点不一样"这个事实。原生侧要适应鸿蒙的 API 风格,JS 侧要接受桥接延迟的客观存在,Redux 中间件反而是整个架构里最稳定的黏合剂。写这套方案时我反复提醒团队:中间件永远只做增强和路由,不做状态存储,不直接碰组件内部逻辑。只要守住这个边界,后续鸿蒙系统再怎么升级,业务代码的迁移成本都很低。我目前已经用这套中间件架构支撑起了三个业务模块的升级迭代,下一步打算把权限中间件和日志中间件抽出来做成一个独立 npm 包,让团队内部其他项目也能复用,这个方向也推荐给正在做同样适配的团队参考。