
最近我在折腾 React Native 和鸿蒙跨平台开发的时候给自己定了一个很具体的小目标不做登录页也不做待办清单而是做一个真正常见、能看到完整交互闭环的玩具——空调遥控器。选择这个项目的原因很直接它既有 UI 布局又有状态联动还有定时、边界判断这些小逻辑但又不依赖后端接口非常适合拿来练手。如果你正准备入门 React Native 鸿蒙跨平台开发又不想一上来就看一堆理论这个空调遥控器的思路和代码可以直接参考。这篇内容会围绕三个词展开React Native、鸿蒙、跨平台开发。我会从为什么选这个题目讲起再依次拆解环境搭建、界面设计、状态管理、交互逻辑最后把我在鸿蒙设备上真机运行遇到的白屏、调试、权限等问题一并记录。整个过程不需要你学过 ArkTS不需要会写原生代码只要懂一点 JavaScript 和 React 基础就能把这个遥控器跑起来。1. 为什么选“空调遥控器”作为 RN 鸿蒙入门项目1.1 空调遥控器的功能拆解看起来简单实际五脏俱全很多人低估了空调遥控器这个项目。表面上看它就是一个面板加几个按钮但真正动手做的时候才发现它涵盖了移动端开发里最常见的几类问题第一布局分层。遥控器需要把温度、模式、风速、定时、扫风这些模块排在一个界面里既要有层次感又不能堆成一团。这很考验对 View、Text、Pressable 这些基础组件的组合能力。第二状态多且互相影响。温度加减有上下限模式切换会影响图标和文案定时结束后要自动关电源关机之后所有按钮要进入禁用状态。这些都是典型的状态联动场景。第三交互反馈。用户按下一个按钮需要立刻看到视觉反馈哪怕只是按下去变个色、数字跳动一下体验差距都非常大。第四不需要网络请求。整个项目不需要登录、不需要接口、不需要数据库数据全在本地内存里。这样你就可以把全部精力放在 React Native 本身的渲染和状态管理上不会被网络层的东西干扰。从教学角度来看它比“计数器”复杂比“商城首页”简单处在一种刚刚好的位置。做完之后你不仅学到了布局和状态还能获得一个可以拿给别人演示的成果这种成就感很重要。1.2 技术选型为什么是 React Native 加鸿蒙而不是 ArkTS 或 Flutter先说结论如果你已经会 React或者团队已经有 React Native 技术栈想在鸿蒙设备上快速跑起来React Native 是非常务实的选择。鸿蒙原生开发的主流方案是 ArkTS 加 ArkUI它确实和鸿蒙结合得最紧密性能也最直接但需要你单独学一套声明式 UI 语法而且写出来的代码只能跑在鸿蒙生态上。对于只想做跨平台、复用已有 React 技术栈的人来说学习成本偏高。Flutter 也是一条跨端路线但它使用的是 Dart 语言和前端团队的知识储备匹配度不高。如果项目没有特殊的多端一致性要求React Native 在代码复用上的优势会更明显。现在鸿蒙社区里有一套成熟的方案通常叫 React Native for OpenHarmony核心包是 react-native-harmony。这套方案做的事情很简单把 React Native 的 JS 组件映射到鸿蒙原生组件上让前端开发用 JS 加 React 语法写业务底层还是用鸿蒙的原生渲染能力。所以回答一个很常见的疑问——“鸿蒙编程需要什么基础”做 RN 鸿蒙开发你需要的基础是 JavaScript 和 React而不是 ArkTS。你不需要先把 ArkTS 学完再上手真正需要的反而是对组件化、状态管理这些前端概念的理解。有人会担心这套方案是否稳定。我的看法是基础的核心组件和 API 已经能支撑这类中小型项目了遇到不支持的 API 再走原生模块找补即可。学习阶段用它来做工具类、业务型的小应用完全没有问题。2. 环境准备与工程初始化从零跑通 React Native 鸿蒙工程2.1 需要准备哪些开发环境这一步看似简单其实很多坑都出在环境版本不一致上。我整理了一份我自己实测下来比较顺的组合工具推荐版本说明Node.js18 或 20 LTS不要用太新的奇数版本避免和 Metro 编译链出现兼容问题JDK17RN 鸿蒙工程的 Gradle 编译依赖版本太老会直接编译失败DevEco Studio5.0 及以上目前新版本对 RN 鸿蒙工程的支持更完整HarmonyOS SDK / OpenHarmony SDK随 DevEco 安装首次启动 DevEco 时下载网络状况好的话耗时较长模拟器或真机推荐真机模拟器能跑但传感器、性能表现不如真机贴近实际这里我要重点提醒一件事不要本机装了一个高版本 Node又装了一个旧版本 DevEco然后抱怨项目跑不起来。我踩过最耗时的坑就是 Node 20 配旧版 DevEco编译时 Gradle 直接报一堆网络错误换成 Node 18 之后立刻就好了。建议在开始之前把 Node 和 DevEco Studio 都升到较新的大版本。2.2 创建 RN 鸿蒙工程脚手架命令与目录结构解读创建 React Native 鸿蒙工程最简单的方式是使用社区提供的脚手架。在终端里执行npx react-native-oh-tpl/clilatest init AirRemote cd AirRemote npm install执行完之后你会看到一个标准的 React Native 工程结构但里面多了一个harmony文件夹。这个文件夹就是鸿蒙应用工程打开harmony后会看到entry、hvigor、oh-package.json5这些鸿蒙工程文件。简单说Metro 负责打包 JS 代码DevEco 负责把鸿蒙原生壳和 JS 包一起组装成可运行的应用。接下来在 DevEco Studio 里不要直接打开整个 RN 工程目录而是打开AirRemote/harmony子目录。等待 DevEco 完成 Sync 和依赖下载。第一次同步通常会拉取很多 Gradle 依赖和鸿蒙 SDK 组件需要多一些耐心。如果你已经有一个现成的普通 React Native 项目也可以尝试走“二次接入”的方式手动安装 react-native-harmony 包并配置 harmony 目录。但我不建议新手一上来就这么干初始化桥接配置很容易漏步骤。先通过脚手架跑通一个全新工程之后再研究迁移才是更合理的路径。2.3 把工程跑起来的完整流程流程分三步走在工程根目录执行npm start启动 Metro 开发服务器。Metro 的作用是把 JS 代码实时编译成设备能执行的 bundle相当于开发模式下的“热菜厨房”每次改动代码后它会自动增量编译给你端上来。在 DevEco Studio 中确认已经连接上模拟器或真机然后直接点击 Run 按钮先把鸿蒙原生壳安装到设备上。这时设备上会多出一个 AirRemote 图标但第一次打开很可能白屏因为你还没有让 app 正确连上 Metro。白屏是 React Native 鸿蒙开发里最常见的第一个问题后面第 5 章会专门讲排查方法。这里先记住一个动作确保电脑和手机连的是同一个局域网并且 Metro 的 8081 端口没有被防火墙挡住。整个流程跑通之后你就能在鸿蒙设备上看到脚手架自带的初始界面。到了这一步基础工程就算立住了后面再改代码去实现遥控器就轻松很多。3. 空调遥控器核心 UI 与状态设计3.1 先搭遥控器外观布局分层与视觉层次设计遥控器界面不要一上来就堆组件。我习惯先在纸上把遥控器区域拆成几块顶部是电源开关和品牌位中间是温度显示区下面是模式选择区再往下是风速、扫风、定时的按钮组。在 React Native 里实现这种分层最基础的方式就是 View 嵌套配合 flex 布局和圆角卡片。为了不引入额外依赖我推荐用纯色背景加多层嵌套的方式模拟“面板质感”而不是直接上需要原生模块支持的渐变库。你可以在最外层用一个深色 View 做遥控器机身内层放一个浅色卡片卡片内部再用 borderBottomWidth 或者 margin 做模块分隔效果就足够了。布局伪代码如下SafeAreaView style{styles.root} View style{styles.remoteBody} View style{styles.powerRow} {/* 电源开关 */} /View View style{styles.tempPanel} {/* 温度和加减按钮 */} /View View style{styles.modeRow} {/* 制冷/制热/除湿/送风 */} /View View style{styles.fanRow} {/* 风速选项 */} /View /View /SafeAreaView这里要特别提醒遥控器按键的触控区域一定要够大至少 44 到 48 个逻辑像素否则在鸿蒙真机上按起来会很费劲。另外别把所有文字都挤在一个 Text 里多个文字块用 flexDirection 和 gap 布局会比手工加空格可靠得多。3.2 用一条 useState 流水线管理空调状态空调遥控器的所有功能本质上都是对一组状态进行读写。我习惯把所有状态集中放在遥控器顶层组件里然后通过 props 传给子组件。这种“状态上提、UI 下沉”的方式最适合这种小型项目。状态清单如下const [power, setPower] useState(true); // 电源开关 const [mode, setMode] useState(cool); // 模式: cool/heat/dry/fan const [temperature, setTemperature] useState(26); // 温度 const [fanSpeed, setFanSpeed] useState(auto); // 风速: auto/low/mid/high const [swing, setSwing] useState(false); // 扫风开关 const [remaining, setRemaining] useState(null); // 定时剩余秒数你可以把这一组 state 想象成遥控器内部的拨杆和记忆芯片。每次按键改变某个 state界面就会重新渲染这就是 React 最核心的“单向数据流”思想。为什么不引入 Redux 或 Zustand因为这个项目的状态量不大而且状态之间的联动不算复杂useState 配合函数式更新就足够了。盲目引入状态管理库只会增加概念负担。等到以后项目变复杂比如遥控器要和多个页面共享数据再考虑状态管理库也不迟。3.3 温度、模式、风速这些控件怎么组织成组件组件拆分的核心原则是“一个组件只负责一件事”。我把遥控器拆成四个子组件TempControl、ModeSelector、FanSpeedSelector、TimerControl。每个子组件通过 props 接收状态和回调函数这样修改温度的逻辑只影响温度组件不会牵连到风速。以温度组件为例function TempControl({ temperature, onChange }) { return ( View style{styles.tempBox} Pressable onPress{() onChange(-1)} style{styles.btn} Text style{styles.btnText}-/Text /Pressable Text style{styles.tempNum}{temperature}/Text Pressable onPress{() onChange(1)} style{styles.btn} Text style{styles.btnText}/Text /Pressable /View ); }父组件通过onChange传进来一个函数子组件不自己管理数据只负责把用户意图抛出去。这种受控组件模式在 React Native 里是最稳健的调试的时候也能清楚每一步的状态是谁改的。4. 交互逻辑让遥控器真正“可操作”4.1 温度加减与上下限从 16 到 30 度空调温度不是无限加减的这里要处理边界限制。温度我设定为 16 到 30 度每按一次加减 1 度。最直接的实现方式是const changeTemperature (delta) { setTemperature((prev) Math.min(30, Math.max(16, prev delta))); };为什么要用Math.min和Math.max而不是先加再判断因为函数式更新可以避免在快速连续点击时读到旧状态这是 React 并发渲染下的一个细节。你也可以封装成更接近业务语义的写法const changeTemperature (delta) { setTemperature((prev) { const next prev delta; if (next 16) return 16; if (next 30) return 30; return next; }); };两种写法效果一样推荐第二种因为后续如果要根据模式动态调整上下限比如制热模式下最低 18 度改造起来更直观。这里有一个容易忽略的点当温度已经到达 30 度时加号按钮虽然不能把温度变得更高但不应该让用户觉得“按键失灵”。正确的处理是给加号按钮一个置灰或者半透明的禁用态让用户一眼看出已经到顶了。4.2 模式循环切换与图标文案联动模式选择我做了四种制冷、制热、除湿、送风。为了节省界面空间我采用“点击切换、循环显示”的模式也就是按一次按钮模式就从 cooling 变到 heating再变到 dehumidify再变到 fan然后回到 cooling。代码可以这样写const MODES [cool, heat, dry, fan]; const switchMode () { setMode((prev) { const index MODES.indexOf(prev); return MODES[(index 1) % MODES.length]; }); };用取模运算的好处是哪怕模式列表以后扩展到六个、八个这段逻辑依然成立不需要写一长串 if else。同时模式会影响温度和风口文案。比如送风模式下温度没有意义应该把温度显示区域隐藏或者显示成“--”。这里可以加一个条件渲染{mode ! fan ? ( TempControl ... / ) : ( Text style{styles.hint}送风模式无温度设置/Text )}这一行条件渲染就是“不同组件状态联动”的典型例子。很多初学者不理解为什么 React 里能做到“数据变了界面跟着变”根源就在这里数据驱动渲染。4.3 定时关机倒计时setInterval 的清理陷阱定时关机是这个项目里比较容易踩坑的点。我定的规则是用户可以选择 0.5 到 8 小时的定时开启后界面每分钟显示剩余时间倒计时归零后自动关闭电源。计时逻辑用setInterval来做但 React 里使用定时器必须格外小心组件卸载和重复创建的问题。一种比较稳的写法useEffect(() { if (!power || remaining null) return; const timer setInterval(() { setRemaining((prev) { if (prev null) return prev; if (prev 1) { clearInterval(timer); setPower(false); return null; } return prev - 1; }); }, 1000); return () clearInterval(timer); }, [power]);这里有一个关键细节effect 只在power变化时创建和清理定时器而每隔一秒更新剩余时间时并不是靠重置定时器来实现而是靠函数式setRemaining读取最新值。如果你错误地把remaining加进依赖数组会导致每次倒计时都重新创建定时器整个倒计时就乱了。显示格式也需要处理一下不要直接显示“1500 秒”。我封装了一个格式化函数const formatRemain (sec) { const h Math.floor(sec / 3600); const m Math.floor((sec % 3600) / 60); return h 0 ? ${h}小时${m}分 : ${m}分钟; };定时开始之后要允许用户取消。我的做法是再次点击定时按钮时如果 remaining 不为空就把remaining置为 null。这样既不用重新启动定时的入口也不会在状态里留下脏数据。4.4 按键反馈与禁用态体验细节不能省很多入门项目做出来功能都对但用起来很“木”问题就出在缺少交互反馈。React Native 里最简单高效的反馈方式是Pressable它可以在按下去和松开时分别应用不同的样式Pressable onPress{...} style{({ pressed }) [ styles.btn, pressed styles.btnPressed, ]} Text style{styles.btnText}/Text /PressablebtnPressed我一般设置成透明度下降加颜色加深按下瞬间能明显看到变化。这个小改动能让整个遥控器的操作质感提升很多。另一个容易被忽略的细节是禁用态。当电源关闭时温度、模式、风速、扫风、定时这些按键都应该不能点击。实现上可以给每个子组件传一个disabled属性父组件统一用!power来决定。禁用态的样式也不要只是不让点最好把文字颜色整体调淡让界面状态一目了然。另外鸿蒙的无障碍能力同样会读取 View 的语义标签。给按钮加上accessibilityLabel是非常低成本的优化比如加号按钮写成“升高温度”长按或读屏时就不至于让用户听到一个孤零零的“加号”。5. 鸿蒙适配与启动白屏排查经验实录5.1 鸿蒙工程里必须检查的几项配置React Native 鸿蒙工程和普通 Android 工程最大的区别是它多了一层鸿蒙原生工程配置。很多时候 JS 代码本身没问题但应用就是跑不起来问题都出在这层配置上。首先要检查的是harmony/entry/src/main/module.json5里的权限声明。开发模式下应用需要通过 Metro 从电脑加载 JS bundle所以必须要有网络权限。缺少INTERNET权限的典型表现就是应用安装成功启动后白屏Metro 日志里看不到任何连接请求。我建议在工程初始化后第一时间确认module.json5里包含网络权限{ requestPermissions: [ { name: ohos.permission.INTERNET, reason: 连接Metro开发服务器加载JS bundle, usedScene: { abilities: [EntryAbility], when: inuse } } ] }还要确认签名配置。DevEco Studio 5.0 之后真机调试基本都要求走自动签名。真机设备如果之前没登录过华为账号直接 Run 会提示签名错误这时候先在 DevEco 里登录账号并完成自动签名再把设备插上基本一次就能过。5.2 启动白屏的排查顺序启动白屏这个词在 React Native 社区里约等于“新手村第一只 Boss”。鸿蒙环境下白屏原因和 Android、iOS 不完全一样我总结了一个排查顺序遇到问题直接按顺序查不要东一榔头西一棒子。现象可能原因处理方式打开 app 白屏Metro 无日志Metro 没启动或端口不通确认npm start在跑真机和电脑在同一局域网Metro 有日志但界面白屏JS 代码有运行时错误看 DevEco 的 Log 窗口找到红色报错堆栈状态栏可见但内容空白首屏渲染异步问题检查AppRegistry.registerComponent名称是否与入口配置一致真机白屏模拟器正常网络权限或防火墙检查 module.json5 权限关闭电脑防火墙测试Release 包白屏bundle 没内置或路径不对确认资源目录下是否存在bundle和assets文件我在鸿蒙真机上最常遇到的是第一种Metro 没连上。原因很朴素手机和电脑没有连同一个 WiFi或者电脑防火墙把 8081 端口拦了。判断方法也很简单先用手机浏览器访问http://电脑IP:8081试试能返回打包状态页面就说明网络通。第二常见的是 JS 运行时错误。鸿蒙环境下的 RedBox 弹窗不如 Android 那么明显有时候只是屏幕卡在白色界面真正的错误信息藏在 DevEco 的 Log 面板里。养成习惯白屏先看 Log而不是反复点击重新加载。5.3 模拟器与真机调试的常见坑模拟器调试非常香但也有限制。DevEco 自带的本地模拟器启动很慢经常要等一长串编译和系统启动过程第一次用要有心理准备。模拟器的性能和网络映射通常没问题适合验证布局和状态逻辑。真机调试的优势是能真实感受触摸和渲染性能尤其是按键反馈这类依赖触摸的交互只有真机测才准确。真机调试时需要注意鸿蒙设备开启开发者模式后USB 连接时要在手机上允许调试授权切到“传输文件”模式另外鸿蒙的调试授权有时会过期如果 DevEco 突然检测不到设备拔线重插、撤销授权重新信任多半能解决。这里再分享一个经验RN 鸿蒙开发里的热更新也就是 Fast Refresh虽然能用但偶尔会失灵。有时候你改了 JS 代码设备界面没任何变化这时候不要怀疑代码写错先手动重新加载一下 app。如果还是不行就把应用从后台划掉重开。切记不要在热更新失灵时反复折腾 DevEco 配置多数情况下只是 JS 层没有刷进来。6. 打包发布与后续可扩展方向6.1 生成可安装 HAP 包的操作路径开发调试通过后如果想把成果发给别人不能只让对方也配一套开发环境。你需要生成一个 HAP 安装包这是鸿蒙应用的安装载体。在 DevEco Studio 里找到 Build 菜单选择 Build Hap(s)/APP(s)会看到 Debug 和 Release 两类构建选项。Debug 包适合测试安装到自己的签名设备上Release 包则需要配置正式的证书和 Profile 才能签名发布。这里注意一个细节Release 打包时React Native 的 JS bundle 需要被内置到 HAP 里否则安装后的应用会因为连不上 Metro 而白屏因为开发服务器不会跟着 HAP 一起走。在 RC 的鸿蒙工程里通常需要把生成的 bundle 和 assets 资源放到 entry 资源目录下。具体路径在工程文档里有明确说明我建议在第一次 Release 打包前先仔细看一下 bundle 目录配置项别等包打出来白屏了才回头找原因。6.2 这个项目还可以怎么玩空调遥控器做完之后再往后扩展就不会觉得没方向了。我个人觉得下面几个方向性价比最高第一加动画。用 Animated 让温度数字跳动的时候有滚动效果或者让模式切换时按钮背景做一次淡入淡出整个遥控器的质感立刻上一个台阶。第二加语音能力。鸿蒙生态里语音交互是很重要的一环你可以试着把“打开空调”“升温”这类指令映射到现有的状态修改函数上本质上就是把语音识别结果变成 setState 调用思路非常顺。第三做多平台复用。同一套代码不只是跑在鸿蒙上由于 React Native 的特性它天然可以回到 Android 和 iOS。我第一次把项目迁移到 Android 环境时几乎没改业务逻辑这颗跨平台的种子在项目初期就种下了。第四如果你想更深入鸿蒙生态可以进一步了解元服务这样的新形态看看遥控器这类轻量工具适不适合用更轻的入口发布。这个话题现在是热点但它不是入门的重点建议先把基础跨平台流程打扎实再碰。最后分享一个我做这类项目时的个人习惯先把状态定义全部写在最顶层让所有交互逻辑先跑通界面先用最简单的文字布局等逻辑不再出错再回头调整 UI 细节和视觉效果。很多新手一上来就纠结按钮圆角、背景色这些视觉细节结果在逻辑层卡了半天体验其实很糟糕。功能先健壮再谈好看这个顺序能帮你省掉大量来回返工的时间。