ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

react-native-localize在OpenHarmony上的适配实践

react-native-localize在OpenHarmony上的适配实践 做React Native开发的都知道国际化这件事看着简单做起来全是细节。react-native-localize是我在多个多语言项目里一直在用的库它封装了设备语言、时区、货币符号、测量单位这些琐碎信息的获取一行getLocales()就能拿到一整份可用的Locale列表。但真正让我对它印象深刻的是最近一次把RN工程往OpenHarmony平台迁移时为了让它在新环境里跑起来前后折腾了一周。OpenHarmony这几年发展很快很多团队都开始评估把现有RN应用迁移过去。但迁移这件事最麻烦的从来不是RN框架本身——社区的react-native-openharmony适配层已经让大部分JS代码直接跑通——真正让人头疼的是三方库。纯JS实现的库还好说只要不依赖Web API基本都能直接用可一旦涉及原生代码比如react-native-localize这种需要通过平台能力读取系统信息的库就得做一套完整的桥接适配。这篇文章就围绕react-native-localize在OpenHarmony上的集成过程把三方库适配的思路、步骤、坑点全部梳理一遍。无论你是在做迁移评估还是已经卡在某个库的编译报错上这篇都能给你一个可落地的参照。1. 为什么偏偏是react-native-localize1.1 多语言项目躲不开的硬需求先聊聊这个库本身。在RN生态里做国际化的方案不少有人自己封装AsyncStorage存语言偏好有人用i18n-js或react-i18next管文案翻译。但不管用哪种方案都绕不开一个问题应用启动时你怎么知道用户当前设备用的是哪种语言react-native-localize解决的就是这个“最后一公里”。它提供了一套跨平台统一的API你不需要关心底层是Android的LocaleList还是iOS的NSLocale直接调用import { getLocales, getCurrencies, getTimeZone } from react-native-localize; const locales getLocales(); // 返回示例 // [{ languageCode: zh, countryCode: CN, languageTag: zh-Hans-CN, isRTL: false, ... }]它能拿到的不只是语言代码还包括完整的语言标签languageTag遵循BCP 47规范地区代码countryCode是否RTL语言阿拉伯语、希伯来语等需要镜像布局时区IANA格式比如Asia/Shanghai货币代码ISO 4217比如CNY温度单位和测量单位摄氏/华氏、公制/英制是否使用24小时制这些信息在App做本地化展示时一个都不能少。举个例子一个电商App的“预计送达时间”如果用户时区不对显示的时间就是错的一个内容社区App如果语言方向判断不对阿拉伯语用户的界面布局就会有严重问题。1.2 为什么选它作为OpenHarmony适配的切入点有的朋友可能会问OpenHarmony生态里值得适配的库那么多为什么单拿这个说事因为react-native-localize是一个非常典型的小而精原生模块。它的源码逻辑清晰原生侧代码量不大但又包含了RN原生模块桥接的全部要素——NativeModule注册、常量导出、跨语言类型映射。把它研究透了你在适配其他原生库时就有了一个可以参考的模板。对比一下其他类型的库react-native-safe-area-context虽然也涉及原生但核心偏UI组件的View管理逻辑相对复杂不适合初学者上手react-native-device-info暴露的API非常多一个库顶了几十个原生方法适配工作量大react-native-localize结构简单但它又依赖系统底层能力能完整体现“从JS调用到原生实现再回到JS”的完整闭环。换句话说这是最好的“教学案例”也是最容易跑通的“实战案例”。我这次适配的版本是RN 0.72 OpenHarmony SDK API 10的组合如果你用的版本略有不同思路完全一样细节上做对应调整即可。2. 拆解react-native-localize的桥接原理2.1 它是怎么工作的在动工之前我建议你先花半小时把react-native-localize的源码读一遍。别看它API简洁内部实现其实暗藏了不少细节。整体架构是这样的JS侧通过NativeModules.RNLocalize访问一个原生单例对象这个单例在模块初始化时一次性获取设备的所有语言、地区相关信息缓存到常量里。JS侧再提供getLocales()、getCurrencies()这些封装函数把原生层返回的数据整理成统一的JS对象结构。关键点在常量导出。你会发现这个库里的大部分数据都不是通过方法调用返回的而是在NativeModule初始化时就通过getConstants()一次性导出。这样做的优势是性能好——不需要每次调用都走一遍原生桥接缺点也明显——如果系统语言在App运行期间发生切换这些常量不会自动更新。为了解决这个问题库内部实现了一个事件监听机制原生侧监听系统语言变化触发RNLocalize事件JS侧收到事件后重新调用原生方法获取最新数据。// JS侧关键逻辑简化版 const RNLocalize NativeModules.RNLocalize; export function getLocales(): Locale[] { return RNLocalize.getConstants().locales; }2.2 OpenHarmony侧需要映射哪些系统能力理解了工作原理之后要回答的问题就清晰了OpenHarmony系统能不能提供同样的底层数据答案是大部分能但路径不同。OpenHarmony的开发生态和Android/iOS不一样它有一套自己的API体系。我对照着原生模块里要求的各个字段逐个确认了映射方案react-native-localize要求OpenHarmony系统API状态语言标签languageTagi18n.System.getDisplayLanguage结合getSystemLanguage可直接映射地区代码countryCodei18n.System.getSystemRegion可直接映射时区timeZonedateTime.getTimeZone可直接映射货币代码currencyCodei18n.System.getSystemCurrency可直接映射是否RTLi18n.System.isRTL低版本可能缺失24小时制i18n.System.is24HourClock低版本可能缺失温度单位需要自定义映射需自行实现这里面的坑在于OpenHarmony不同API版本的能力差异很大。API 9的ohos.i18n模块提供了基础的语言、地区、时区获取但像is24HourClock这种能力部分低版本SDK并没有暴露。我的做法是在原生侧做能力降级判断拿不到真实值时返回合理的默认值保证JS侧不会因为字段缺失而崩溃。2.3 静态信息与动态更新的取舍适配过程中我一直在想一个问题为什么不用TurboModule的新架构而选择传统NativeModule后来想明白了。react-native-localize这个场景下传统NativeModule完全够用而且兼容性更好。OpenHarmony上的RN适配层新架构Fabric TurboModule的支持还处于逐步完善阶段传统架构反而是最稳定的路径。但也有例外。如果你要适配的是高频调用的API比如获取传感器数据TurboModule的同步调用能力就有价值了。传统NativeModule的异步桥接在这种场景下性能损耗明显。特此记一笔适配前先评估调用频率再决定桥接架构。3. 实操从零到getLocales()跑通3.1 环境准备与脚手架选型先交代一下我的环境方便你对照操作系统Ubuntu 22.04macOS / Windows也可以编译差异不大OpenHarmony SDKAPI 104.0 ReleaseDevEco Studio4.0Node.js18.18.0React Native0.72.6在OpenHarmony上跑RN目前主流的做法是用社区维护的脚手架创建工程。你可以直接从一个空RN工程开始然后通过react-native-oh-tpl/cli来初始化OpenHarmony平台支持# 初始化RN工程 npx react-native-community/cli init RnLocalizeDemo # 进入工程目录添加OpenHarmony平台支持 cd RnLocalizeDemo npx react-native-oh-tpl/cli init这样生成的项目结构里会多出一个harmony目录——这就是OpenHarmony的原生工程壳子和iOS的ios/、Android的android/目录一个性质。RN JS侧代码和正常工程完全一样原生侧则是HarmonyOS的ArkTS工程。注意创建工程前先确认你的Node和RN版本匹配。RN官方脚手架对Node版本有明确要求社区适配层还会再叠加一层版本要求最好直接用npx react-native info检查一遍。3.2 锁定三方库版本矩阵版本问题是我这次踩的第一个坑这里单独拿出来说。react-native-localize的官方版本比如3.x在OpenHarmony上直接用是会报错的因为它的原生代码针对的是Android/iOS系统API。正确的做法是安装社区适配版npm install react-native-oh-tpl/react-native-localize安装这个包时你可能会疑惑名字怎么和原版不一样其实它就是在原版的基础上增加了OpenHarmony的原生实现代码JS API完全兼容。安装后你代码里依然是从react-native-localize导入import { getLocales } from react-native-localize;社区适配包的package.json里通过别名机制让这个包“冒充”了react-native-localize你不需要改任何业务代码。这里推荐一个经验在任何RN三方库集成前先去npm仓库搜一下react-native-oh-tpl/包名看有没有对应的适配版。目前的适配生态已经覆盖了async-storage、gesture-handler、safe-area-context这些常用库。官方还维护了一个兼容性列表查一下能省掉大量自行适配的功夫。3.3 原生模块的自动链接与手动检查安装完成后理论上RN的autolinking机制会自动完成原生模块的链接。但在OpenHarmony平台上有个环节经常出问题——自动链接不会自动生效。因为OpenHarmony的RN工程原生侧不是Gradle自动管理的你需要手动检查harmony/entry/oh-package.json5确认依赖是否已经写入{ dependencies: { rnoh/react-native-openharmony: ./react-native-openharmony, // 三方库适配包需要出现在这里 react-native-oh-tpl/react-native-localize: file:../../node_modules/react-native-oh-tpl/react-native-localize } }如果发现没有自动写入手动补上然后执行cd harmony hvigorw assembleHap这个过程会重新生成原生侧的模块索引文件。如果一切顺利RNLocalize这个NativeModule就会被注册到运行时里。注意hvigorw assembleHap的编译时间取决于你的机器性能我第一次全量编译花了差不多5分钟。编译报错时先看是不是路径问题file:开头的相对路径在window上偶尔会解析异常建议统一用正斜杠。3.4 写一个最简单验证页面原生侧编译通过后在App.tsx里写一个最简单的验证页import React, { useEffect, useState } from react; import { View, Text, Button } from react-native; import { getLocales, getTimeZone, getCurrencies } from react-native-localize; function App() { const [info, setInfo] useState(); const loadLocalizeInfo () { const locales getLocales(); const timezone getTimeZone(); const currencies getCurrencies(); setInfo( JSON.stringify( { locales, timezone, currencies, }, null, 2, ), ); }; useEffect(() { loadLocalizeInfo(); }, []); return ( View style{{ flex: 1, justifyContent: center, padding: 20 }} Button title重新获取 onPress{loadLocalizeInfo} / Text style{{ fontSize: 12 }}{info}/Text /View ); } export default App;把应用跑起来点击按钮如果控制台输出了类似这样的JSON{ locales: [ { languageCode: zh, countryCode: CN, languageTag: zh-Hans-CN, isRTL: false } ], timezone: Asia/Shanghai, currencies: [CNY] }说明桥接已经成功getLocales()全家桶在OpenHarmony上已经可用了。但如果你的输出是undefined、null或者干脆抛异常那就进入下一节的排查环节。4. 常见问题与排查技巧实录4.1 “Native module cannot be null”的排查路径这是集成中最常见的报错字面意思是RN运行时找不到RNLocalize这个原生模块。我从日志里截一段真实的错误Error: NativeModule: RNLocalize is null.出现这个报错按顺序排查第一确认适配包是否安装成功。去node_modules/react-native-oh-tpl/react-native-localize目录下看一眼确认里面有harmony子目录。如果只有android和ios目录说明你装错了安装的还是原版。第二确认原生工程是否链接成功。打开harmony/entry/oh-package.json5确认依赖已经写入。我遇到过一个诡异情况执行npm install后oh-package.json5被自动更新了但版本号带^符号导致原生编译时拉取到了不兼容的新版本。建议直接固定版本号不要用^范围匹配。第三检查原生侧模块索引。OpenHarmony的RN适配层有一个自动生成的模块列表文件路径一般在harmony/entry/src/main/cpp/RNOhModules.cpp或类似位置。如果适配包没有出现在这个文件里你需要手动注册。这步在新版本适配层里已经很少遇到了但如果你用的是较早版本还是会撞上。4.2 语言切换后不刷新的问题react-native-localize在Android和iOS上都会注册系统语言变化监听事件App回到前台时会自动触发更新。但在OpenHarmony的适配版里这个监听事件需要单独确认因为OpenHarmony的系统事件回调机制和Android不完全一样。我实测下来的表现是在OpenHarmony系统设置里切语言回到App后getLocales()返回的还是旧语言。排查后发现适配包原生侧没有实现语言切换的监听方法。绕开适配包我直接在业务侧做了一个兜底方案import { AppState } from react-native; // 在App重新回到前台时强制刷新语言信息 useEffect(() { const subscription AppState.addEventListener(change, (state) { if (state active) { // 重新调用getLocales刷新缓存 reloadI18nResources(); } }); return () subscription.remove(); }, []);这个方法不优雅但胜在稳定。在适配包的监听能力补全之前这是最靠谱的临时方案。4.3 部分API返回空值需要兜底默认值我测试了不同系统版本发现一个规律API 10以下的OpenHarmony上getTemperatureUnit()和uses24HourClock()大概率返回null或空值。原因是OpenHarmony的低版本SDK中没有暴露对应的系统API适配包拿不到数据只能返回空。这个问题的根治方案是等适配包更新但做项目不能干等我选择在业务侧加归一化逻辑import { getTemperatureUnit, getLocales } from react-native-localize; export function getSafeTemperatureUnit() { const unit getTemperatureUnit(); if (!unit) { // 根据地区推断默认单位比如美国默认华氏其他地区默认摄氏 const locale getLocales()[0]; return locale?.countryCode US ? fahrenheit : celsius; } return unit; }这类兜底逻辑建议统一封装在一个localizeHelper.ts文件里业务侧不要直接调用库的API一来方便统一处理兼容性问题二来以后适配包更新了删除兜底逻辑也方便。4.4 顺带解决一次模拟器上的渲染异常在OpenHarmony的x86模拟器上联调时我遇到了另一个和react-native-localize无关、但大概率会在OpenHarmony开发中遇到的现象——页面渲染异常。具体表现是打开页面后出现花屏、闪烁色块滑动列表时画面撕裂严重。和react-native-localize没关系但没有它在模拟器上联调我也不会撞上这个坑。先说结论这基本是x86模拟器的GPU虚拟化渲染兼容问题不是RN适配层的bug。排查路径是真机上跑同一段代码渲染完全正常排除JS层逻辑问题模拟器上降低动画帧率异常频率下降说明和渲染管线有关最终方案在模拟器设置里关掉“硬件加速”选项改为软件渲染问题解决。如果你们团队也用模拟器做日常联调我的建议是在x86模拟器上只做JS逻辑调试画面UI检查以真机为准。模拟器的渲染管线本身就和真机有差异纠结于模拟器上的花屏意义不大。4.5 排查速查表最后汇总一张速查表方便后续排查现象可能原因排查动作NativeModule is null适配包未安装或未链接检查oh-package.json5依赖getLocales()返回空数组原生层常量导出失败看原生日志有没有报错语言切换不生效缺少系统监听事件用AppState做兜底刷新温度/24小时制返回空低版本SDK能力缺失业务侧做默认值兜底x86模拟器花屏GPU渲染兼容问题改用软件渲染或真机验证5. 后续维护与生态思考5.1 把localize能力封装成自己的模块适配跑通只是第一步。后面做多语言切换、RTL布局适配时我越来越觉得直接在业务里散落地调用第三方API是坏味道。所以我把react-native-localize的能力重新封装了一层对外暴露的API全部是业务语义的// i18n/locale.ts import * as RNLocalize from react-native-localize; export interface AppLocale { languageTag: string; languageCode: string; countryCode: string; isRTL: boolean; } export function detectAppLocale(): AppLocale { const locales RNLocalize.getLocales(); const best RNLocalize.findBestLanguageMatch([ { languageTag: zh-Hans, isRTL: false }, { languageTag: en-US, isRTL: false }, ]); return { languageTag: best?.languageTag ?? en-US, languageCode: locales[0]?.languageCode ?? en, countryCode: locales[0]?.countryCode ?? US, isRTL: best?.isRTL ?? false, }; }这样做的收益是当react-native-localize适配包更新、API发生变化时我只需要改这一个文件业务代码零改动。另外findBestLanguageMatch这个API是它的亮点用来做语言智能匹配非常方便能在用户没有明确设置首选语言时通过优先级列表自动选择最合适的语言包。5.2 OpenHarmony三方库适配的通用方法论这次适配给我的最大收获其实是沉淀了一套RN三方库在OpenHarmony上的适配判断框架。拿到任意一个三方库先问几个问题第一有没有原生代码纯JS库直接拿到OpenHarmony上用大概率没问题。你可以通过看package.json里有几个平台目录判断。第二原生代码依赖了哪些系统能力如果是加密依赖keystore、蓝牙、定位这种深度系统能力适配成本会很高。像react-native-localize这种只是读取系统配置的属于中等难度。第三社区有没有人已经做过了搜一下react-native-oh-tpl/前缀的包存在就说明已经有项目踩过坑了直接用能省很多时间。如果还没有适配包就得评估自己动手的成本了。5.3 测试策略不能只在真机上验证语言和地区的适配比普通功能更需要“穷举”。我强烈建议你在集成测试阶段做一个矩阵验证至少覆盖这几种场景简体中文 中国大陆 Asia/Shanghai英语 美国 America/New_York阿拉伯语 沙特阿拉伯 Asia/Riyadh重点验证RTL返回日语 日本 Asia/Tokyo繁体中文 中国台湾 Asia/Taipei为什么要单独提阿拉伯语因为isRTL这个字段如果返回错误整个页面布局的方向会完全错乱这种问题在模拟器上很难暴露必须用真机、切真实的阿拉伯语环境才能发现问题。如果你用的是模拟器还需要额外注意模拟器设置里的“语言”选项能不能真正同步给系统API。有时候模拟器上切换语言只改了UI系统底层的Locale并没有变化这也会导致联调时的误判。这里说一个最后的小经验适配react-native-localize的过程中我对OpenHarmony的ohos.i18n模块API反而比官方文档更熟了。用系统提供的i18n.System.getSystemLanguage()和RN侧的返回值交叉验证能很快定位桥接层的问题到底出在原生侧还是JS侧。这个方法建议你也试试。另外一个实用的做法是在loadLocalizeInfo那段代码里加上异常捕获。万一某个API在特定设备上抛异常逻辑能优雅降级至少给用户显示默认语言而不是白屏崩溃。以上就是一个RN老油条在OpenHarmony上集成react-native-localize的全过程。这个库只是个开始等你把async-storage、gesture-handler这些常用组件都跑通之后会慢慢建立起对OpenHarmony生态的信心。适配的过程本质上是学习一个新平台的过程踩过的坑都会成为团队后续迁移效率的保障。
返回列表