上个月接到一个需求:把团队现有的动漫社区应用 AnimeHub 移植到 OpenHarmony 设备上,第一个要交付的页面就是收藏页面。这个页面看起来不复杂——展示用户收藏过的动漫列表,支持取消收藏、下拉刷新、点击进详情页。但真正动手之后才发现,RN for OpenHarmony 这六个字背后,藏着一整条链路的问题:选型、环境、桥接、性能、生态差异,每一步都可能翻车。
这篇文章我就把自己做 AnimeHub 收藏页面这段实战经历完整记录下来,包括为什么选 React Native 而不是 ArkTS 原生,收藏页面的需求到底该怎么拆,RN 工程怎么跑上 OpenHarmony,数据层和 UI 层怎么落地,以及真机调试时踩到的坑。如果你正准备在 OpenHarmony 上做 RN 开发,或者单纯好奇这条技术路线现在到底能不能用,这篇应该能帮你省不少时间。
1. 选型复盘:为什么是RN而不是ArkTS原生开发
1.1 OpenHarmony应用的三种主流开发路线
先说结论:在 OpenHarmony 上做应用,目前主要就三条路——ArkTS/ArkUI 原生、Flutter、React Native。每条路背后都有一批人在踩坑,也都有各自适合的场景,但很多人一开始就陷入了"sologan 之争":原生党说跨平台性能差,跨平台党说原生开发慢。实际做项目的时候,这个争论毫无意义,你只需要看自己的约束条件。
ArkTS/ArkUI 是 OpenHarmony 官方主推的原生方案,性能确实最好,平台能力也是最新的,菜单、弹窗、安全控件这些系统能力都是第一时间支持。但问题是,ArkTS 这语言太新了,团队里没几个人写过,从零开始学再加业务开发,工期根本压不住。而且你如果有 Web 端或者安卓端的存量业务,ArkTS 一套代码完全没法复用。
Flutter 在 OpenHarmony 上也有社区适配,渲染性能很能打,Dart 语言也相对好上手。但 Flutter 的 OpenHarmony 适配版本滞后比较明显,很多插件都要自己改,遇到平台能力缺口的时候,排查问题的难度比 RN 更高。
React Native 这条路最初我是犹豫的,因为它在安卓和 iOS 上的老架构性能一直被诟病。但后来我仔细盘了一下自家团队的情况:前端工程师占大头,React 技术栈非常熟,公司还有一套跑在安卓上的 RN 业务代码可以抽取复用。RN for OpenHarmony 虽然不是官方出品,但社区迭代速度肉眼可见,已经有完整的组件映射和桥接方案。综合下来,RN 是这三条路里最能"用现有团队、保现有资产、控交付周期"的路线。
1.2 RN for OpenHarmony 的生态现状摸底
决定走 RN 之后,我先花了两天时间把社区适配情况摸了个底。目前主流的适配项目是 react-native-openharmony,它通过 OpenHarmony 侧的组件映射和桥接层,把 RN 的 JS 运行时、渲染指令和原生组件翻译成鸿蒙的 ArkUI 组件。也就是说,你写的<View>最后会映射成 ArkUI 的Column或者Stack,<Text>映射成Text,<Image>映射成Image。
需要冷静看待的是它的版本跟进速度。RN 官方现在都推到 0.78 了,但 OpenHarmony 适配版还停留在 0.72 左右的基线,新架构 Fabric 的适配也只在早期验证阶段。这意味着你在 npm 上看到的最新版 RN 组件,很多都没法直接用,得在 0.72 这个生态里找兼容版本。
不过对我这个 AnimeHub 收藏页面来说,这个限制影响不大。我要用的核心组件无非就是 View、Text、Image、FlatList、TouchableOpacity,这些都是最基础的映射组件,适配成熟度已经很高,不用整天跟原生层缠斗。真正要小心的是一些需要原生能力支撑的三方库,比如图片缓存、网络请求、本地存储,这部分我后面单独讲。
1.3 当时做的技术决策清单
为了让自己事后不后悔,我把决策依据写成了清单,这里直接分享给你参考:
- 团队技术栈:前端 8 人,有 5 人熟练 React/RN,ArkTS 只有 1 人看过文档。选 RN 意味着全员可以立刻进入开发状态,选原生意味着要先花 3-4 周培训。
- 业务复用:AnimeHub 已经有 Web 端管理后台,收藏列表的数据结构、图片裁剪规则、分页协议都是现成的,RN 可以直接复用相同的接口设计。
- 页面性能要求:收藏页是一个典型的长列表页面,不涉及高频刷新的复杂动画。RN 的 FlatList 在优化到位的情况下,完全能保障每秒 60 帧的滚动体验。
- 交付周期:整个 AnimeHub 项目第一版要求 6 周内出可演示的 Demo,收藏页面只是其中一个模块。RN 的开发效率在这里是决定性优势。
- 生态风险:RN for OpenHarmony 目前还不支持新架构的完整能力,所以我把"新架构落地"标记为长期观察项,而不是当前阻塞项。
2. 需求拆解:收藏页面到底要解决哪些问题
2.1 收藏场景的业务闭环分析
收藏页面的第一个坑,就是把它当成一个单纯列表页来做。实际上,收藏是一个典型的业务闭环,入口和出口都比表面上多。用户从动漫详情页点击"收藏"按钮,数据写入收藏列表;然后这个列表被带到收藏页面展示;用户在这里再次点击"取消收藏",数据要从列表移除。这个闭环里还夹着登录态、收藏数统计、服务端同步等问题。
我在 AnimeHub 里把收藏闭环拆成了四个环节:收藏动作(详情页写入)、数据存储(本地和服务端双写)、列表呈现(收藏页拉取和渲染)、取消动作(用户操作和状态回写)。每个环节都有自己的异常场景:写入失败怎么办?服务端同步失败本地要不要保留?取消收藏时接口超时怎么提示?
如果不把这个闭环想清楚,收藏页很容易做成"只读死列表",连基本的操作反馈都没法保证。
2.2 交互细节:用户真正在意的是什么
做开发的人往往盯着功能能不能跑通,但用户在意的是手感。我访谈了团队里几个重度二次元用户,总结出收藏页最在意的四个交互点:
- 点击卡片任意区域都能进详情,但收藏按钮区域不能误触跳转,必须做事件拦截。
- 取消收藏要有即时反馈,按钮状态从"已收藏"变回"未收藏",最好配合一个缩出的动画,让用户明确感知到"操作生效了"。
- 空状态不能是一片白板,要有插画、文案和跳转引导,把用户从"我什么都没有"的低落情绪里拉出来。
- 下拉刷新不要重置滚动位置,用户看到第 30 条的时候刷新一下,结果跳回顶部,谁都会崩溃。
这些细节看着不起眼,但决定了这个页面到底是一个"能用"的列表,还是一个"好用"的收藏中心。
2.3 数据模型设计与详情页联动
数据模型是整个页面的地基。我在 AnimeHub 里定义了这样的结构:
interface AnimeItem { id: string; title: string; coverUrl: string; lastEpisode: number; // 用户看到的最新一集 totalEpisodes: number; // 动漫总集数 tags: string[]; // 类型标签,如"热血""科幻" isFavorite: boolean; // 收藏状态 updatedAt: number; // 服务端最近更新时间,用于排序 }这里有个关键决策:lastEpisode必须单独存,不能每次从详情页现查。因为收藏列表要展示"你追到第几集了"这个信息,如果用户详情页只看了一集,回到列表页它必须立刻反映出来,不能等重新拉接口。这个字段会让收藏页和详情页产生联动:详情页更新了观看进度,收藏页下次进入时要同步显示。
分页参数也在这个环节定下来,我用的是基于游标的分页协议:每页 20 条,响应里带有nextCursor,没有nextCursor就代表没有下一页了。相比传统的page/pageSize,游标分页在收藏数据频繁增删的时候更稳,不会因为新插入的数据导致翻页重复或遗漏。
3. 环境搭建与工程联调:让RN跑在开源鸿蒙上
3.1 项目初始化与依赖版本选择
先说版本。RN for OpenHarmony 的适配版本和 RN 官方版本是绑定的,我用的组合是:
{ "react-native": "0.72.15", "react-native-openharmony": "0.72.5", "@react-native-async-storage/async-storage": "1.19.3", "react-native-gesture-handler": "2.12.1" }这里要特别强调:不要直接安装最新版的 RN。我见过有人用npx react-native init直接拉了一个 0.76 的工程,然后去配 OpenHarmony 适配包,结果依赖冲突排了一下午。正确做法是先看 react-native-openharmony 官方仓库的 version mapping 表,它明确写了支持哪个 RN 版本,然后按那个版本初始化工程。
初始化完成之后,按照仓库的集成文档,把 OpenHarmony 原生工程放到harmony子目录下,同时配置metro.config.js让 Metro 打包器能找到鸿蒙侧的入口文件。这个入口文件路径和安卓的不一样,我一开始漏配了,Metro 直接报Unable to resolve module @react-native-openharmony/turboModule,排查了很久才发现是路径匹配的顺序问题。
3.2 桥接层基础:JS侧与OHOS原生模块的通信逻辑
说一个很多初学者不理解的概念:为什么 RN 在 OpenHarmony 上跑,还需要一个"桥接层"?因为 RN 的 JS 引擎本身没有能力直接调用 OpenHarmony 的系统 API,比如读取本地文件、获取设备信息、弹一个系统的 Toast。这些能力都在 OHOS 原生侧,JS 侧必须通过桥接模块发一个消息过去,原生侧处理完再回调结果。这个模型和安卓上的"Bridge"本质上是一样的。
在 react-native-openharmony 里,桥接模块用 ArkTS 写在原生工程里。比如我写了一个获取设备型号的模块:
import { TurboModule } from '@ohos/ReactNativeBridge'; export class DeviceInfoModule extends TurboModule { getDeviceModel(): string { return deviceInfo.marketName; } }JS 侧调用的时候:
import { TurboModuleRegistry } from 'react-native'; const DeviceInfo = TurboModuleRegistry.get('DeviceInfoModule'); const model = DeviceInfo.getDeviceModel();理解了这个机制,后面遇到"某个 RN 原生模块在 OHOS 上不能用"的时候,你就知道应对思路了:自己写一个桥接模块,把系统能力暴露给 JS。收藏页面里"拨打电话"和"读取本地缓存"就是这么干的,后面踩坑章具体讲。
3.3 一次成功的构建过程示范
搭建环境最容易卡壳的就是构建。我记录了一次完整成功的构建链路,你可以照着走:
- 安装 DevEco Studio并配置 OpenHarmony SDK,我用的是 API 10 的 SDK。
- 把
harmony原生工程导入 DevEco Studio,这个工程在项目初始化/集成步骤里生成,不要手动新建。 - 配置
entry模块的module.json5,把包名、版本号和应用图标按实际项目改掉。 - 配置签名。真机调试必须签名,模拟器调试可以跳过。我一开始图省事没配签名,结果 HAP 包装不上真机,报错信息又很隐晦,浪费了半小时。
- 执行 hvigor 构建。DevEco Studio 里直接点运行按钮即可,它会自动编译原生侧、打包 HAP、然后通过 hdc(OpenHarmony 的调试工具,类似 adb)安装到设备。
- 启动 Metro 服务。在项目根目录执行
npm start,Metro 会监听 8081 端口,提供 JS bundle 给设备加载。 - 在真机上确认 Metro 连接。进入 App 后,开发者菜单里检查 bundle 来源是不是你本机 IP:8081。第一次连接必须确认这一步,否则页面渲染不出来,大概率是网络或来源配置问题。
我建议把以上步骤保存成一个BUILD.md文档,因为团队成员各自搭建环境时,卡的点五花八门,但基本都是这里面的某一环节。
4. 数据层落地:收藏列表的存储、分页与状态同步
4.1 本地持久化选型与封装
收藏数据需要本地持久化,原因很朴素:如果用户断网打开 App,他至少还能看到上次缓存的收藏列表,而不是一个空白页。RN 生态里最常用的持久化方案是 AsyncStorage,但它在 OpenHarmony 上不是开箱即用的。
我踩过的坑是:@react-native-async-storage/async-storage这个包虽然声明支持 OpenHarmony,但我在真机上测试时发现,部分版本在系统重启后数据会丢失,怀疑是底层实现没有正确调用 Preferences 的 flush 机制。后来我果断换方案:自己写一个桥接模块,底层用 OpenHarmony 的 Preferences 存储 API。这个库是同步落盘的,可靠性比 AsyncStorage 的异步写入更稳。
封装后的调用方式很简单,JS 侧不需要关心底层逻辑:
// storageHelper.ts import { NativeModules } from 'react-native'; const StorageBridge = NativeModules.OHOSStorageBridge; export const StorageHelper = { async getItem(key: string): Promise<string | null> { return StorageBridge.getItem(key); }, async setItem(key: string, value: string): Promise<void> { return StorageBridge.setItem(key, value); }, async removeItem(key: string): Promise<void> { return StorageBridge.remove(key); }, };4.2 分页拉取与"加载中/加载失败"状态机
收藏列表要有分页拉取,但真正重要的是把状态定义清楚。我见过太多人只在 state 里放一个data数组,然后loading一个布尔值,结果一遇到加载失败就卡在"没有提示也没有重试"的死角。
我在 AnimeHub 收藏页里定义了完整的状态机:
type FetchState = | { status: 'idle' } | { status: 'loading'; isInitial: boolean } | { status: 'success'; data: AnimeItem[]; nextCursor: string | null } | { status: 'error'; message: string; retryable: boolean };isInitial字段用来区分首屏加载和分页加载:首屏加载显示全屏 Loading,分页加载只在列表底部显示一个"加载中"的行。retryable字段决定错误提示是"重试"还是"返回上一页"。这个状态机写清楚之后,后续任何 UI 联动都变得非常简单——只需要根据status分支渲染对应组件。
分页参数我会在每次请求时带上cursor,服务端返回nextCursor。收藏数据本身是高频变动的,用户上一秒取消收藏,下一秒服务端列表就变了,用游标分页可以避免分页错位。
4.3 取消收藏的乐观更新与失败回滚
取消收藏这个动作,常见的做法是等接口返回成功后再更新列表。但这在弱网环境下体验很差,用户点了取消,转圈两秒,才有反应,而且要是接口挂了,用户以为取消了,其实没有。
我采用乐观更新:点击按钮的瞬间,先把本地列表里该项的isFavorite置为 false,并从列表中移除,然后发起接口请求。请求成功就完事;请求失败,就把该项插回原位,同时弹 Toast 提示"取消失败,请检查网络"。
代码结构大概是这样:
const removeFavorite = useCallback(async (item: AnimeItem) => { // 记录原位置,用于失败回滚 const prevData = dataRef.current; const removedIndex = prevData.findIndex((d) => d.id === item.id); // 乐观更新 updateData((d) => d.filter((it) => it.id !== item.id)); try { await api.removeFavorite(item.id); } catch (err) { updateData((d) => { const next = [...d]; next.splice(removedIndex, 0, item); return next; }); Toast.show('取消失败,请检查网络'); } }, []);关键点是"记录原位置",因为分页列表可能已经滚动过了,直接插回数组末尾位置就不对了。用removedIndex能保证数据回到它原来所在的位置,视觉上不会产生跳跃感。
5. UI实现:把收藏卡片做出"手感"
5.1 卡片布局的关键取舍
收藏卡片我用的是单列大卡布局,而不是双列瀑布流。原因很直接:收藏列表要展示的信息量比普通内容流大得多。卡片上要有封面、标题、追更进度、标签、最近更新时间。双列模式下,这些信息挤在一起,用户阅读效率反而下降。
单列卡片的竖向长度我控制在 120 物理像素左右,封面尺寸固定为 84x112(3:4 比例),标题最多两行截断,进度信息放在卡片底部,整个视觉重心保持稳定。这里有个细节:封面图的裁剪模式用resizeMode="cover",并且封面容器隐藏溢出部分,这样服务端返回的图片无论什么尺寸,都不会撑破卡片布局。
5.2 FlatList列表性能配置细节
收藏列表理论上可以上千条,用 ScrollView 渲染是灾难,必须用 FlatList。但默认配置的 FlatList 在低端 OpenHarmony 设备上滚动还是会卡顿,需要针对性地调参。
我最终采用的配置是:
<FlatList data={favorites} keyExtractor={(item) => item.id} renderItem={renderCard} getItemLayout={(_, index) => ({ length: CARD_HEIGHT, offset: CARD_HEIGHT * index, index, })} initialNumToRender={6} maxToRenderPerBatch={8} windowSize={7} removeClippedSubviews onEndReachedThreshold={0.3} onEndReached={loadMore} ListEmptyComponent={EmptyState} ListFooterComponent={FooterLoading} showsVerticalScrollIndicator={false} />逐项说下理由:getItemLayout最关键,它让 FlatList 可以直接计算每个 item 的位置,省去动态测量,滚动跳转性能提升非常明显。initialNumToRender={6}控制首屏渲染数量,避免一次性渲染 20 条导致首帧卡顿。maxToRenderPerBatch={8}控制每帧最多渲染 8 个新 item,避免长列表滚到底部时一次性渲染大量组件造成掉帧。windowSize={7}是渲染窗口的高度倍数,过大会浪费内存,过小会导致快速滚动时白屏,7 是实践下来的平衡值。
5.3 空状态与骨架屏的取舍
很多团队喜欢一上来就做骨架屏,但骨架屏只对"快速渲染内容"的场景有意义。收藏页面的首屏数据如果 200ms 内能返回,骨架屏一闪而过,用户根本注意不到,反而浪费开发工时。我最终只做了全屏 Loading 和空状态两个互补组件。
空状态组件我花的心思最多。收藏是用户主动积累的内容,如果收藏列表为空,用户情绪通常是"我怎么没有收藏东西",这时文案要正向引导,而不是冷冰冰一句"暂无数据"。我用的文案是"喜欢的番剧还没收藏哦",配一个手绘风格的看板娘插画,下面一个"去发现好番"按钮。这个按钮结合了收藏页面的深层需求——把用户带回推荐流,重新激活收藏业务闭环。
5.4 交互动效实现路径
取消收藏的动效,我用的是 RN 内置的Animated库,没有引入额外的动画框架。原因很简单:一个缩移动效而已,引入 reanimated 还要处理新架构兼容问题,在 OpenHarmony 上风险更高。
动效逻辑是:用户点击取消按钮时,卡片先做一次透明度淡出和缩放(从 1 缩到 0.85),同时卡片 X 轴向右偏移 24 个像素,然后才真正从数据源中移除。动画时长 150ms,不长不短,用户能感知到"操作生效"但不会觉得拖沓。
const scale = useRef(new Animated.Value(1)).current; const handleRemove = () => { Animated.parallel([ Animated.timing(scale, { toValue: 0.85, duration: 150, useNativeDriver: true }), Animated.timing(translateX, { toValue: 24, duration: 150, useNativeDriver: true }), ]).start(() => { removeFavorite(item); }); };这里有个细节:动画用useNativeDriver: true,把动画执行放到原生侧,JS 侧只发一次指令,性能远好于 JS 驱动动画。OpenHarmony 上 RN 对useNativeDriver的支持已经比较稳定,可以放心用。
6. 真机调试踩坑实录:OHOS独占的那些问题
6.1 图片加载被拦:网络安全配置的坑
第一个让我抓狂的坑是图片加载。模拟器上封面图渲染得好好的,一上真机全部变成空白图,控制台也没有报错。我一开始以为是图片地址的问题,单独在 WebView 里打开那个 URL,又能正常显示。
排查过程是这样的:先确认接口请求有没有发出去,在 Metro 面板看到网络请求是发了的,说明不是接口层的问题。接着怀疑是 RN 的 Image 组件映射异常,把图片换成本地资源测试,本地图片能显示,说明映射没坏。最后才想到是不是网络安全策略的问题——我用的封面图地址是http://开头,OpenHarmony 默认禁止明文流量。在安卓上这个策略叫 cleartext traffic,OpenHarmony 也有类似机制,需要在module.json5里配置网络安全策略。
解决方案是在entry/src/main/resources/base/profile/network_config.json里放行明文流量,或者更稳妥的办法:把所有封面图换成 HTTPS 地址。我最终选择的是强制 HTTPS,因为配置明文流量放开等于降低了全 App 的安全性,不值得为图片加载妥协。
6.2 拨打电话功能的桥接差异
收藏列表里有一个"电联催更"的入口,用户点击可以直接拨打客服电话。这个需求在安卓上写起来很轻松,RN 的Linking模块直接调Linking.openURL('tel:10086')就行。但在 OpenHarmony 上,RN 适配版的Linking模块对tel:协议支持不完全,调用后毫无反应,也没有报错。
我当时的排查思路是:先用Linking.canOpenURL('tel:10086')检测系统是否能处理这个协议,返回 false,说明系统层面不支持。这就不是 RN 的问题了,而是 OHOS 没有把tel:协议映射到电话应用。解决办法只能自己写桥接模块,调用系统 API 打开拨号界面。
ArkTS 侧的核心代码:
import { featureAbility } from '@ohos.abilityFeatureAbility'; import { wantConstant } from '@ohos.abilityWantConstant'; export function dialPhone(phoneNumber: string): void { const want = { action: wantConstant.Action.dial, uri: `tel:${phoneNumber}`, parameters: {}, }; featureAbility.startAbility(want); }JS 侧通过自建桥接模块调用,效果和原生一致。这个坑给我的教训是:在 OpenHarmony 上做 RN 开发,任何依赖系统能力的功能都要先在真机验证一遍,不能默认"安卓怎么跑这里就怎么跑"。
6.3 本地文件资源与FTP场景的适配
AnimeHub 的运营后台会把番剧封面和预告片素材同步到云存储,但也有一些素材是放在内部资源服务器上的,比如局域网内的 FTP 服务。收藏页有一步"下载缓存封面到本地"的需求,涉及到从 FTP 访问文件的场景。
OpenHarmony 访问本地文件或者 FTP 资源时,权限模型和安卓不太一样。你需要先在module.json5里声明ohos.permission.READ_MEDIA或ohos.permission.INTERNET等权限,而且运行时还要动态弹窗授权,不能在后台静默访问存储空间。
另外,如果图片组件要加载 FTP 或者自定义协议的资源,RN 的 Image 组件默认不支持ftp://这类的 URI,不能直接塞进 source 里。正确做法是先把远端文件下载到应用沙箱目录,再把本地文件路径交给 Image 渲染。我封装了一个downloadFileToCache(url)的工具函数,底层用 OHOS 的@ohos.request模块下载,完了返回本地路径。
6.4 热更新与调试端口问题
最后一个高频坑是调试连接。RN 开发最爽的是改代码后 Ctrl+S 页面自动刷新,这个能力依赖 Metro 和设备的局域网连接。但 OpenHarmony 真机的网络环境比安卓复杂,设备连的 Wi-Fi 和开发机不在同一个网段时,Metro 的localhost:8081根本访问不到。
我的习惯是启动 Metro 时强制指定 host:
npx react-native start --host 0.0.0.0同时在设备的开发者菜单里手动设置 Debug server host 为开发机的局域网 IP(形如192.168.x.x:8081)。如果设备无法直连开发机网络,还有一个备选方案:通过 hdc 做端口映射,把设备的 8081 端口映射到开发机的 8081 端口,这样设备上的 App 就可以通过localhost:8081访问 Metro 了。
这个映射写法是:
hdc fport tcp:8081 tcp:8081做完映射后再确认设备上 App 的 Debug 地址设为localhost:8081。热更新卡住、页面转菊花的时候,九成是这层网络问题没打通。
7. 性能优化:结合RN新老架构谈OHOS现状
7.1 老架构在OpenHarmony上的瓶颈
谈到性能优化,绕不开 RN 的新老架构对比。老架构(也就是 RN 0.72 及之前的版本)的核心问题是所有 JS 和原生通信都要经过 Bridge 消息队列,走 JSON 序列化,异步传递。你以为调用了一个原生模块,实际上这条消息要先被序列化,跨过 Bridge,再被反序列化,原生执行完,再走同样的流程回来。这个过程在低端 OpenHarmony 设备上的延迟比较明显,尤其是滚动列表中频繁触发图片加载、事件上报这种高频操作,掉帧是常态。
OpenHarmony 适配版目前基本还是基于老架构的桥接模型,所以我做收藏页时对 Bridge 调用非常克制。能合并的原生调用就合并,比如存储读取我封装成批量接口,避免每次滚动都触发一次原生异步调用。
7.2 新架构Fabric适配进度与选择建议
新架构(Fabric + JSI + TurboModule)是对老架构的彻底重构。它的核心变化有两个:一是 JS 可以直接通过 JSI 同步调用原生方法,不再需要 JSON 序列化和异步桥;二是渲染管线从 Shadow Tree 变成事件驱动的同步渲染,布局和提交效率大幅提升。直观效果是启动速度更快、列表滚动更流畅、原生模块调用开销趋近于零。
但 OpenHarmony 适配版的新架构还处于早期验证阶段,社区主线版本都还没把 Fabric 完整跑通。我当时的评估是:生产环境继续用老架构,新架构作为技术调研跟进。如果项目对列表性能有极端要求,优先通过优化 FlatList 配置、图片缓存和减少 Bridge 调用来解决,而不是铤而走险切换新架构。
7.3 列表流畅度的几项实测优化
最后列一下我在 AnimeHub 收藏页上实测有效的几项优化,这些都是直接改在代码里的:
- 图片缓存:收藏列表封面的图片缓存是最大性能提升点。OpenHarmony 上默认的 Image 组件不带磁盘缓存,每次滚动到某个 item 都要重新下载图片。我给 Image source 包了一层自研缓存组件,下载完成后复用本地路径,实测滚动掉帧率从 45% 降到 12%。
- React.memo 包裹卡片组件:收藏列表的数据经常更新(比如取消收藏、进度变化),如果列表里 500 个 item 全都需要重渲染,那性能就废了。用
React.memo包住卡片,配合稳定的 props 引用,数据更新时只有真正变化的 item 会重渲染。 - 避免匿名函数传递:
renderItem里不要直接写onPress={() => ...},每次都创建新函数,会让 memo 失效。我把点击处理写成useCallback包裹的稳定函数,再传给子组件。 onEndReached防重复触发:分页加载时,onEndReached在边界位置很容易连续触发多次。我在loadMore里加了if (isFetching || !nextCursor) return的守卫,避免重复请求。
最终在 OpenHarmony 真机(设备是四核 A55,内存 4GB)上测试,收藏列表 500 条数据,快速滚动帧率稳定在 50-60fps,满足上线标准。说实话,这个表现比我预期的好。很多说"RN 在 OpenHarmony 上不能用"的人,大概率是没做性能优化就直接下结论了。
这个项目做下来,我的体会是:RN for OpenHarmony 这路目前确实蛮荒,很多能力要靠自己造轮子,但核心渲染链路和基础组件的成熟度已经能支撑真实业务了。收藏页面只是 AnimeHub 的第一块拼图,后面还有播放页、个人中心、评论区,每一步翻车留下的经验,都会成为下一次重建时的垫脚石。