前阵子团队接了个硬性需求在鸿蒙系统上做一个垃圾分类答题应用。业务本身不复杂但有个现实约束——Android、iOS 两端的 React Native 代码库已经迭代了好几年答题逻辑、题库数据、状态管理全部沉淀在里面如果为了鸿蒙拿 ArkTS 从零再写一遍等于以后要同时维护两套业务核心。最终我选了 React Native 的鸿蒙适配链路用同一套 JS 业务代码跑通了 HarmonyOS 真机这篇就把整个过程拆一遍环境怎么搭、题库怎么建模、答题交互怎么写、三端差异怎么收口以及启动白屏这类高频问题怎么排查。如果你正在纠结要不要为鸿蒙重写业务这篇文章的思路可以直接套用。1. 项目拆解与跨端方案选型1.1 为什么在鸿蒙上还要用React Native先把背景说清楚。HarmonyOS NEXT 生态已经不再兼容安卓 APK想在鸿蒙上跑应用摆在面前的路线无非两条走 ArkTS 原生开发或者走跨端框架适配层。前者更贴近系统能力后者能最大程度复用已有工程资产。我当时选择 React Native核心原因有三个。第一团队已有的 RN 代码库沉淀了大量业务语义比如垃圾分类的判别规则、错题记录逻辑、计分统计这些和 UI 无关属于可以复用的纯 TS/JS 代码。第二RN 的跨端理念在鸿蒙上依然成立底层由社区维护的 React Native for OpenHarmony 项目提供能力对话框、列表、网络请求这些基础组件都有对应的鸿蒙原生实现。第三生态迁移成本相对可控大部分纯 JS 实现的 npm 包可以直接用只有涉及原生能力的库才需要找鸿蒙适配版本。这个选型带来的影响范围也不小。对团队来说意味着人力可以继续围绕 RN 技术栈培养不需要立刻全员转 ArkTS对个人开发者来说意味着你之前积累的 RN 经验没有浪费鸿蒙上依然能吃老本。当然这个方案也有代价后面我会专门讲平台差异和适配红线别指望零成本白嫖。1.2 垃圾分类答题应用的需求边界开始动工之前我先把产品需求收敛了一下。垃圾分类答题应用本质上是“知识问答 即时反馈 学习巩固”的组合。核心模块拆出来就四块题库系统、答题流程、结果统计、错题科普。题库系统要管数据模型每道题对应一件垃圾物品需要记录它的名称、类别、难度、解析文案。答题流程要处理倒计时、选项点击、正误反馈、下一题切换。结果统计要算分、算正确率、记录答题历史。错题科普是我主动加的模块答错的题会生成知识卡告诉用户这件垃圾为什么属于这个类别避免用户只是蒙对了。技术边界也做了取舍。第一版不接后端题库放本地 JSON答题记录存本地存储这样应用离线可用、工程简单、上线风险低。等用户量起来之后题库下发、排行榜、每日挑战这些能力都可以在同一套数据模型上扩展。类是这种“先跑通单机闭环、再补服务端”的思路特别适合个人开发者练手也适合团队快速验证跨端方案。2. 环境搭建与鸿蒙工程接入全流程2.1 开发环境准备一个 RN 鸿蒙工程的前置环境比普通 RN 工程要多装一个 IDE。我当时实际用到的工具和版本建议整理成了表格你照着装就行。工具版本建议作用DevEco Studio当前最新稳定版编译鸿蒙 HAP 包的 IDEHarmonyOS SDKAPI 12 及以上提供鸿蒙系统能力Node.js18 及以上RN 工具链依赖JDK17hvigor 构建工具依赖HarmonyOS 手机或模拟器HarmonyOS NEXT 及以上真机调试目标安装 DevEco Studio 的时候SDK 组件记得一起勾上我第一次装的时候漏了 HarmonyOS SDK后面同步工程直接报错。装完环境后在终端里先跑一遍基础检测node -v java -version hdc versionhdc是鸿蒙的真机调试命令类似 Android 的 adb。三条命令都能正常输出版本号说明基础环境没有大问题。这里有两个容易踩的坑一个是 JDK 版本hvigor 构建对 JDK 版本敏感不要用太老的版本另一个是环境变量DevEco Studio 自带工具链但命令行里要顺手用的话最好把hdc和hvigorw的路径加进 PATH不然后面执行构建命令还得切目录找工具。2.2 一行命令初始化RN鸿蒙工程环境就绪之后初始化工程我用了社区提供的脚手架react-native-oh-tpl/cli它会在标准 RN 工程基础上额外生成一个harmony/目录里面是完整的鸿蒙原生工程。npx react-native-oh-tpl/cli init GarbageQuiz cd GarbageQuiz初始化完成后重点看两个地方。一个是根目录的package.json里面多了harmony配置块这个配置块决定了鸿蒙侧 SDK 的依赖方式另一个是harmony/目录用 DevEco Studio 打开它等 IDE 完成 Gradle 之外的第一次同步。这一步很容易出现 SDK 版本对不上的报错解决方案通常是打开 DevEco Studio 的 SDK Manager把package.json里harmony.dependency.version指定的版本装齐。开发模式下跑应用根目录执行npm start启动 Metro Bundler 之后再用 DevEco Studio 把鸿蒙工程跑起来。开发阶段 Metro 和模拟器走的是同一个局域网真机调试时需要注意 IP 配置这个细节我在第四章启动白屏部分会重点讲。2.3 package.json里的鸿蒙配置块到底在配什么很多 RN 开发者第一次接触鸿蒙工程都会卡在看不懂package.json里这段配置{ harmony: { dependency: { version: 0.0.31 }, buildOption: { arkts: true } } }简单解释一下。harmony.dependency.version指定的是 React Native 鸿蒙适配层的版本号这个版本要和react-native主版本匹配否则原生侧和 JS 侧的方法调用会对不上。buildOption.arkts表示允许鸿蒙工程里使用 ArkTS 语法这是鸿蒙原生侧文件的标准写法。我踩过最疼的坑是版本组合问题。RN 社区版本迭代很快新的鸿蒙适配版本往往只支持特定 RN 主版本比如适配层 0.0.3x 可能对应 RN 0.72升级到 RN 0.73 之后部分原生模块会失效。所以我的建议是先锁死一套组合版本不要轻易单独升级 RN 或者适配层。另一个建议是所有涉及原生能力的第三方库去 npm 上找名字带react-native-oh-tpl前缀的适配版本纯 JS 的库才可以直接用原版。这个经验能帮你避开大半构建期报错。3. 垃圾分类题库与答题交互实现3.1 先给垃圾建个模型四分类知识数据结构垃圾分类的领域知识不能散落在 UI 代码里第一步是建结构化数据模型。国内通用分类标准是四类可回收物、有害垃圾、厨余垃圾、其他垃圾。我定义了一个GarbageItem接口每个题目就是一个吃垃圾的知识卡export interface GarbageItem { id: string name: string description?: string category: 0 | 1 | 2 | 3 // 0可回收物 1有害垃圾 2厨余垃圾 3其他垃圾 difficulty: 1 | 2 | 3 tip: string // 答案解析答错时展示 }四类垃圾的判定逻辑我在设计题库时归纳成一句话“材质可回收看干净程度有毒有害单独挑能腐烂的进厨余剩下难降解难回收的都是其他。” 但实际写题的时候会发现边界案例特别多比如塑料瓶属于可回收物但沾满油污的塑料餐盒属于其他垃圾大棒骨因为难降解不能算厨余垃圾而是其他垃圾。这类“易错边界题”恰恰是答题应用最有价值的部分我会专门给它们加difficulty: 3的标签。题库我直接用 JSON 文件维护一个条目一个条目扩展不搞数据库。本地 JSON 的好处是静态打包、加载快、改起来直观等题库量大之后再做服务端下发也不迟。每个题目只存数据不存 UI 状态UI 状态全部交给组件管理这样后端接入时数据结构不用变。3.2 不靠设计稿也能撑场面的答题UI与交互实现答题页的 UI 结构不复杂从上到下依次是当前题号、物品名称与描述、四个分类选项按钮、倒计时进度、解析区域。布局用 RN 的 Flexbox 就能搞定我在鸿蒙上实测的核心组件代码如下import React, { useEffect, useState } from react import { View, Text, TouchableOpacity, StyleSheet } from react-native const categoryNames [可回收物, 有害垃圾, 厨余垃圾, 其他垃圾] interface QuizCardProps { question: GarbageItem onAnswer: (categoryIndex: number, isCorrect: boolean) void } export default function QuizCard({ question, onAnswer }: QuizCardProps) { const [answered, setAnswered] useState(false) const [selected, setSelected] useStatenumber | null(null) const handlePress (idx: number) { if (answered) return setSelected(idx) setAnswered(true) const correct idx question.category onAnswer(idx, correct) } return ( View style{styles.card} Text style{styles.itemName}{question.name}/Text Text style{styles.desc}{question.description}/Text {categoryNames.map((name, idx) ( TouchableOpacity key{name} style{[ styles.option, answered idx question.category styles.optionCorrect, answered idx selected idx ! question.category styles.optionWrong, ]} onPress{() handlePress(idx)} Text style{styles.optionText}{name}/Text /TouchableOpacity ))} {answered ? Text style{styles.tip}{question.tip}/Text : null} /View ) }交互状态机只有三态待答题、已作答、进入下一题。点击选项后立刻锁定其他按钮同时展示正确与错误的高亮色并弹出tip解析文案。这个状态机虽然简单但能保证用户不会反复点击触发多次计分。答题页面是典型的重状态组件所以我用useState管理当前题目的作答状态用useMemo缓存解析文案避免每次渲染都重新拼字符串。3.3 倒计时、计分与错题科普闭环答题如果没有倒计时用户会慢慢悠悠查资料失去答题节奏。我用setInterval实现了一个 15 秒倒计时重点在于每道题切换时都要重置计时器并且组件卸载时清理避免定时器泄漏const [left, setLeft] useState(15) useEffect(() { setLeft(15) const timer setInterval(() { setLeft(prev (prev 0 ? prev - 1 : 0)) }, 1000) return () clearInterval(timer) }, [question.id])这里有个细节useEffect的依赖数组里用了question.id而不是question对象本身因为题目对象可能在切题时被重新创建导致不必要的影响。倒计时归零后就自动触发一次超时答案计 0 分并切换下一题。计分规则我定的是答对加 10 分连续答对额外加 2 分连击奖励答错不扣分但记录错题。连击奖励会让答题过程有一点点游戏化的刺激感。错题记录是答题闭环里最有价值的一块每题答错时把题目 ID、用户选择、正确答案写进本地存储生成错题列表。下一版计划让用户直接重刷错题这一版先做好数据采集。3.4 题库加载与本地存储方案第一版不接后端题库直接打进包里。我用了动态import加载 JSON避免首屏一次解析全部题目const loadQuestions async () { const data await import(../data/questions.json) return data.default }本地存储用的是 AsyncStorage 的鸿蒙适配版本。名字带react-native-oh-tpl前缀的适配包才能跑在鸿蒙上用原版会报找不到原生模块。答题记录的结构很简单interface AnswerRecord { questionId: string selectedCategory: number correctCategory: number timestamp: number }存储层我封装了几个 Promise 方法读历史、写记录、清空数据业务页面不直接碰 AsyncStorage API。这一层封装的意义在于以后如果要从 AsyncStorage 换成 SQLite或者接后端同步只需要改存储层的实现页面代码不用动。统计功能也是读存储层数据在内存里算的不额外建表体量小的时候最省事。4. 鸿蒙平台适配与性能优化4.1 三端组件差异与适配红线RN 跑在鸿蒙上并不是所有组件都跟 Android、iOS 行为完全一致。这块差异如果不提前心里有数真机跑起来会被各种诡异问题浪费大量时间。能力AndroidiOSHarmonyOS 实测表现SafeAreaView靠系统状态栏处理自动避开刘海需要引入鸿蒙适配版安全区组件Platform.OSandroidiosharmony 或 ohos因版本而异本地图片资源require 可用require 可用适配层对资源路径处理有差异StatusBar可控制可控制系统接管程度高需有限适配第三方原生模块生态丰富生态丰富必须找带 oh-tpl 前缀的适配包Platform.OS的判断值一定要抽成公共常量。因为不同版本的鸿蒙适配层返回值可能不同直接在业务代码里散落判断版本升级时容易出遗漏。我在工程里建了一个platform.tsimport { Platform } from react-native export const isHarmony Platform.OS harmony || Platform.OS ohos export const isAndroid Platform.OS android export const isIOS Platform.OS ios所有平台差异都基于这个文件判断业务层只认isHarmony这样的语义化变量不认识底层值。适配红线就是凡涉及原生能力的代码一律先查有没有鸿蒙适配包没有适配包宁可自己包一层原生桥接也别裸着用原版库。4.2 启动白屏的三大根因与解决手段“React Native 鸿蒙启动白屏”是社区里讨论度最高的问题我自己也踩过。白屏的原因通常分三类。第一类是开发模式下 Metro 连接失败。HarmonyOS 真机通过 USB 连着电脑开发时Metro 默认监听localhost:8081但真机访问不到电脑的 localhost必须配置成电脑的局域网 IP。可以通过 DevEco Studio 的 DevTools 设置 Metro host或者运行adb reverse类似的能力做端口映射。第二类是 Bundle 加载太慢。开发模式每次启动都要从 Metro 拉 JS BundleBundle 体积大真机拉取耗时可能好几秒这个窗口期不渲染任何东西就是白屏。解决手段是根视图先渲染一个轻量的原生 Splash 占位等onLoad事件触发后再渲染主界面用户感知上就不是白屏而是正常的启动画面。第三类是根组件启动逻辑太重。我之前把题库加载、历史记录读取全部放在 App 组件初始化里这些异步任务在首帧渲染前执行直接阻塞了 JS 线程。改进后先把基础配置渲染出来题库用InteractionManager.runAfterInteractions延后处理启动速度提升明显。4.3 答题过程不卡顿的性能调优答题应用的性能压力主要来自三方面页面频繁重渲染、倒计时每秒触发更新、长列表渲染。对应调优手段也很直接。倒计时组件必须要独立。我最开始把倒计时状态放在答题页面大组件里每秒setInterval都会触发整个页面重渲染选项按钮的按下反馈会明显掉帧。拆成独立CountdownTimer组件后只有它自己每秒重渲染题卡和选项不跟着变。题库列表用 FlatList。错题列表、排行榜这些长列表一律用 FlatList 的虚拟列表能力不要直接map渲染几百个 View内存和滚动帧率差的不是一点半点。选项按钮的反馈动画用Animated驱动不要靠setState反复切换样式动画跑在 UI 线程JS 线程压力更低。答题卡的图片资源全部压缩到 WebP 或者小尺寸 PNG垃圾分类题目不需要高清大图清晰能认出来就够。5. 鸿蒙真机调试与HAP打包发布5.1 真机联调与日志排查真机联调是整个流程里最能发现问题的一步。HarmonyOS 手机上开启开发者模式的方法是进入“设置-关于手机”连续点击版本号直到提示开启开发者选项然后在开发者选项里打开 USB 调试。用数据线连上电脑后先在终端验证设备hdc list targets能看到设备序列号说明连接成功。安装调试包用hdc install entry-default-signed.hapRN 侧日志查看我常用的是hilog过滤 ReactNativeJShdc shell hilog | grep ReactNativeJSJS 侧的console.log会输出到 hilog原生的报错信息也能在这里看到。联调阶段强烈建议先跑通一个最小 Demo 再叠加业务模块。我在第一版直接把整个答题应用跑上去结果白屏后日志里全是报错分不清是 Metro 连接问题还是原生模块注册问题。后面学乖了先模拟器跑通“Hello World”再逐步加题库模块、存储模块、动画模块每次加一个都真机验证一次定位问题从半小时缩短到五分钟。5.2 HAP打包与签名分发开发调试跑通之后最后一步是打正式包。鸿蒙应用最终产物是 HAP 文件类似 Android 的 APK。在harmony/目录下执行hvigorw assembleHap构建产物一般在harmony/entry/build/default/outputs/default/目录下文件名类似entry-default-signed.hap。签名这块DevEco Studio 支持自动签名需要登录华为开发者账号配置好自己的签名证书。如果是个人内部测试真机安装调试包一般够了如果要上架应用市场还需要走华为应用市场的审核流程把 App 的图标、隐私声明、版本信息都整理齐。打包前一定要检查 release 模式的 Bundle 配置。开发模式走 Metro 在线加载 Bundle正式包必须把 JS Bundle 打进 HAP 本地资源里否则断网或者 Metro 不可用时应用直接白屏。鸿蒙侧 RN 适配层的 Release 构建脚本会处理 bundle 打包但你需要确认构建配置里 bundle 资源路径正确这块建议在打包后把 HAP 装上真机断网冷启动一次实测。6. 高频问题排查与实践复盘6.1 鸿蒙上跑RN高频问题速查表我把自己和身边朋友踩过的问题整理成了速查表按现象排查基本能覆盖大部分情况。问题现象可能原因排查方向启动白屏Metro 连接失败或 Bundle 加载慢检查 Metro host 配置、根组件首帧逻辑字体忽大忽小鸿蒙 PixelRatio 与 Android 不一致统一文本缩放策略按设计稿固定逻辑像素图片不显示资源路径适配问题改网络图片或鸿蒙原生资源目录网络请求失败缺少 INTERNET 权限检查 module.json5 权限声明与 HTTPS 配置构建报 SDK 版本错误HarmonyOS SDK 与适配层版本不匹配用 SDK Manager 补齐依赖版本第三方模块不生效原版 RN 包没有鸿蒙原生实现换成带 oh-tpl 前缀的适配包网络请求是很多人的盲区。鸿蒙应用要在module.json5里声明ohos.permission.INTERNET否则请求会被系统直接拦截。Android 上如果目标版本较低网络权限往往已经默认配置导致很多 RN 开发者在鸿蒙上栽跟头。另外鸿蒙对明文 HTTP 有默认限制开发环境如果接口是http://需要额外配置网络安全策略否则请求也会失败。6.2 做完复盘哪些坑值得一开始就避开整轮开发做下来我最想分享的复盘经验有四点。第一JS 核心代码要保持纯净平台差异全部收敛到platform.ts一类的适配文件里。项目越小越容易忽视这条规则但一旦模块多了散落各处的isHarmony判断会让后续维护变得非常痛苦。第二第三方库在引入前先查鸿蒙兼容性。我在项目里最开始用了某个装得很火的图片缓存库原包装进去直接崩溃换成适配包才跑通。现在我的习惯是先看 README 有没有 HarmonyOS 章节再看 npm 上有没有 oh-tpl 前缀版本最后看 issue 区有没有人报鸿蒙问题三个条件全过才引入。第三开发态和发布态的 Bundle 行为完全不一样。开发态各种小问题在发布态会被放大尤其是首屏启动和资源加载建议上线前用真机测一轮“断网冷启动 弱网环境”感受会比开发模式下直观很多。第四功能跑通只是起点三端手感一致才是跨端方案的价值所在。同样的答题动画iOS 上顺滑、Android 上卡顿、鸿蒙上掉帧的情况我都遇到过性能调优占了整个项目接近三分之一的时间。我个人实际做下来最大的体会是跨端框架解决的是业务复用问题不是免学习问题。鸿蒙的窗口形态、后台策略、系统控件和 Android、iOS 都有差异但这些差异通过一层薄薄的自定义封装是可以收敛的。如果你带着一支 RN 团队想快速切入鸿蒙或者你自己就是想验证一套代码三端跑的可行性垃圾分类答题这个体量的项目是最合适的练手载体。题库、答题、存储、打包、分发链路完整且没有复杂业务绑架跑通一遍你对 RN 鸿蒙开发的基本功就建立起来了。
阅读完成 · 觉得有帮助?