在 OpenHarmony 系统上跑 React Native,再把一个经典 TodoList 做出漂亮的任务卡片阴影效果,这件事听起来像给自己找麻烦,但实际上是检验跨端框架适配水平的最佳方式。我花了两周时间,把 RN 工程从项目初始化一路做到 OpenHarmony 真机运行,期间反复打磨的核心细节正是任务卡片的阴影效果。最终结论有点出乎意料:阴影这个看起来只是一个 style 属性的东西,在 iOS、Android、OpenHarmony 三端背后走的是完全不同的渲染路径。你把这一条链路吃透了,基本就搞清楚了 RN 在 OpenHarmony 上的渲染适配现状。
这篇文章会把能运行的 TodoList 代码、阴影效果的踩坑过程、RN 新老架构对适配的影响,以及与电话拨打、图片裁剪库联动的经验一并整理出来。不论你是想评估 OpenHarmony 生态落地难度的移动端开发者,还是正在处理 React Native 跨端方案的团队,都可以把这篇当作一份能直接参考的实操笔记。
1. 项目选型与整体设计:为什么用 RN 在 OpenHarmony 上做 TodoList
1.1 从业务痛点出发:现有 RN 项目如何拥抱 OpenHarmony
先交代一下背景。我手头有一个已经同时在 iOS 和 Android 运行的 React Native 项目,业务逻辑基本共用一套 JS 代码,原生扩展通过公共模块管理。当设备侧需要支持 OpenHarmony 时,团队当时面临两条路线:一条是用 ArkTS 重新写一套界面,这个方向文档成熟、工具链完善,但意味着双套 UI、双份维护成本,而且之前积累的 RN 生态组件基本全部作废;另一条是评估 RN for OpenHarmony 的适配进度,尽量复用现有代码和组件库。
两条路我都做了技术预研。ArkTS 那条路走到一半,我发现业务逻辑迁移倒还好办,真正难的是 RN 生态里那些成熟库——图片裁剪、电话拨号、图表绘制等等——在 ArkUI 里往往没有现成对应物,要么自己封原生,要么从零写。而 RN 适配路线虽然也存在兼容性不确定性,但至少能把状态管理、网络层、UI 描述这些大部分业务代码原样带过去,性价比明显更高。
其实现在社区里问“OpenHarmony 能不能直接跑 RN”,答案不是非黑即白,关键看你对“能跑”的定义是什么。弹一个 Hello World 是一回事,跑一个完整业务模块是另一回事。从我实测的结论来看,凡是托管在 JS 层的逻辑——状态管理、网络请求、基础列表渲染——可用度已经相当不错;涉及原生侧自定义视图或者系统能力,就需要逐个模块去验证适配。所以项目一开场我就做了一个决策:不把一堆原生依赖一次性塞进工程,先用最少的原生模块跑通核心 UI 链路。这个决策后面被证明非常关键,它让排查范围始终聚焦在渲染和样式上,而不是在几十个原生依赖之间互相甩锅。
整个项目我用的是最经典的 TodoList 形态,但刻意把“任务卡片的阴影效果”作为主攻方向。选这个切入点是有意的:阴影够简单,任何做过移动端的开发者都能看懂;又够复杂,背后牵扯样式解析、渲染管线、原生绘制差异和性能优化一整条链路,刚好是评估 RN on OpenHarmony 成熟度的一个抓手。
1.2 RN 新老架构对比:OpenHarmony 适配走到了哪一步
聊到 RN 在 OpenHarmony 上的适配,新老架构的问题是绕不开的。React Native 的架构在近几年变化非常大,我用同一个项目分别跑过旧架构适配版本以及部分新架构特性的版本,体验差异值得展开。
旧架构的核心是 Bridge 桥接机制。JS 层和原生层之间隔了一条异步通道,所有跨层通信都要先序列化成消息队列,再逐条投递。你可以把 Bridge 理解成两栋楼之间的信件滑梯:请求封装成信丢进滑梯,原生处理完再把响应封装成信丢回来。好处是边界清晰、稳定安全,坏处是吞吐量有明显天花板。快速滚动列表这种高频交互场景,命令经过桥接排队后,帧率很容易被拖下来。RN for OpenHarmony 的早期适配正是在这条基线上做的,遇到复杂列表加动态样式,性能压力肉眼可见。
新架构则是另一种思路。Fabric 渲染器让 JS 直接持有 C++ 层对象的引用,TurboModule 让 JS 调用原生方法时不再做 JSON 序列化,改走 JSI(JavaScript Interface)直调。还是用前面的类比:旧架构是来回递信,新架构是面对面直连。效率提升非常大,但也对底层渲染引擎的集成提出了高得多的要求。
那么 OpenHarmony 适配目前落在哪里?从我的实践看,RN for OpenHarmony 当前以旧架构的工作方式为稳定性基线,核心组件比如 View、Text、ScrollView、FlatList 以及样式系统,已经有相当完整的映射。新架构在 OpenHarmony 上的落地进度相对靠后,因为需要把 Fabric 的渲染协调器接入到 OpenHarmony 的原生组件树上,这是一个工程量很大的底层工作,不是简单换个编译目标就能完成。
这个背景和阴影效果计划有直接关系。老架构适配版本里,样式解析走的是统一映射逻辑,新的渲染器则可能在阴影属性的计算方式上有差异。我的建议是:在把阴影样式从 iOS/Android 搬到 OpenHarmony 之前,先确认你用的 RN 适配版本采用的是哪套渲染管线,然后针对性查阅该版本对 shadow 系列属性的支持说明。这个工作不值得跳过,跳过了后面大概率会花几倍时间来排查“阴影怎么不生效”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. TodoList 核心功能拆解与状态管理实现
2.1 任务数据模型与本地持久化
TodoList 看着简单,但真正写起来需要认真拆层。我把它分成三个层次:数据模型层、状态管理层、本地持久化层。
数据模型方面,任务对象我定义了五个字段:
typescript复制interface TodoItem {
id: string; // 唯一标识,用 Date.now() + 随机数拼接
title: string; // 任务内容
completed: boolean; // 完成状态
createdAt: number; // 创建时间戳,用于排序
contactPhone?: string; // 可选:联系人电话,为电话能力预留
imageUri?: string; // 可选:附件图片路径,为图片裁剪联动预留
}
状态管理没有引入 Redux 或者 MobX,这个项目规模用 Context + useReducer 就够了。三个业务 action 分别是:添加任务、切换完成状态、删除任务,外加一个从本地存储恢复数据的初始化 action。useReducer 最大的好处是状态变更逻辑非常集中,所有修改都走 reducer 里的 case,方便审查也方便测试。
持久化选的是 @react-native-async-storage/async-storage,RN 生态做本地 KV 存储的事实标准。加载时机放在应用启动后的 useEffect 里,整个任务数组用 JSON.stringify 序列化后写入,读取时再 JSON.parse 回来。这个方案在小数据量场景下表现足够好,没必要上 SQLite 或者 WatermelonDB。值得提醒的是 AsyncStorage 的读写是异步的,如果你在应用冷启动后就立刻读取,UI 渲染和存储加载会有一个短暂的竞态窗口。我的做法是在存储层封装一个独立的 storage.ts 模块,加载完成后通过状态分发整体替换,同时用一个 isReady 字段控制列表区的渲染时机,避免出现“闪烁一下空白”的体验。
2.2 用 FlatList 渲染任务列表
列表渲染选择了 FlatList 而不是 ScrollView 套 Map。原因很简单:FlatList 做窗口化渲染,只挂载可视区附近的 item,而 ScrollView 会把所有子节点一次性渲染出来。任务数量超过二三十个时,两者的性能和内存占用差距就非常明显了。重点是,这个窗口化计算发生在 JS 侧,OpenHarmony 适配层同样受益,所以这个选择在目标平台上依然成立。
核心配置大概是这样的:
jsx复制<FlatList
data={tasks}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TaskCard
task={item}
onToggle={toggleTask}
onDelete={deleteTask}
/>
)}
contentContainerStyle={styles.listContent}
ItemSeparatorComponent={Divider}
showsVerticalScrollIndicator={false}
extraData={tasks}
/>
这里有两个坑值得单独拿出来说。
第一个坑是 keyExtractor 必须返回稳定且唯一的字符串。如果 id 生成逻辑有问题,或者多个任务共用了同一个 key,列表复用机制会出现错乱。我遇到过的典型症状就是阴影样式“串台”——某张卡片已经滑出屏幕,它残留的阴影却留在了另一张卡片的位置上。排查这种问题最好的办法不是盯着样式看,而是检查 key 的唯一性。
第二个坑是 extraData。FlatList 默认是 PureComponent 的优化逻辑,如果 renderItem 依赖外部的状态数据并且没有显式传入 extraData,你点了“完成”按钮之后列表可能纹丝不动。这个症状非常容易误判为“阴影没刷出来”,实际只是列表没有触发重渲染。我在第一次集成时就在这里多花了半天时间,反复重启应用,最后才意识到是 extraData 漏掉了。
2.3 添加、完成、删除任务:不可变状态更新的正确姿势
交互逻辑我遵循一个原则:组件只负责表达意图,状态变更永远走 reducer。
添加任务是最朴素的流程:顶部输入框加提交按钮,提交前用 trim() 去掉首尾空格,空内容直接拦截。新任务统一插到数组头部,因为人眼对列表顶部的变化更敏感,配合新卡片从无到有的浮现动画,用户得到的操作反馈会比插到尾部强很多。
完成状态的切换需要注意不可变更新模式:
typescript复制case 'TOGGLE':
return state.map(item =>
item.id === action.id
? { ...item, completed: !item.completed }
: item
);
这里不是玄学,而是 React 渲染判定的基础。React 靠引用比较决定要不要重新渲染,如果你直接写 item.completed = !item.completed,对象的引用没变,FlatList 很可能不刷新。TaskCard 上完成后的视觉变化——文字变灰、背景变暗、阴影减弱——全部依赖正确的重渲染。我第一次写的时候偷懒直接在原对象上改属性,结果卡片状态切了但 UI 不动,还以为是 OpenHarmony 适配的 bug,最后发现是自己违背了不可变原则。
删除操作我加了轻量确认弹层,用 Alert.alert 实现。RN for OpenHarmony 对基础弹窗的适配比较完整,按钮点击回调也正常。不过要注意部分早期适配版本弹窗按钮的样式可能与 iOS/Android 有细微差异,建议真机提前确认,不要拖到提测阶段才发现。
3. 任务卡片阴影效果完整实现
3.1 阴影渲染原理:iOS / Android / OpenHarmony 三端差异
这部分是整个项目里最有学习价值的地方。任务卡片的阴影效果,看着就是几个 style 属性,背后却是三套完全不同的渲染机制。
iOS 上,RN 的阴影样式直接映射到 Core Animation 层的 shadowPath、shadowOffset、shadowRadius、shadowOpacity。这是真正的画布阴影,视觉平滑,支持透明背景,可以多层叠加。本质上,iOS 在绘制视图时会基于图层路径做一次离屏渲染,把模糊后的形状作为阴影合成到最终画面里。
Android 上则是另一回事。从 Lollipop 开始引入 elevation(高度)概念后,阴影被理解为“物体抬离纸面的高度”,由系统原生绘制。RN 的适配策略是:Android 只认 elevation,shadowColor、shadowOffset 这些属性在 Android 上是无效的。所以严格来说,RN 生态里的阴影从来就不是一套 API 通吃三端,而是 iOS 用 shadow 系列、Android 用 elevation 的双轨制。
OpenHarmony 又有点特殊。ArkUI 原生有自己的一套阴影能力,通过 boxShadow 或者 shadow 样式属性来设置,支持模糊半径和颜色等参数。RN for OpenHarmony 的适配层做的是尽量抹平差异,按 RN 的 API 约定去解析样式。我的实测结论是,shadow 系列属性和 elevation 在 OpenHarmony 上的表现介于 iOS 和 Android 之间——适配层会把 shadow 系列映射到系统阴影能力,elevation 的生效程度则取决于渲染器版本。
所以我的最终策略是:shadow 系列主用,elevation 兜底,三端尽量保持视觉一致。这样写出来的样式即使某个平台某个版本对某个属性覆盖不全,也不至于出现“完全无阴影”的极端情况。
3.2 阴影样式参数详解与最佳配置
TaskCard 组件里跑得最稳的阴影配置长这样:
javascript复制const styles = StyleSheet.create({
card: {
backgroundColor: '#ffffff',
borderRadius: 16,
padding: 16,
marginVertical: 8,
marginHorizontal: 12,
// 主用阴影属性(iOS / OpenHarmony 适配层)
shadowColor: '#1E2A3A',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.12,
shadowRadius: 10,
// Android 兜底
elevation: 4,
},
});
逐个参数拆开讲讲我调参时的理解。
shadowColor 我用了偏深的蓝灰色 #1E2A3A,而不是纯黑。纯黑阴影在浅色背景下容易显得“脏”,尤其是多个卡片叠放时会产生一种雾蒙蒙的感觉。用带一点色相的阴影色,视觉上会干净不少。这个细节是我对比了好几组颜色之后确定的,效果差异肉眼可见。
shadowOffset 表示阴影方向的偏移量。height: 4 相当于光源在正上方,阴影向下投射 4 个逻辑像素;width: 0 保证水平方向不偏移,卡片看起来是正的,不会像歪了。这里需要特别注意,在部分较早的 RN for OpenHarmony 适配版本中,{ width: 0, height: 4 } 这个对象的解析可能存在边界 bug,width 为 0 会被某些逻辑当成 falsy,导致只读取了 height 而忽略 width。遇到阴影表现异常时,可以试试把 width 改成 1 再做对比测试,这是一个很有效的排查手段。
shadowOpacity 是阴影不透明度。0.12 是我反复试出来的值:从 0.05 起步,每次加 0.02,到 0.12 时层次刚好,再往上走阴影就开始显脏。这个参数直接控制阴影在视觉上的强度,宁小勿大。
shadowRadius 是模糊半径。10 给到了一个比较柔和的弥散效果,边缘过度自然。要注意的是 radius 过大会让阴影显得像一层“雾气”,和卡片本身的联系变弱;过小又会变成生硬的描边。10 是相对靠谱的中间值。
elevation 实际上是 Android 的高度概念,数值 4 折中偏浅。在 OpenHarmony 适配版本里,我的经验是保留它作为兜底不会出错,但不要指望它作为唯一的阴影方案,因为不同版本对 elevation 的映射策略持续在变。
3.3 阴影与卡片状态联动:完成、按压、焦点三态平滑切换
TodoList 的灵魂在于卡片状态切换的反馈,我在阴影上也做了三个状态的联动。
默认状态就是上面的干净白色卡片加标准阴影。完成状态时,标题文字变灰、背景微微变暗,同时阴影明显减弱——shadowOpacity 降到 0.04,elevation 降到 1——整张卡片视觉上“沉下去”,这是一眼能感知到的状态变化。按压状态则模拟物理反馈,阴影缩小加上卡片轻微内移,看起来像被手指按下去。
jsx复制<Pressable
onPress={() => onToggle(task.id)}
style={({ pressed }) => [
styles.card,
task.completed && styles.cardCompleted,
pressed && styles.cardPressed,
]}>
卡片的 cardPressed 样式我定义为 shadowOffset 的 height 从 4 降到 2,shadowRadius 从 10 降到 6,配合一个 translateY 上移 1 个像素。这样按下去时阴影会自然收紧,释放后恢复,比单纯改变背景色要真实得多。
这个交互在 iOS、Android、OpenHarmony 三端都能正常工作,因为 pressed 状态最终映射到触摸事件。不过我在 OpenHarmony 真机上发现,使用较大 blur 半径时,按压动画在低端设备上会有可察觉的掉帧。原因也很直接,阴影计算量跟模糊半径正相关,半径越大,渲染压力越大。解决思路是在 pressed 状态下主动降低 shadowRadius,减少计算量,动画就能明显流畅起来。
还有一个性能陷阱值得单独提醒:不要频繁动态创建新的 style 对象传给组件。React 每次渲染都会对 style 做 diff,如果每次 render 都生成新对象,在 OpenHarmony 的部分设备上会表现为阴影闪烁。正确做法是把状态相关的样式组合提前定义成静态对象,例如把 cardCompleted、cardPressed 都通过 StyleSheet.create 固化下来,运行时直接按状态取用。
3.4 阴影性能优化:从 shadowPath 到渲染减负
阴影最怕的永远是性能。复杂模糊半径加高频动画,帧率说掉就掉。这里分享几个实测有效的减负手段。
第一个思路是给 iOS 系渲染提供 shadowPath。如果卡片形状是简单的圆角矩形,提前指定 shadowPath 可以避免系统每次重新计算阴影轮廓。RN 里可以借助 View 的 onLayout 拿到卡片尺寸,再手动构造圆角路径传入对象。OpenHarmony 适配层目前对 shadowPath 的映射支持还不够全面,但加上去对 iOS 没有副作用。
第二个思路是控制阴影的“覆盖范围”。列表连续滚动时,卡片的 shadowRadius 过大就会出现阴影互相交叠,视觉效果会像沥青路面。我最终把 marginVertical 设为 8,shadowRadius 控制在 10,既保证有层次感,又避免阴影区域大面积重叠带来看不清的脏乱感。这个平衡点在真机上反复对比过。
第三个思路是减少 elevation 的层级差。Android 和 OpenHarmony 的 elevation 会影响 z 轴的绘制顺序,多个卡片高度差过大时,系统要为每一层分别计算投影,开销成倍增长。同一列表里尽量保持卡片高度一致,只在交互状态切换时短暂改变,是压低渲染成本的有效做法。
4. OpenHarmony 适配实战:踩坑记录与排查方案
4.1 阴影不显示的典型原因排查
我在 OpenHarmony 适配过程中遇到最多的问题就是“阴影不显示”。典型表现是卡片有背景色、有圆角,但阴影完全消失,卡片像贴平在纸面上。这里有几条排查顺序,我建议按顺序走。
第一,先确认适配版本。RN for OpenHarmony 的版本差异对样式兼容影响很大,旧版本上 shadowOffset 对象解析可能不完整,所以不只是阴影不显示的问题,连部分阴影属性都会出现异常表现。这里我的建议是在工程里固定适配版本号,不要每次拉最新依赖,避免静默升级带来的行为漂移。
第二,检查父容器是否裁剪。这是最隐蔽的坑。如果 TaskCard 的外层容器设置了 overflow: 'hidden',阴影会被父容器裁剪掉。列表场景下 FlatList 默认不会裁剪,但自定义 contentContainerStyle 时如果带上了 overflow,阴影就没了。排查时先检查卡片所有父级容器的 overflow 取值。
第三,检查兄弟节点遮挡。RN 的 zIndex 在 Android 上受 elevation 影响,OpenHarmony 上也有类似层级约定。如果阴影被同一层级的其他卡片覆盖,表现就是“这张卡没有阴影”。这个场景我给卡片显式加 zIndex: 1 就能解决,但要注意 FlatList 复用列表项时的层级一致性。
第四,检查半透明背景色。TaskCard 如果设置了半透明背景,阴影会被背景色吸收一部分,视觉效果上“阴影变淡”甚至“消失”。业务里确实需要半透明背景时,记得在真机上把 shadowOpacity 稍微调大重新验证。
4.2 集成 rn 调用电话功能:点击任务卡片直接联系联系人
TodoList 做到中途,我决定加一个贴合实际场景的功能:任务卡片绑定联系人电话,点击号码直接拉起拨号。这个功能在 RN 生态里最轻量的做法不是引第三方库,而是直接用 Linking 模块。
核心代码非常简单:
javascript复制import { Linking, Alert } from 'react-native';
const handleCall = async (phone: string) => {
const url = `tel:${phone}`;
try {
const supported = await Linking.canOpenURL(url);
if (supported) {
await Linking.openURL(url);
} else {
Alert.alert('提示', '当前设备不支持拨号功能');
}
} catch (error) {
Alert.alert('错误', '拨号失败,请检查号码格式');
}
};
这里有三个实际经验想分享。
一是 canOpenURL 并不是万能保险。iOS 上有 scheme 白名单机制,OpenHarmony 上则要确认系统里有没有注册 tel scheme 的处理者。我在 OpenHarmony 真机上测试时 canOpenURL 返回 true,openURL 也能正常拉起系统拨号面板,但这个行为依赖系统默认应用配置,某些精简版系统或特定设备上可能没有预装拨号应用,这时必须有降级提示,不能直接 openURL 然后不处理异常。
二是电话号码在 UI 上的呈现方式直接影响误触率。我把“联系”两个字放在卡片右下角,用文字链接样式展示,而不是放一个大按钮。这样视觉层级后台化,用户不会在滑动列表时误触拨号。当 contactPhone 不存在时,这个区域渲染成空占位符,保持卡片高度稳定,避免因高度跳动引发的阴影位置错乱。
三是权限问题。iOS 端拨号一般不需要额外权限,但一些 Android 版本对 tel scheme 的拦截逻辑并不统一。OpenHarmony 的拨号能力属于系统能力调用,在项目配置 module.json5 中可能需要声明对应的系统能力类型。建议在集成式测试阶段就把这个配置确认好。
4.3 图片裁剪 rn 库接入:平台分流与功能降级
另一个值得记录的是图片裁剪能力。TodoList 卡片上允许附加一张和任务相关的图片,比如购物清单里的商品照片,选择后自动裁剪成卡片封面。
RN 生态里做图片裁剪最常用的库是 react-native-image-crop-picker,它把相册选择、拍照、裁剪、压缩统一封装好了。但我在 OpenHarmony 上接入时很谨慎,因为这个库的原生代码基于 iOS/Android 实现,OpenHarmony 侧需要原生模块层有对应实现,不是简单地 npm install 就能无缝跑起来。
我采用的方案是在 JS 层抽象一个 ImagePickerService 接口,平台实现内部动态隔离。iOS 和 Android 走 react-native-image-crop-picker,OpenHarmony 暂时走系统相册选择能力,拿到图片后手动压缩到目标尺寸来模拟裁剪效果:
typescript复制const pickAndCropImage = async (): Promise<string | null> => {
if (Platform.OS === 'harmony') {
// 简化示例:使用 OpenHarmony 系统相册选择能力
const result = await HarmonyPicker.select({ maxCount: 1 });
const compressed = await compressImage(result.uri, { width: 480, height: 360 });
return compressed.path;
} else {
const image = await ImageCropPicker.openPicker({
width: 480,
height: 360,
cropping: true,
});
return image.path;
}
};
这个“接口抽象 + 平台分流”的模式在跨端项目里非常值得推广。它把不确定性挡在接口后面,即便 OpenHarmony 侧的能力还没完全对齐,业务层也能维持稳定。等将来社区适配的图片裁剪库在 OpenHarmony 上成熟了,我只需要替换内部实现,调用方代码一行都不用改。
图片处理完的 UI 呈现和阴影也有联动。带图片的任务卡片,我把 shadowOffset 的 height 从 4 降到 2,因为附件的视觉重量本身比较大,阴影太强会喧宾夺主。这个细节是调 UI 时一点点试出来的,记录在这里供参考。
5. 项目扩展方向与个人体会
5.1 后续还能玩什么:动效增强与模块化收纳
这个 TodoList 项目的基础闭环已经完成了,但我觉得还有几个方向值得继续折腾。
一是动效增强。目前的阴影状态切换是瞬时的,下一步可以给 shadowOpacity 和 shadowRadius 加上 Animated 过渡,让按压和完成切换更顺滑。需要注意 OpenHarmony 适配层对 Animated 的支持情况,建议先小范围测试再铺开。
二是组件模块化收纳。TaskCard 目前还是单一组件,可以拆成 ShadowCard 基础容器,把阴影逻辑独立封装成可复用组件,这样不仅是 TodoList,任何卡片类场景都能直接使用。这个抽离动作可以让阴影样式只维护一份,后续调参也方便。
三是数据层面扩展。把 AsyncStorage 换成真正的数据库,或者增加多设备同步能力,让 TodoList 具备更完整的业务价值。不过这一步开始涉及原生模块的深度适配,需要单独评估工作量。
5.2 我最后想说的话
真实体验下来,RN for OpenHarmony 目前还谈不上“开箱即用”,很多细节要自己踩、自己试。但恰恰是这个过程,把 React Native 从“黑盒调 UI”变成了“理解渲染原理”的修炼场。任务卡片的阴影效果之所以值得作为练手核心,就是因为它把样式系统、渲染管线、平台差异、性能优化全部串在了一条链路里,做完一遍收获非常大。
如果你也想拿这个项目练手,我给三条实在的建议。
第一,从一开始就把真机测试纳入日常开发节奏。模拟器和真机的渲染差异在阴影、圆角这些视觉效果上比 iOS/Android 更明显,不要等最后才上真机。
第二,不要把 iOS/Android 的视觉细节默认迁移到 OpenHarmony。同一份代码,iOS 上阴影柔和自然,OpenHarmony 真机偶尔偏硬,这不一定是代码问题,而是渲染实现本身的差异。此时微调 shadowRadius 和 shadowOpacity,比硬找代码 bug 有效得多。
第三,多利用社区的适配兼容清单。很多踩坑经验、组件兼容状态都散落在 SIG 仓库的 issue 和讨论区里,遇到问题先搜“react native harmony + 关键词”,大概率比看官方文档收获更大。拿这个最小闭环项目趟一遍,你就知道 RN 在 OpenHarmony 上能走到多远了。
