最近用 React Native 把一个小型 TodoList 完整跑到了 OpenHarmony 设备上,任务卡片的阴影效果前后调了两天。这个项目本身不复杂,复杂的是“RN + OpenHarmony”这个组合。React Native 在 Android 和 iOS 上已经非常成熟,但到了 OpenHarmony 上,你会发现很多“想当然能用”的写法都处于薛定谔状态——尤其是阴影这种纯视觉的东西,看起来很基础,真落地时却处处是坑。这篇文章就围绕这个 TodoList 项目,把三件事讲透:RN for OpenHarmony 的工程怎么搭、任务增删改怎么做、阴影效果为什么要在 OpenHarmony 上单独处理。
如果你是第一次在 OpenHarmony 上跑 RN,或者你只是好奇阴影这类样式在非 Android/iOS 平台上的表现差异,这篇实战记录应该都能帮你省点时间。
1. 为什么我会在 OpenHarmony 上跑 React Native
1.1 从跨端选型说起:为什么不是 ArkUI 原生
先说结论:能用 ArkUI 原生写,就用 ArkUI 原生写。我这次选 RN,纯粹是因为团队里已经有大量现成的 React Native 业务代码,前端同学不需要为了一个小项目重学 ArkTS 和 ArkUI 的声明式写法,迁移成本是决定性因素。
如果你是从零开始做一个 OpenHarmony 应用,没必要绕一圈跑到 RN 上来。ArkUI 原生对系统能力的调用最直接,性能也最有保障,而且 DevEco Studio 对 ArkUI 的调试支持明显比 RN 适配层要完整。但如果你像我一样,手里已经有一个跨端 RN 项目,想低成本跑上 OpenHarmony 设备,那么 RN for OpenHarmony 这套社区方案值得一试。
Flutter 在 OpenHarmony 上也有社区适配,但发布节奏和工具链成熟度目前都要打个问号。相比之下,RN 的 JavaScript 生态更庞大,前端工程师的上手门槛更低,而且 Metro 打包、热更新这些基础设施在 OpenHarmony 上已经有可用的实现。所以我的选型排序是:ArkUI 原生 > RN > Flutter。
1.2 新老架构的坑:选对 RN 版本比写代码更重要
RN 圈最近最热的词之一就是新老架构对比。老架构通过 Bridge 做异步序列化通信,新架构用 JSI 实现 JS 和原生之间的直接调用,性能提升明显,但代价是原生模块必须按 JSI 的规范重新封装。
这个对比在 OpenHarmony 上意味着什么?意味着适配层的工作量会差一个量级。OpenHarmony 的 RN 适配仓库通常优先支持老架构,因为老架构的 Bridge 协议稳定、实现简单。而新架构的 Fabric 渲染器、TurboModules、CodeGen 这套东西对原生侧的要求高很多,适配层需要为 OpenHarmony 的 ArkUI 组件做专门映射,进度自然慢。
我当时选型的原则很简单:用社区适配仓库明确支持的稳定版本,不追求最新也不碰新架构特性。RN 不是越新越好,在 OpenHarmony 上,适配层的支持情况才是真正的版本天花板。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 搭一个能跑起来的 RN 开发环境
2.1 工程骨架与依赖版本锁定
RN for OpenHarmony 的开发流程和标准 RN 略微不同。标准 RN 里,react-native 包自带 Android 和 iOS 工程;但 OpenHarmony 上你要额外引入一个 ArkTS 壳工程,这个壳工程负责创建 HarmonyOS 的 Ability 和页面,然后在里面加载 RN 的 JS 运行时。
我这次的项目结构大致长这样:
text复制RNTodo
├── app.json
├── index.js
├── src
│ ├── components
│ │ └── TaskCard.js
│ ├── screens
│ │ └── TodoScreen.js
│ └── store
│ └── todoReducer.js
├── harmony
│ ├── entry
│ │ └── src
│ │ └── main
│ │ ├── ets
│ │ │ ├── entryability
│ │ │ └── pages
│ │ └── module.json5
│ └── oh-package.json5
└── package.json
harmony 目录就是 OpenHarmony 的壳工程,用 DevEco Studio 打开这个目录,等它同步完 ohpm 依赖,再连上设备或者模拟器就能跑起来。RN 的 JS 代码依然在 src 目录里写,Metro 把 JS bundle 打包后,壳工程会在运行时加载它。
依赖版本这块一定要锁死。react-native 的版本、react-native-openharmony 适配包的版本、OpenHarmony SDK 的 API 版本,三者之间是强关联的。网上很多跑不起来的案例,十有八九是这三个版本互相不匹配。我从头到尾没有升级过依赖,项目能稳定跑起来比什么都重要。
2.2 在 OpenHarmony 设备上拉起第一个页面
初始化完成后,第一件事不是写业务代码,而是先把空壳工程跑起来,确认 RN runtime 能正常加载。我在 DevEco Studio 里连接了一个 OpenHarmony 模拟器,直接运行 entry 模块,等壳工程起来后,Metro 终端会输出类似这样的日志:
text复制 BUNDLE ./index.js ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░ 91.9% (530/576) module: @react-native/virtualized-lists
看到 bundle 进度走完,设备屏幕上出现 RN 默认的文字页面,说明你的 JS 到设备这条链路已经通了。这一步看似简单,但它是后续所有工作的地基。我见过不少人在这一步卡住,最后查出来是壳工程的签名配置不对,或者设备没有打开调试模式。
2.3 双向通信的边界:哪些 JS 代码可以直接跑
RN 在 OpenHarmony 上并不是“所有纯 JS 代码都能直接跑”。基础组件如 View、Text、TextInput、FlatList 基本可用,但很多依赖原生模块的功能就不一定了。
我一开始还抱着“RN 生态里找个库装上去就能用”的心态,后来发现这是最大的误区。比如图片裁剪库,社区里那些成熟方案基本都依赖 Android/iOS 的原生代码,在 OpenHarmony 上根本编译不过。你必须在动手之前就想清楚:这个 RN 库的底层是不是有原生代码?有没有 OpenHarmony 适配?没有的话,要么自己封装 ArkTS 原生模块,要么换纯 JS 实现的库。
3. TodoList 核心功能拆解与实现
3.1 数据模型与状态管理的轻量化选择
TodoList 的业务逻辑不复杂,我没上 Redux,甚至连 Zustand 都没用,直接用 useReducer 就够了。任务对象的数据结构如下:
js复制{
id: 'task_1701234567890',
title: '完成 RN for OpenHarmony 阴影调研',
done: false,
createdAt: 1701234567890,
}
id 用时间戳加随机前缀生成,保证在模拟器、真机上都不会冲突。done 字段控制任务完成态。createdAt 留给未来做排序和统计。
状态管理选轻量方案是有意的。项目规模决定技术复杂度,TodoList 这个量级的应用,引入 Redux 纯属过度设计,反而会增加调试负担。把状态收敛到一个 TodoReducer 里,逻辑清晰,还方便后面写单元测试。
3.2 任务增删改的完整交互链路
TodoList 的交互场景有四个:添加任务、勾选完成、删除任务、编辑任务。我在实现时尽量把逻辑收敛到 reducer 里,视图层只负责派发 action:
js复制function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TASK':
return [action.payload, ...state];
case 'TOGGLE_TASK': {
const target = state.find((t) => t.id === action.payload.id);
if (!target) return state;
return state.map((t) =>
t.id === action.payload.id ? { ...t, done: !t.done } : t
);
}
case 'DELETE_TASK':
return state.filter((t) => t.id !== action.payload.id);
default:
return state;
}
}
添加任务时,输入框的 onSubmitEditing 事件里要先做空字符串拦截,否则用户猛敲回车会创建一堆无意义任务。编辑任务我放到了弹窗里,点击卡片上的编辑按钮弹出 Modal,输入新标题后保存。删除操作做了一次确认提醒,避免误触导致任务丢失。
3.3 FlatList 性能细节:别等到卡顿再优化
任务列表用 FlatList 渲染,这是 RN 最常用的长列表组件。由于任务数量会有几十条甚至上百条,渲染性能不能忽视。
我做了几件事来保证流畅:
- 给每条任务一个稳定且唯一的
key,让FlatList的 diff 算法正常工作。 - 用
React.memo包裹TaskCard组件,避免无关任务更新时整列重渲染。 - 把 renderItem 里的内联函数全部提到组件外部,减少每次渲染的闭包创建。
这些优化在标准 RN 里是老生常谈,但在 OpenHarmony 上更重要。因为 RN 的渲染层最终要桥接到 ArkUI 的组件树,一次多余的 JS 重渲染,在 ArkUI 侧可能对应一次完整的节点 diff。控制渲染频率,就是在控制性能损耗的源头。
4. 重头戏:任务卡片阴影效果
4.1 为什么阴影在 RN for OpenHarmony 上会“失灵”
这是整篇文章最有价值的部分。很多人写 RN 的阴影,第一反应就是 shadowColor、shadowOffset、shadowOpacity、shadowRadius 这一套,但你可能不知道,这套属性从设计之初就不是跨平台的。
标准的 RN 里,shadow* 系列属性只对 iOS 生效,Android 上用的是 elevation,而且 elevation 只支持数值,不支持单独的阴影颜色和透明度配置。所以你在真机调试时经常会看到“iOS 有阴影,Android 没有”这种诡异现象。
到了 OpenHarmony 上,情况更复杂。RN 的样式最终要转换为 ArkUI 的组件属性,而 ArkUI 原生支持 shadow() 方法,可以对组件设置阴影颜色、半径、偏移。问题在于 RN 适配层是否把这个转换做实了。我实测下来的结果是:shadow* 系列在 OpenHarmony 上基本不可靠,elevation 能触发部分效果,但表现和 Android 上也有细微差异。盲区就在这里:你在 Android 上调好的效果,换到 OpenHarmony 上可能直接没了,连个报错都没有。
4.2 几种阴影方案的实测对比
我把常见的阴影实现方案在 OpenHarmony 上逐个测了一遍,结果记录如下:
| 方案 | Android 表现 | OpenHarmony 实测表现 | 推荐度 |
|---|---|---|---|
shadowColor + shadowOffset + shadowRadius |
不生效(仅 iOS) | 大部分版本不生效 | 低 |
elevation |
正常,但样式粗糙 | 部分生效,效果不一致 | 中 |
boxShadow 内联样式 |
较新版 RN 支持 | 视适配层版本而定 | 中高 |
| 纯 View 层级模拟 | 可控性强 | 可控性强 | 高 |
| ArkUI 原生自定义 Shadow 组件 | 需要额外封装 | 效果最真实 | 高 |
表格里最引人注目的其实是最后两项。boxShadow 是 RN 较新版本引入的跨平台阴影方案,它在 Android/iOS 上统一了写法,理论上 OpenHarmony 适配层只要做了对应映射就能直接支持。但“理论上能用”和“实测能用”之间还是有距离,我建议你动手前先在设备上跑一个最小示例验证一下,不要直接写进业务代码。
如果 boxShadow 不可用,纯 View 层级模拟是兼容性最稳的方案。
4.3 基于 View 层级的通用阴影方案
纯 View 模拟阴影的思路很简单:在卡片外面套一层“假阴影容器”,让这层容器露出一种偏移的半透明背景色,营造出阴影的错觉。它的兼容性几乎为 100%,因为它只用了最基础的 View、backgroundColor 和 transform 属性。
我最终采用的 TaskCard 结构如下:
jsx复制function TaskCard({ task, onPress }) {
return (
<View style={styles.shadowWrapper}>
<View style={styles.card} onPress={onPress}>
<Text style={styles.title}>{task.title}</Text>
<Text style={styles.time}>
{new Date(task.createdAt).toLocaleString()}
</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
shadowWrapper: {
marginHorizontal: 8,
borderRadius: 16,
backgroundColor: 'rgba(15, 23, 42, 0.15)',
transform: [{ translateY: 4 }],
},
card: {
borderRadius: 16,
backgroundColor: '#ffffff',
padding: 16,
transform: [{ translateY: -4 }],
},
});
核心逻辑就在 shadowWrapper 和 card 的协作里。外层容器往下偏移 4 个像素,露出底部一条半透明的灰色背景,内层卡片再往上偏移 4 个像素把它盖回去,视觉上就形成了一条底部阴影。
但我要提醒一句:这种方案做出来的阴影比较“硬”,不像真阴影那样有自然的模糊过渡。它在浅色背景上效果还行,遇到深色背景就容易暴露。如果你把卡片放到渐变背景或者深色页面里,务必要降低预期,或者干脆用更极端的扁平化设计:去掉阴影,改用浅色边框和不同背景色来区分层级。
4.4 想要真阴影:走 ArkUI 原生封装这条路
模拟方案说到底只是应急。如果你的设计稿里阴影效果是视觉重点,必须做出那种柔和的、有扩散感的真阴影,那么正路是绕过 RN 的样式桥接,直接在 ArkUI 侧封装一个原生阴影组件。
思路是这样的:在 ArkTS 里写一个自定义 ShadowCard 组件,内部用 ArkUI 原生 API 的 .shadow() 方法添加真正的阴影效果,然后通过 RN 的自定义原生组件机制暴露给 JS 调用。RN 侧只需要传入 shadowColor、shadowRadius 这些参数,真正绘制阴影的工作全部交给 ArkUI 完成。
这样做的优点是效果真实、性能好,缺点是你要写 ArkTS 代码,还要理解 RN 原生组件桥接的整套机制。如果你和我一样只是想快速交付业务,这个方案的开发成本会显得略高。但如果你想长期在 OpenHarmony 上打磨 RN 项目,这个方向值得投入——它一劳永逸地解决了阴影乃至其他样式桥接不完整的问题。
4.5 阴影附带的高频细节:圆角、裁剪与层级
阴影问题解决了,后续还有三件小事经常坑人。
第一,阴影容器和卡片的圆角必须一致。shadowWrapper 和 card 我都用的 16,如果内外圆角不一致,露出来的阴影底部会出现奇怪的直角或尖角。
第二,阴影容器要有足够的 margin,避免被父组件裁剪。FlatList 或外层容器在 overflow: 'hidden' 时,阴影会被直接切开,这可能让你误以为阴影又没生效。
第三,阴影和相邻卡片之间的距离要留够。如果两张卡片的 margin 太小,上层的阴影会压到下层的标题文字上,看起来非常脏。我最后给每张卡片的上下都留了 10 以上的间距,才避免了这种重叠。
5. 开发调试实录与常见问题排查
5.1 调试工具链搭建:Metro、DevEco Studio 与设备真机
OpenHarmony 上调试 RN,你的主战场依然是 Metro。Js 代码的修改、bundle 更新都靠 Metro 完成。流程保持这个习惯:项目目录里先跑 npm start 把 Metro 启动起来,再用 DevEco Studio 运行壳工程,两者配合,迭代速度就快多了。
进入调试状态后,还有一个心得体会:RN 的开发者菜单在 OpenHarmony 上响应不如 Android 积极,快捷键与常见手势不一定全覆盖。最可靠的办法是封装一个仅测试环境可用的调试按钮,点击时执行 DevMenu.show() 类似的逻辑,这样就不会出现想刷新页面却找不到入口的尴尬。这些细微差别很容易让人浪费大量时间在普通操作上,提前有一套固定的调试路径会极大提升效率。
5.2 高频踩坑实录:电话能力与图片裁剪库的适配边界
项目做完后,我顺手验证了社区里两个高频需求在 OpenHarmony 上的真实表现。
第一个是 rn 调用电话功能。在标准 RN 里,通过 Linking.openURL('tel:123456') 即可拉起系统拨号盘。OpenHarmony 上,这个 API 同样可以拉起系统电话应用而不需要额外的通告权限,前提是包体已声明对应的系统能力。但要注意,拉起拨号盘只是“预填号码”,用户还需要手动按拨打键,这样可以绕开很多权限敏感问题。若你需要实现“直接呼叫”能力,权限模型就要复杂得多,务必先确认系统是否支持以及权限是否已正确配置。
第二个是图片裁剪 rn 库。我在项目里想加上“给任务添加图片附件”的功能,顺手试了几个社区里常见的裁剪库,结论是没有一个能直接跑通。这些库的原生代码基本都是面向 Android/iOS 的 JNI 或 UIKit 封装,OpenHarmony 侧没有对应的 bridge 实现,编译阶段就直接挂了。可行的替代方案有两个:找一个专门支持 OpenHarmony 的图片处理组件,或者把裁剪逻辑下沉到 ArkUI 侧,用 ArkTS 实现裁剪功能,再通过原生模块暴露给 RN 调用。如果你有跨端复用需求,方案二是长期正解。
5.3 问题速查表
| 问题现象 | 排查方向 | 解决方案 |
|---|---|---|
| 阴影不显示 | 样式桥接是否支持 shadow* / elevation / boxShadow |
用纯 View 模拟阴影,或走 ArkUI 原生封装 |
| 卡片圆角出现锯齿 | 阴影容器与卡片圆角不一致 | 统一内外 borderRadius 值 |
| 阴影被列表裁剪 | 父容器 overflow 设置不当 |
给阴影容器增加 margin,避免被裁切 |
| Metro 能打包但页面空白 | 壳工程 JS bundle 加载失败 | 检查依赖版本匹配、设备调试模式与 Metro 连接 |
| 图片裁剪库编译失败 | 原生模块缺少 OpenHarmony 适配 | 换支持 OpenHarmony 的组件,或将能力下沉到 ArkUI 侧 |
| 电话功能拉起失败 | 系统电话应用缺失或权限未配置 | 改用拨号盘跳转方式,确认包体权限声明 |
这张表是我这次项目踩坑经验的浓缩。每个问题背后都对应一次真实的“折腾”过程,希望你能直接绕过这些坑。
最后留下的几行代码
这次项目做完,我最想分享的其实不是某一行的写法,而是一种心态调整。在 OpenHarmony 上写 RN,不能拿着“写一遍到处跑”的预期,而要接受“写一遍,到处调”的现实。Android 能过的样式,OpenHarmony 要实测;iOS 能过的组件,OpenHarmony 要验证。但这也正是这个方向有意思的地方:桥接的空白地带,恰好是你能通过封装原生能力创造价值的地方。
如果你也正在做类似的项目,建议先把阴影方案固定下来,再铺业务代码。样式问题早确认,后面就能安心写逻辑了。
