ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

React Native鸿蒙迁移:bundle白屏根因与排查

React Native鸿蒙迁移:bundle白屏根因与排查 去年底开始我们团队有一个跨端项目需要落到鸿蒙设备上。技术栈固定在 React Native于是先想到了一个问题RN 打出来的 bundle 文件能不能直接在鸿蒙的 DevEco Studio 工程里跑起来这里说的 DevEco Studio就是华为官方的鸿蒙应用开发 IDE不少人手滑打成 DevEvo Studio标题里这个拼法就是这么传开的。整个迁移过程里最折磨人的不是环境搭建也不是容器集成而是一切看起来都正常但 run 起来就是白屏。先后排查了 bundle 文件是否进包、是否有日志、是否资源缺失最终才定位到根因从 Android 构建产物里拷出来的 bundle其实是 Hermes 字节码不是 JS 源码。这篇文章把完整的现象、排查思路、定位过程和解决方法都记录下来给正在做同样迁移的同学一个参照。1. 为什么要把 React Native 的 bundle 搬进鸿蒙迁移背后的真实背景1.1 项目诉求一套业务代码尽可能多的运行平台以前跨端项目主要盯 Android 和 iOS业务逻辑、组件、状态管理全都沉淀在 React Native 代码里。鸿蒙这边用户量在涨业务不能不做但如果为鸿蒙单独维护一整套原生代码开发、测试、上架的成本都要翻倍小团队根本扛不住。所以最理想的方案是业务代码不变鸿蒙端只提供一个能加载 RN bundle 的容器。这里需要先理清一个概念React Native 的产物是 JS 文件也就是 bundle它本身不依赖某个具体的操作系统靠的是宿主环境提供的 JS 引擎和原生能力桥接。因此在鸿蒙上跑 RN本质上就是把宿主环境从 Android/iOS 换成鸿蒙。鸿蒙提供 ArkTS 和原生 C 能力RN 容器完全可以构建在这套能力之上。另外要说明一点HarmonyOS NEXT 已经不支持直接加载 Android 的 APK也没法去复用安卓里的 RN 容器。想跑 React Native只能走鸿蒙原生容器 RN JS bundle这条路。这也是为什么必须在 DevEco Studio 里做集成编译而不是简单拿个 APK 塞进去。1.2 技术选型为什么用社区维护的 react-native-harmony做技术选型的时候摆在我面前无非两条路一是自己基于 ArkTS/ArkUI 写一套 RN 运行时桥接层二是直接用社区已经适配好的方案。自己写的成本极高RN 的原生模块、事件机制、渲染映射、网络栈全都要重新对接不是一个人短期内能完成的。社区这边OpenHarmony 生态里已经有人在做 RN 适配形成了 react-native-harmony 这样的框架接口尽可能对齐 React Native 官方 API。实际集成的时候发现它的工作方式跟 RN 官方在 Android 上的容器很像初始化一个 RNApp 或者 RNInstance 对象指定 bundle 名称和入口组件然后交给 JS 端渲染。听起来不难但正是这种看起来不难让我在后面吃了不少苦头。因为框架只负责把容器搭起来bundle 文件怎么生成、怎么放、怎么命名都需要你自己确认。任何一步不匹配最终表现就是白屏。1.3 DevEco Studio 在整条链路里的作用DevEco Studio 是华为官方的鸿蒙应用开发 IDE基于 IntelliJ IDEA 二次开发支持 ArkTS、C、Java 等语言负责鸿蒙应用的工程管理、编译、签名、模拟器和真机调试。在 RN 迁鸿蒙这个场景里DevEco Studio 承担的角色是把鸿蒙容器代码和 RN bundle 一起打包成 hap 安装包。你可以把它理解成 Android 开发里的 Android Studio只不过最终产物是鸿蒙的 hap不是 apk。这里有一个关键点如果只是把 bundle 文件放到鸿蒙工程里编译的时候bundle 的二进制内容本身不会被二次处理。它会被当成 rawfile 资源原样打进 hap真正参与编译的是容器代码。很多人以为 IDE 会做 RN 优化其实不会bundle 是什么格式进包就是什么格式。这个认知在后面排查 Hermes 字节码问题时非常重要。2. 打包与集成Metro、bundle 和鸿蒙工程的对接方式2.1 从 RN 到 bundleMetro 打包命令的关键参数React Native 的 JS 代码默认由 Metro 打包器输出成 bundle 文件。在 Android/iOS 工程里这个动作通常由 Gradle 或 Xcode 自动完成但鸿蒙工程不会主动帮你做所以需要手动执行打包命令。我当时用的命令大致是这样npx react-native bundle \ --platform android \ --dev false \ --entry-file index.js \ --bundle-output ./dist/index.bundle \ --assets-dest ./dist/assets几个参数分别解释一下--platform声明目标平台。这里写 android 还是 harmony取决于适配层有没有提供对应的平台解析规则。如果配置里没有单独映射 harmony沿用 android 的解析方式通常也能跑因为 RN 业务代码里很少用平台专属分支即便有也可以通过Platform.OS判断。--dev false产出 release 模式的 JS 代码关闭调试器连接和部分 dev-only 逻辑。--entry-file指定入口文件一般是 index.js。--bundle-output是 bundle 文件的输出路径。--assets-dest是静态资源输出目录图片、字体等资源会按相对路径放好。这一步最需要注意的是不同 RN 版本、不同适配层对 bundle 文件名的预期可能不一样。有的工程固定要index.bundle有的要main.jsbundle。别想当然先去看容器初始化代码里写的名字再决定打包参数。2.2 bundle 在鸿蒙工程里的落位rawfile 目录Metro 打包完成之后得到dist/index.bundle和dist/assets目录。接下来把它们放进鸿蒙工程。默认鸿蒙工程里entry 模块的资源目录是entry/src/main/resources其中 rawfile 子目录专门放原样打包、不做编译处理的文件bundle 就应该放这里。目录结构大概长这样entry/src/main/ ├── ets/ │ ├── entryability/ │ └── pages/ ├── resources/ │ ├── base/ │ └── rawfile/ │ ├── index.bundle │ └── assets/把这两个东西放进去之后DevEco Studio 构建 hap 时会把 rawfile 中的内容原封不动地打包进产物。这里有一个小坑Metro 的 assets 输出是按平台路径组织的比如assets/src/...如果打包时用--platform android资源内部路径可能包含drawable-mdpi之类的目录。鸿蒙容器加载资源的时候通常使用相对 bundle 文件位置的路径对目录结构的要求和 Android 不完全一致。所以打包后最好检查一下 assets 目录的结构确保和容器代码里引用的资源路径对得上。2.3 容器初始化代码bundle 名称和入口的对应关系bundle 文件放进 rawfile 之后还需要确认鸿蒙容器那边初始化时指向的文件名和入口组件。在 react-native-harmony 的集成代码里一般会看到类似这样的初始化逻辑new RNApp(MyRNApp, { bundleName: index.bundle, entryComponent: App, })这里的bundleName必须和 rawfile 里的文件名完全一致大小写也要一致。否则运行时会去加载一个不存在的文件直接失败。我第一次运行就栽在这里rawfile 里放的是index.bundle但初始化代码里写的是index.android.bundle结果运行时提示 Unable to load script。这种问题属于低级错误但往往不容易一眼发现因为 IDE 不会报编译错误只有运行日志里才会暴露出来。3. 现场还原白屏、加载失败与日志排查链路3.1 现象页面白屏没有红屏也没有错误弹窗把 bundle 文件放好、初始化代码对齐之后我在 DevEco Studio 里构建出 hap装到模拟器上启动结果页面一片空白。没有红屏警告RN 在开发模式经常出现的那种没有弹窗进程也没崩溃。就感觉 JS 完全没有执行或者执行了但 UI 渲染不出来。最让人头疼的是模拟器上没有任何明显的 JS 异常提示。我当时的第一反应是是不是容器初始化的时序问题导致页面挂载失败于是反复调整入口组件和生命周期改了好几版还是白屏。这里提醒一下还在用模拟器的朋友鸿蒙模拟器对运行环境的支持目前还有限制有些版本只能在 arm64 平台上跑 JS 运行时如果开发机是 x86 架构用模拟器就可能遇到环境层面的不兼容。排查问题时先确认模拟器本身能正常跑一个 hello world 鸿蒙应用再往下查 RN 层的问题不然很容易被误导。3.2 第一步排查bundle 文件到底有没有进 hap解决白屏问题第一步不是看代码而是确认最终的 hap 安装包里到底有没有我们放的 bundle 文件。因为有时候 rawfile 路径放错了或者构建缓存没刷新bundle 根本没被打进安装包模拟器启动后容器在加载路径上找不到文件自然白屏。我当时的验证方法很简单构建完成后直接从 DevEco Studio 的 build 输出目录找到entry-default-signed.hap用解压工具打开检查resources/rawfile/下有没有index.bundle和assets目录。如果发现没有问题多半出在rawfile 目录路径写错比如放到了resources/base下面构建缓存没刷新先试一下 Build 菜单里的 Clean Project配置了多模块工程但 bundle 放到了非 entry 模块的 rawfile 里导致最终 hap 没包含它。我这次的情况是bundle 确实打进去了文件大小也对得上所以这一步可以排除。3.3 用 hdc 抓日志hilog 里的关键线索排除了文件没进包之后就得看运行日志了。鸿蒙系统有自己的日志系统 hilog对应 Android 的 logcat。连接模拟器或真机后先清空缓冲区再启动应用避免被历史日志淹没hdc shell hilog -r hdc shell hilog | grep ReactNativeJS通过ReactNativeJS这个标签可以过滤出 RN 容器里 JS 层的日志和异常信息。当时的输出大概是这样E ReactNativeJS: Unable to load script. Make sure youre either running Metro (try npx react-native start) or that your bundle index.bundle is packaged correctly for release.这一句非常经典基本上等于告诉你bundle 文件加载失败了。但这个报错没有给出具体原因——是文件不存在还是格式不对还是文件内容有问题它只说要么你去跑 Metro要么确认 bundle 正确打包。到这里排错思路就清晰了Metro 肯定没在跑我们是离线加载 bundle所以问题出在 bundle 文件本身。可这个 bundle 是从我们正式的 RN 构建流程里出来的Android 端验证过没有任何问题。那为什么 Android 正常鸿蒙就不行这是我接下来一直在想的问题。4. 根因定位这份 bundle 是 Hermes 字节码不是 JavaScript4.1 为什么 Android 上正常、鸿蒙上就白屏这里要回到 Android 端的一个构建细节。现在的 React Native 在 Android 上默认使用 Hermes 作为 JS 引擎。Hermes 有一个特点它可以把 JS 源码预编译成字节码在启动时直接加载字节码省去解析和执行脚本的开销从而加快冷启动。在 Android 的 release 构建流程里Gradle 插件会自动调用 Hermes 编译器把 Metro 输出的 JS bundle 转成 Hermes 字节码文件文件名通常还是index.android.bundle。所以很多团队从 Android 构建产物目录里拿到的bundle本质上已经不是 JS 源码了。我当时犯了同样的错误为了省事直接从 Android 的构建产物里复制了index.android.bundle觉得反正都是同一个 RN 项目打出来的包鸿蒙应该也能用。这个认知在鸿蒙这条路线上完全不成立。4.2 用文件头魔数确认 Hermes 格式要确认一个 bundle 到底是 JS 还是 Hermes 字节码不需要复杂的工具。Hermes 字节码文件有固定的文件头魔数十六进制下前四个字节是c6 1f bc 03而纯 JS bundle 文件的第一行通常以注释开头内容类似var __BUNDLE_START_TIME__或者/**。在终端里执行xxd index.android.bundle | head -n 3如果输出像这样00000000: c61f bc03
返回列表