
做 AnimeHub 这个项目之前我已经在 React Native 上摸爬滚打两年多大部分页面都是给 Android 和 iOS 做的。这次接到一个有点特殊的任务用 RN for OpenHarmony 给动漫资讯应用 AnimeHub 开发“即将上映”页面。所谓即将上映就是把未来一段时间内要播出的新番按时间线排出来用户能一眼看到哪部作品哪天开播顺手点个“想看”。这个页面看起来简单但牵扯到列表渲染、日期边界、跨端原生能力接入踩坑记录足够攒一篇实战总结了。如果你正在做 OpenHarmony 应用又对 RN 跨端方案感兴趣这篇文章应该能帮你省掉不少时间。1. 项目背景与整体设计思路1.1 为什么选 RN for OpenHarmonyAnimeHub 是一款动漫资讯类应用团队最初用 ArkTS 开发过一版首页但很快发现每做一个新页面都要在原生侧写大量 UI 逻辑而且团队里大部分人更熟悉 React 技术栈。于是我们引进了 RN for OpenHarmony核心思路是让 OpenHarmony 应用通过 React Native 运行时来渲染 JS 组件直接把现成的 React 生态搬到 OpenHarmony 上。这里有一个认知需要先建立OpenHarmony 是开源的操作系统底座RN 社区通过react-native-oh/react-native这类桥接包把 React Native 运行时移植到了 OpenHarmony 上。这带来的最大好处是业务层可以复用绝大部分现有 RN 代码不必为每个系统都以原生方式重写。对于 AnimeHub 这样页面多、迭代快的资讯应用这个优势非常明显。不过选型不能只看好处。我整理了一个简单对比维度原生 ArkTSRN for OpenHarmonyFlutter开发效率中熟悉 TS 后可上手高复用 React 生态中Dart 学习成本组件生态较少需从零写丰富但需要适配中等性能表现高中上列表需要优化高热更新难可以支持难团队技能需要 ArkTS 专精JS/TS 前端即可需要 Dart 重构最终我们选择 RN原因就两条团队现有 React 组件库能直接复用同时 RN 生态里现成的状态管理、网络请求库都能用开发速度确实快很多。如果只做 OpenHarmony 一个平台并且对性能要求极度苛刻原生 ArkTS 是更优解但这不是我们当下的场景。1.2 AnimeHub 的“即将上映”页面到底要做什么产品给的需求清单看起来并不复杂展示未来 30 天内即将上映的动漫按上映日期升序排列。每一张卡片包含封面、中文名/日文名、上映日期、热度值、点击“想看”收藏。支持下拉刷新、上拉加载分页。支持按热度/日期排序以及“7天内 / 30天内 / 全部”时间区间切换。这个页面的产品价值很明确追番用户需要一个“这周看什么、本月追什么”的入口运营也可以通过对预告内容的预热提升社区活跃度。但落到开发上它其实是一个典型的分页列表 筛选 多种交互状态的复合页面非常适合作为跨端实战样板。在动手之前我先梳理了页面状态与模块划分src/pages/upcoming/UpcomingScreen.tsx页面主组件负责组合列表、Tab 切换、加载状态。src/components/upcoming/UpcomingCard.tsx列表单元格展示单条动漫信息。src/api/upcoming.ts网络请求封装。src/store/upcomingStore.ts基于 Zustand 的状态管理。src/utils/date.ts日期格式化与“即将上映”判定。src/types/anime.ts类型定义。状态管理我选了 Zustand 而不是 Redux。原因很简单当前页面涉及的全局状态只有列表、分页、筛选条件和加载标记用 Redux 那一套 action、reducer、selector 会写大量模板代码Zustand 十几行就能搞定。当然如果后续 AnimeHub 要接用户登录、收藏同步等复杂功能Redux Toolkit 会更合适但那是后话了。1.3 页面整体架构与数据流页面数据流是一条单向链路前端请求GET /api/v1/animes/upcoming拿到{ list, page, hasMore }后存入 store页面通过订阅 store 渲染 FlatList。下拉刷新会重新请求第一页并替换list上拉加载会请求下一页并追加到list尾部。这里有一个很容易忽略的点筛选条件变化时不要直接在现有列表上做前端过滤而应该重新请求接口。因为后端可能还有热度排序、推荐加权等逻辑前端过滤会导致分页数据错乱。我们的做法是每次切换时间区间或排序方式都把page重置为 1然后走完整的请求链路。2. 环境搭建与工程初始化2.1 开发环境与版本选择RN for OpenHarmony 的环境搭建比普通 RN 工程多了一个原生侧编译环节。我用的环境大致是这样Node.js 18 或 20RN 构建工具链依赖它。OpenHarmony SDK 4.0 或 4.1需要与react-native-oh/react-native的适配版本对应。OpenHarmony 官方 IDE 或命令行工具 hvigor。一台真机或官方模拟器调试页面用。版本匹配是这个阶段最容易踩的坑。RN 0.72.x 对应的桥接包大概是 0.72.5 这个量级如果你顺手把react-native升级到 0.74但原生侧的 ArkTS 映射没跟上编译时会各种报错。我的建议是项目创建后锁死package.json里的版本号不要随便用latest除非你能确认桥接包同步发布了新版本。2.2 初始化工程初始化命令使用的是社区模板大致如下npx react-native init AnimeHub --template react-native-oh/template --version 0.72.5 cd AnimeHub npm install网络不稳时可以给 npm 配置镜像源否则依赖下载会拖很久。工程生成后目录里会多出一个ohos目录这就是 OpenHarmony 原生工程android、ios目录是其他平台的暂时不用管。RN 的 JS 代码和原生壳是相对独立的我们主要在ohos/entry/src/main下做原生配置。用 IDE 导入ohos目录后第一次编译会比较慢因为要同时编译 React Native 的 C 适配层和 ArkTS 源码。如果报错提示找不到ohos/react-native先检查根目录的oh-package.json5是否正确声明了依赖然后再看 SDK 路径是否正确。2.3 权限声明与依赖安装页面要联网第一步先把网络权限加上。在ohos/entry/src/main/module.json5的requestPermissions里声明{ name: ohos.permission.INTERNET }OpenHarmony 默认不授予网络权限这一步漏了页面会一直请求失败而且控制台报错还不明显。JS 侧依赖我安装了三个npm install axios zustand react-native-safe-area-context依赖作用注意事项axios网络请求拦截器方便统一处理错误zustand状态管理代码量少适合中小页面react-native-safe-area-context状态栏/底部安全区域适配OpenHarmony 上需要适配版本这里要特别说一句react-native-safe-area-context。在 Android/iOS 上它可以直接用但 OpenHarmony 的刘海屏、手势区域和它们不一样必须确认安装的是适配过 OHOS 的版本否则页面顶部会被状态栏遮住一部分底部也会多出一块空白。3. 核心页面开发与细节实现3.1 页面结构与交互状态设计UpcomingScreen从布局上拆成了四块顶部标题栏与 Tab 区域、FlatList 列表区域、底部加载状态、异常重试区域。交互状态则定义为一个清晰的对象type UpcomingState { list: AnimeItem[]; page: number; refreshing: boolean; loadingMore: boolean; hasMore: boolean; range: week | month | all; };初次进入页面自动请求page1下拉刷新把page重置为 1用新数据替换旧列表上拉到底且hasMore为 true 时请求下一页请求失败时保留原列表只弹提示不整页崩溃。整页异常状态单独做一张错误图方便用户点击重试。这里有个细节refreshing和loadingMore必须分开。如果共用一个状态下拉刷新还没有结束用户就上拉会触发两个并发请求产生数据覆盖问题。我在初始化时就把这两个锁独立出来后面的坑少了很多。3.2 网络请求层封装axios 实例我放在了src/api/upcoming.ts统一设置超时和拦截器import axios from axios; export const apiClient axios.create({ baseURL: https://api.animehub.example.com, timeout: 10000, }); apiClient.interceptors.request.use(config { // 注入 token、签名等 return config; }); apiClient.interceptors.response.use( response response.data, error Promise.reject(error), );请求即将上映列表的方法是这样export const fetchUpcoming async (params: { page: number; pageSize: number; range?: week | month | all; sort?: date | hot; }) { const res await apiClient.get(/api/v1/animes/upcoming, { params }); return res.data as { list: AnimeItem[]; hasMore: boolean }; };一个很重要的经验后端返回的日期字段最好用 ISO 8601 字符串比如2025-07-20T00:00:00Z不要用纯时间戳。时间戳在时区转换时容易出错而且前端可读性差。这个建议我在第五章还会展开。3.3 列表单元格 UpcomingCardUpcomingCard的结构并不复杂但有几个容易让页面变慢的细节。先看核心 JSXView style{styles.card} Image source{{ uri: item.cover }} style{styles.cover} resizeModecover / View style{styles.info} Text style{styles.title}{item.title}/Text Text style{styles.date}{formatReleaseDate(item.releaseDate)}/Text View style{styles.hotRow} View style{[styles.hotBar, { width: ${item.hotScore}% }]} / Text{item.hotScore} 热度/Text /View /View /View两点提醒。第一封面图不要在列表里直接展示原图最好让后端提供 400x600 左右的缩略图或者前端在请求图片 URL 时加上 CDN 裁剪参数。全尺寸图片在 OpenHarmony 上滚动时会明显感到内存压力甚至直接闪退。第二热度条宽度直接用后端返回的hotScore来渲染不要在前端通过“当前最大值”动态计算。否则列表数据变化时已经渲染的卡片宽度会突然变化看起来非常跳。另外FlatList 的keyExtractor一定要用稳定的 id绝不能用数组 index。分页追加之后 index 会漂移React 复用节点时可能出现图片错位这个坑我印象很深。3.4 日期逻辑与“即将上映”判定“即将上映”听起来是个很自然的判断但实际实现时有很多边界情况。我的工具函数是这样的const startOfTodayUTC Date.UTC( new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate(), ); const isUpcoming (releaseDate: string) { return new Date(releaseDate).getTime() startOfTodayUTC; };为什么强调 UTC因为如果后端返回的是 UTC 时间而前端直接用本地时间取“今天”在 UTC8 时区下会有一个时间窗口导致当天日期少算一天。更稳妥的做法是后端直接把日期格式化成“年月日”再给前端前端不做时区推断。但既然接口是标准 ISO 格式我们就统一用 UTC 做边界比较展示时才转本地时间。展示格式我写成了类似“07月20日 周六”的样子export function formatReleaseDate(iso: string): string { const date new Date(iso); const month String(date.getMonth() 1).padStart(2, 0); const day String(date.getDate()).padStart(2, 0); const week [周日, 周一, 周二, 周三, 周四, 周五, 周六][date.getDay()]; return ${month}月${day}日 ${week}; }注意如果 ISO 里带的是 UTC 时间这个函数直接用本地时间格式化日期仍然可能偏移一天。所以在真实项目里我们干脆让后端返回已经格式化好的日期字符串前端只负责展示如果需要倒计时计算再单独传一个 UTC 时间戳字段。3.5 骨架屏与首屏体验优化网络页面最怕白屏。我顺手做了一个简单的骨架屏组件用灰色块替代卡片再配合透明度闪烁动画const SkeletonCard () ( View style{styles.card} View style{[styles.cover, { backgroundColor: #E5E7EB }]} / View style{styles.info} View style{{ width: 60%, height: 16, backgroundColor: #E5E7EB, borderRadius: 4 }} / View style{{ width: 40%, height: 14, backgroundColor: #E5E7EB, borderRadius: 4, marginTop: 8 }} / /View /View );页面在 loading 状态下渲染 6 个SkeletonCard数据到达后替换为真实列表。这个方案比转圈菊花友好很多成本也低。另外如果列表前几条海报图较大等用户滑动时才开始下载图片会一块块地出现。可以在拿到列表数据后用Image.prefetch预加载后面几条封面图。注意别预加载太多最多 5 张否则会抢占网络带宽反而拖慢首屏。4. 原生能力接入与性能调优4.1 通过 NativeModule 实现“电话提醒”前面说过RN for OpenHarmony 的价值不只是渲染列表它还能通过原生模块扩展系统能力。我在这个页面里做了一个“电话提醒”的演示功能用户在即将上映页点击“提醒我”后可以选择通过电话客服登记提醒。实现方式是在 OpenHarmony 工程里新增一个 NativeModuleimport { TurboModule } from ohos/react-native-oh; export class DialManager extends TurboModule { makeCall(phoneNumber: string, callback: (result: boolean) void) { // 调用系统电话能力申请对应权限 } }在 RN 侧调用import { NativeModules } from react-native; const { DialManager } NativeModules; DialManager.makeCall(400-800-1234);这里要特别提醒电话、通讯录这类能力属于敏感权限OpenHarmony 上必须申请运行时权限并且应用商店审核时会检查权限声明和实际功能是否匹配。如果只是做个 demo也可以先用Linking.openURL(tel:400-800-1234)顶一下但这个方案在不同 OHOS 版本上的行为并不一致我在真机测试时发现有的版本不会弹出拨号界面。生产项目建议还是走 NativeModule语义清楚、行为可控。4.2 相机扫码能力接入思路热搜词里有 openharmony camera正好可以提一个扩展方案。运营想要在“即将上映”页给海报增加“扫一扫看 PV”的入口这就涉及调用相机扫码。整体思路和电话模块一样原生侧封装CameraModule调用 OpenHarmony 相机能力完成扫码。JS 侧通过NativeModules调用拿到扫码结果后跳转到预告播放页。权限需要声明ohos.permission.CAMERA并使用 AccessToken 做运行时申请。我没有把扫码做成线上功能因为产品排期不够但从技术验证来看这条路是通的。唯一要注意的是这类功能如果要做 XTS 兼容性认证权限申请必须与实际使用场景严格对应不能提前申请用不到的敏感权限。我在权限治理上吃过亏后面会专门讲。4.3 列表滑动性能专项优化FlatList 在 OpenHarmony 上的行为和 Android 类似但优化参数必须自己反复测。我最终用在页面上的配置是这样FlatList data{list} renderItem{renderItem} keyExtractor{(item) item.id} initialNumToRender{8} maxToRenderPerBatch{8} windowSize{5} removeClippedSubviews getItemLayout{(_, index) ({ length: 120, offset: 120 * index, index })} onRefresh{handleRefresh} refreshing{refreshing} onEndReached{handleLoadMore} onEndReachedThreshold{0.3} /各参数的作用initialNumToRender控制首屏渲染的条数设太大会拖慢首屏设为 8 比较适合卡片高度固定的场景。windowSize控制可视区外预先渲染的窗口大小过小会白屏过大浪费内存。removeClippedSubviews减少不可见视图的布局计算但如果卡片里图片样式比较复杂可能会引起图片闪烁。遇到闪烁时检查是否有zIndex或position样式冲突。getItemLayout在卡片高度固定时能极大提升滚动性能省去动态测量。还有一个常见的性能杀手父组件内联定义renderItem会导致子组件每次父级更新都重新渲染React.memo直接失效。所以我在页面里把renderItem单独提取成函数并且把UpcomingCard用memo包了一层。性能验收时打开开发者菜单的 Perf Monitor重点观察 JS 线程耗时。如果经常超过 16ms优先检查renderItem里有没有复杂计算把日期格式化等重复操作缓存起来。4.4 网络缓存与弱网兜底为了降低弱网下的重复请求我加了一层很简单的 Map 缓存const cache new Mapstring, { data: any; timestamp: number }(); const getCachedOrFetch async (key: string, fetchFn: () Promiseany, ttl 60000) { const cached cache.get(key); if (cached Date.now() - cached.timestamp ttl) { return cached.data; } const data await fetchFn(); cache.set(key, { data, timestamp: Date.now() }); return data; };下拉刷新时强制绕过缓存保证用户看到最新数据非手动刷新时直接读缓存可以明显减少弱网下的白屏等待。注意不要把分页整体缓存进去否则上拉加载会和缓存冲突推荐只对首页数据或单个详情做短时缓存。5. 常见问题与排查技巧实录5.1 编译与环境问题现象可能原因处理方式RN 依赖下载超时npm registry 不稳定配置镜像源原生工程编译报xxx not foundSDK/模板版本不匹配按模板锁定的版本重装依赖Metro 启动后设备连不上设备与电脑不在同一网段检查 8081 端口和网络连通性JS bundle 一直加载中bundle 路径配置错误检查 Entry 里加载的 bundleUrl最典型的坑是手动升级react-native之后桥接包没有同步升级导致界面能打开但所有组件渲染失败。原生侧和 JS 侧的组件映射必须严格对应所以依赖版本以模板为准不要自己乱调。5.2 页面渲染问题页面白屏时先确认 Metro 是否在运行再看entry/src/main/ets里的加载地址是否指向开发机 IP不要用localhost。列表滚动卡顿就去看有没有大量重复渲染React.memo之后一般能缓解。图片不显示先确认 URL 是 HTTPS并且测试环境下后端允许防盗链这个排查最容易被忽略代码完全正确但图片域名被 CDN 拦截了。下接刷新不触发也是用户经常反馈的问题。我排查时发现很多情况下是contentContainerStyle里写了flex: 1把列表高度撑成了全屏FlatList的滚动手势被干扰。删掉flex: 1改成自然高度就正常了。5.3 日期与分页数据问题日期提前一天这个问题我之前已经强调过根源基本都是 UTC 与本地时区混用。解决思路是后端统一返回本地化日期字符串或前端明确用 UTC 做边界比较。上拉重复加载的处理方式也很直接在handleLoadMore开头加一把锁if (loadingMore || !hasMore || refreshing) return;如果筛选切换后列表还残留旧数据是因为请求新数据前没有清空list。Zustand 里切换筛选参数时要先用replace而不是concat。5.4 权限与合规问题无法访问网络先查module.json5里有没有ohos.permission.INTERNET。调电话、相机没反应除了权限声明外还要确认应用是否有对应的上下文。很多新手只写了静态权限没有在原生侧做运行时授权系统会静默失败。如果将来要做 XTS 认证权限要做到最小化、按需申请。我在页面里没有申请任何多余权限只有在点击“提醒我”时才动态请求电话权限这样对审核更友好。6. 打包上线与后续扩展6.1 构建 HAP 包拿到签名证书后在 IDE 里配置签名然后构建 HAP通过hdc install安装到真机。这里有个经验调试签名和发布签名要分开不要在发布配置里勾选“自动生成”否则后续升级版本时签名不一致会非常痛苦。建议团队从一开始就在 CI 里固定签名文件每次构建都复用同一份。6.2 后续功能扩展方向“即将上映”页面后续有几个很自然的扩展点“想看”收藏涉及本地存储或服务端接口日历提醒点击“提醒我”后写入系统日历这也是原生模块扩展练手的好场景后端接口如果由 Django 团队维护前后端分离模式下前端这边只需要对齐接口契约页面开发节奏会更独立。最后再分享一个我个人的体会做 RN for OpenHarmony 的列表页真正难的不是组件写法而是跨端适配的边界——哪些能力原生侧负责、哪些交给 JS 侧以及日期、缓存这些基础概念在不同平台上的差异。刚开始不用贪多把一个列表页的完整链路跑通再往电话、相机这些原生能力扩展节奏会稳很多。