React Native for OpenHarmony 实战:三方库 react-native-phone-input 的鸿蒙化适配指南 本文记录把react-native-phone-input带国家选择的手机号输入框适配到 HarmonyOS 的完整过程。它做的事一个输入框 国家旗帜 下拉选国家内部用google-libphonenumber做号码解析/校验/格式化。结论可用而且是这几轮里代码最干净、判据最扎实的一次。预检的九项检查老写法 ListViewViewPropTypesText.propTypesmeasureuseNativeDriverNativeModulesPlatform.OSisRequired全部为空只缺一个 peerreact-native-picker/picker含原生滚轮⇒ 我用纯 JS 桩隔离CountryList实测恰好 243 个国家、CountryList[0]的iso2af/dialCode93全部命中最硬的一条是setValue(8613800138000)之后界面上的TextInput的值真的变成了8613800138000✓本轮还有一个方法论收获从.d.ts抄方法清单时要分清原型方法和箭头类字段——我把 20 个方法一股脑断言都在prototype上结果有 6 个不在而那 6 个正是箭头类字段。★★★★★ 一、预检九项检查全部为空这几轮里最干净的库预检项结果createClass/React.PropTypes/ViewPropTypes/Text.propTypes/ 字符串 ref /this.refs/mixins/componentWillReceiveProps/componentWillMount/getDefaultProps✅✅全部为空ListView.DataSource/ListView✅ 空measure()/onLayout/useNativeDriver/NativeModules✅ 全部空Platform.OS✅空连平台判断都不需要isRequired✅ 空dependencies⚠️google-libphonenumber纯 JS需装lodash宿主已有peerDependencies⚠️react-native-picker/picker 2.0含原生滚轮⇒ 需桩scripts.prepare⚠️npm run build⇒必须--ignore-scripts交付包✅dist/完整PhoneInput.js31262 B /CountryPicker.js14345 B /PhoneNumber.js12790 B /countries.json39 KB /resources/flags/*.png 全套.d.ts⇒⇒ 这是至今预检最干净的库没有任何已移除 API也没有平台分支。★★★★ 二、唯一的 peer用 30 行纯 JS 桩隔离原生滚轮react-native-picker/picker的用法只有两处dist/CountryPicker.jsL34 / L99constPickerItemrequire(react-native-picker/picker).Picker.Item;// L34Picker style{…}selectedValue{…}onValueChange{…}{items}/Picker// L99⇒ 桩只需提供Picker组件与Picker.Item组件constPickerItem({label,value})Text style{{display:none}}{label}/Text;constPickerReact.forwardRef(({children,selectedValue,style,...rest},ref){// 把 children 里与 selectedValue 匹配的那一项的 label 找出来constselectedLabel/* 遍历 children 找 props.value selectedValue */;returnView ref{ref}style{[{minHeight:24,justifyContent:center},style]}{...rest}Text{selectedLabelnull?PICKER 桩未选中:PICKER 桩selectedLabel}/Text/View;});Picker.ItemPickerItem;两个设计要点退化成把选中项显示成一行文字—— 这样选了哪个国家在布局 dump 里也能看到Picker上额外挂了focus/blur两个空方法—— 避免调用方一调用就崩。⇒ 通过metro.config.js的extraNodeModules顶掉库代码零改动✓★★★★★ 三、契约15 条 PASS含两个精确数字NOTE | 默认导出 PhoneInput 的 typeof function PASS | ★★ 默认导出 PhoneInput 是 classfunction PASS | ★ PhoneInput 有 prototype.render NOTE | CountryPicker 的 typeof function PASS | ★★ CountryPicker 是 classfunction PASS | ★ CountryPicker 有 prototype.render NOTE | CountryList 是否数组 true PASS | ★★ CountryList 是数组 NOTE | CountryList 的 length 243 PASS | ★★★ CountryList 恰好 243 个国家 ← ★ 精确数字命中 NOTE | CountryList[0] 的键 name, iso2, dialCode, priority, areaCodes NOTE | CountryList[0].iso2 af NOTE | CountryList[0].dialCode 93 PASS | ★★ CountryList[0].iso2 af PASS | ★★ CountryList[0].dialCode 93 NOTE | PhoneNumber 的 typeof object PASS | ★★ PhoneNumber 是 object PASS | ★ PhoneInput 有 static setCustomCountriesData⇒⇒243这个数字不是猜的—— 是预检时在 PC 端把countries.jsonConvertFrom-Json数出来的数组长度 243✓⇒⇒CountryList[0]的iso2af/dialCode93也是从那一项读出来的 ✓⇒这就是精确数字判据的正确做法先在设备外拿到真值再在设备上断言同一个值✓★★★★★ 四、一个方法论上的收获.d.ts里要分清原型方法与箭头类字段我从dist/PhoneInput.d.ts抄了 20 个公开方法一股脑写成都在prototype上FAIL | ★★★ 20 个公开方法都在 prototype 上 | 期望0 | 实际6 NOTE | prototype 上缺失的方法 onChangePhoneNumber, onPressFlag, getFlag, getISOCode, selectCountry, setValue再看一遍.d.ts就明白了—— 这 6 个的写法是箭头类字段onChangePhoneNumber:(number:any)void;// ← 箭头类字段onPressFlag:()void;getFlag:(iso2:any)any;getISOCode:()any;selectCountry:(iso2:any)void;setValue:(number:any)void;而其余那些是普通方法签名getPickerData():any;// ← 普通方法在 prototype 上getCountryCode():any;getAllCountries():any;getDialCode():string;getValue(text?:any):any;getNumberType():any;isValidNumber():any;format(text:any,iso2?:any):any;⇒⇒ 所以 6 个缺失完全正常它们是箭头类字段、挂在实例上、不在prototype上✓这条规则现在有三档实测写法在prototype上在实例上实测出处普通方法getX() {}✅ 有✅经原型链本轮getCountryCode/formatconstructor里bind✅ 有✅ 有own keyrn-68 实测箭头类字段x () {}❌没有✅有own key本轮 6 个 rn-72 的 lightbox⇒教训从.d.ts抄清单时看name: (…) …还是name(…): …—— 前者是实例字段、后者是原型方法。⇒更稳的做法清单照抄断言时同时查prototype[name]与实例inst[name]inst[name] ! undefined对两者都成立。★★★★★ 五、最硬的证据setValue真的把号码写进了输入框我在时间轴里调用inst.setValue(8613800138000)然后读布局 dump 里的TextInputTextInput#TextInput(56)0x… [183,1059,1257,1179] 8613800138000 clickable longClickable scrollable⇒⇒TextInput的值变成了8613800138000✓✓⇒ 这说明三件事同时成立setValue()方法存在且被正确调用它是箭头类字段在实例上 ✓google-libphonenumber在鸿蒙上正常工作setValue内部要做解析/格式化库的内部状态真的流到了渲染层不是调用成功但界面没变⇒⇒ 这种值出现在TextInput里的证据比方法没抛错强得多✓⇒ 也顺带解释了为什么placeholder(“PHI 手机号占位”)找不到——输入框里已经有值了placeholder 自然不显示✓这是正确行为不是缺陷★★★ 六、CountryList的结构243 个国家CountryList[0] {name: Afghanistan (افغانستان), iso2: af, dialCode: 93, priority: 0, areaCodes: null} 键 name, iso2, dialCode, priority, areaCodes⇒priority用于把常用国家排到前面如 US/CN/GB⇒areaCodes用于同一国家内的区号细分大多数为null⇒dialCode不含93而不是93—— 而实例方法getDialCode()返回的带。★★ 七、其余预检结论预检项结果外部 import只有reactreact-native真实依赖google-libphonenumber/lodash在 dist 里按需 require资源dist/resources/countries.json39 KBdist/resources/flags/images/*.png240 张旗帜图PhoneNumber导出是objectgoogle-libphonenumber的封装static setCustomCountriesData✅ 存在可替换国家数据源八、接入与预检要点# ★ 必须 --ignore-scripts它的 scripts.prepare 是 npm run buildtscnpminstallgithttps://atomgit.com/oh-react-native/react-native-phone-input.git#1.4.1-ohos-1.0.0--ignore-scripts# ★ 显式装它的运行时依赖file: 接入不会自动装npminstall--ignore-scripts google-libphonenumber^3.2.32# ★ peerreact-native-picker/picker 含原生 ⇒ 用纯 JS 桩隔离见正文第二节九、交付包质量与基线说明类别结果交付包内容dist/完整4 个组件的.js 全套.d.tscountries.json39 KBresources/flags/240 张 png✓270 文件spec.json⚠️缺upstreamCommitvalidation⚠️build-pass-device-pending⚠️ 声明的基线reactNative: 0.88.0rnoh: 0.84.4⇒两字段自相矛盾备注库代码零改动关于基线本次验证在RN0.84.1 RNOH0.84.3上完成。本库没有任何平台分支Platform.OS零命中⇒结论跨基线成立✓十、已知限制与本次验证边界10.1 用这个库要知道的事项说明★★★★★react-native-picker/picker是 peerDep含原生滚轮 ⇒ 要装官方鸿蒙化版本或用纯 JS 桩隔离桩需提供PickerPicker.Item★★★★★ 6 个方法是箭头类字段onChangePhoneNumber/onPressFlag/getFlag/getISOCode/selectCountry/setValue不在prototype上在实例上★★★★★CountryList有 243 个国家每项{name, iso2, dialCode, priority, areaCodes}dialCode不带★★★★setValue()会真的把号码写进输入框实测TextInput的值变成8613800138000★★★getDialCode()带与CountryList[i].dialCode不带不同★★★ 有static setCustomCountriesData(json)可替换国家数据源★★scripts.prepare会跑tsc安装必须--ignore-scripts★★ 资源是本地 PNG 旗帜图dist/resources/flags/images/*.png240 张打包时需一并处理10.2 本次验证的边界★★★ 未验证点旗帜弹出国家选择器需要真实触摸 picker 桩的实际交互★★★ 未验证真实键盘输入onChangePhoneNumber未被用户输入触发只验证了setValue编程路径★★ 未验证 240 张旗帜 PNG 在鸿蒙上的加载只确认了文件存在未逐张渲染★★ 未验证isValidNumber()/getNumberType()的返回值B 段日志本轮未完整抓到★★ 未验证format()的各种格式参数未验证initialCountry切换只用us未验证setCustomCountriesData()替换数据源未在RN 0.88.0/RNOH 0.84.4上复验未测其它屏幕宽度未做跨平台对照。库代码未改动新增的是stubs/react-native-picker.js。十一、常见问题Q1它在 RN 0.84 上能跑吗能而且很干净。九项预检全空、无平台分支、无原生代码契约 15 条 PASS含CountryList恰好 243 个国家setValue(8613800138000)后界面TextInput的值真的变成了8613800138000LogBox无通知。Q2Cannot find module react-native-picker/picker它是 peerDependency 且含原生滚轮。两个选择① 装官方鸿蒙化版本②用纯 JS 桩隔离只需Picker与Picker.Item两个组件正文第二节有完整代码。Q3setValue/getISOCode在prototype上找不到它们是箭头类字段setValue (number) {…}挂在实例上、不在prototype上。判断方法看.d.ts里是name: (…) …字段还是name(…): …原型方法。断言时直接查实例typeof inst.setValue function。Q4placeholder不显示因为输入框里已经有值了。本轮的setValue(8613800138000)让TextInput的值变成该号码placeholder 自然不显示 —— 这是正确行为不是缺陷。Q5getDialCode()和CountryList[i].dialCode为什么不一致CountryList[i].dialCode是裸数字如93getDialCode()返回带如93。拼接 E.164 号码时要注意这一点。Q6国家列表能换吗能用静态方法importPhoneInputfromreact-native-phone-input;PhoneInput.setCustomCountriesData([{name:中国,iso2:cn,dialCode:86,priority:0,areaCodes:null}]);实测static setCustomCountriesData存在且是函数 ✓Q7安装时为什么要--ignore-scripts它的scripts.prepare npm run build要跑tsc把src编译到dist。而交付包里已经带了完整的dist/⇒ 不需要再编译直接--ignore-scripts更快也不会失败。小结react-native-phone-input是带国家选择的手机号输入框预检九项全空、无平台分支、无原生代码—— 这几轮里最干净的一个。它真正的依赖只有两个google-libphonenumber纯 JS负责号码解析/校验/格式化和peer 的react-native-picker/picker含原生滚轮用桩隔离。最值得记下来的是这几条★★★★★ 「精确数字判据」的正确做法先在设备外拿到真值再在设备上断言同一个值。我在 PC 端把countries.json解析出来数组长度 243、[0] {iso2:af, dialCode:93}然后在设备上断言CountryList.length 243、CountryList[0].iso2 af⇒一次全中✓✓⇒ 这比断言 length 0强得多 ——精确值能抓到数据被截断/替换这类问题。★★★★★.d.ts里要分清「原型方法」与「箭头类字段」。我抄了 20 个方法一股脑断言都在prototype上6 个 FAIL ——而那 6 个setValue/getISOCode/selectCountry/getFlag/onPressFlag/onChangePhoneNumber正是箭头类字段.d.ts里写成name: (…) …⇒实例字段.d.ts里写成name(…): …⇒原型方法⇒更稳的断言写法查实例typeof inst[name] function对两者都成立✓⇒ 这条与 rn-72 的发现合起来成员归属三档规则现在已经全部实测过。★★★★★ 最硬的渲染证据是值出现在TextInput里。setValue(8613800138000)之后dump 里TextInput的标签就是8613800138000✓✓这一条同时证明了方法存在、google-libphonenumber在鸿蒙上能跑、内部状态真的流到了渲染层。⇒比方法没抛错强得多。★★★★placeholder不显示是因为有值不是缺陷。差一点把找不到 placeholder记成问题 —— 实际上TextInput里已经是我设进去的号码 ✓★★★ peer 是原生滚轮时桩只要两个组件就够。PickerPicker.Item30 行并且把选中项退化成一行文字⇒ 判据也能看到选了哪个国家 ✓★★★ 本库连Platform.OS都是零命中—— 这在三方库适配里很少见说明它把平台差异完全交给react-native的TextInput/Image了 ⇒跨基线风险最低✓★★getDialCode()带、CountryList[i].dialCode不带—— 拼 E.164 号码时容易踩。本篇用到的库项内容三方库react-native-phone-input上游1.4.1的鸿蒙适配版适配仓库https://atomgit.com/oh-react-native/react-native-phone-input适配 TAG1.4.1-ohos-1.0.0需要 HAR / 权限 / ohpm都不需要纯 JS库类型手机号输入国家选择 号码解析/校验/格式化运行时依赖⚠️google-libphonenumber必须显式装lodash宿主已有peerDependencies⚠️react-native-picker/picker 2.0含原生滚轮⇒ 需装或写桩导出形态默认导出PhoneInputclass 具名CountryPickerclass/CountryList243 项数组/PhoneNumberobject⚠️scripts.preparenpm run buildtsc⇒安装需--ignore-scripts许可证MIT上游仓库https://github.com/rili-live/react-native-phone-input宿主工程RNOH084Demo测试页rnAppKey PhoneInputTestAppnpminstallgithttps://atomgit.com/oh-react-native/react-native-phone-input.git#1.4.1-ohos-1.0.0--ignore-scriptsnpminstall--ignore-scripts google-libphonenumber^3.2.32# peerreact-native-picker/picker 含原生 ⇒ 装官方鸿蒙版或用纯 JS 桩import PhoneInput, {CountryList, CountryPicker, PhoneNumber} from react-native-phone-input; const ref useRef(null); PhoneInput ref{ref} initialCountryus onChangePhoneNumber{(num) console.log(num)} placeholder请输入手机号 / // 实例方法里【在 prototype 上】的可以直接取 ref.current.getCountryCode(); // 当前国家码如 us ref.current.getDialCode(); // 1★ 带 ref.current.getAllCountries(); // 243 项 ref.current.getPickerData(); ref.current.getValue(); ref.current.isValidNumber(); ref.current.getAccessibilityLabel(); ref.current.format(8613800138000); // 实例方法里【是箭头类字段】的在实例上不在 prototype 上 ref.current.setValue(8613800138000); ref.current.getISOCode(); ref.current.selectCountry(cn); ref.current.getFlag(cn); ref.current.onPressFlag(); ref.current.onChangePhoneNumber(138...); // ⚠️ CountryList 有 243 项每项 {name, iso2, dialCode, priority, areaCodes} // ⚠️ CountryList[i].dialCode 不带 而 getDialCode() 带 // ⚠️ setValue() 会把号码真的写进 TextInput此时 placeholder 不再显示 // ⚠️ 安装必须 --ignore-scriptsprepare npm run build // ⚠️ peer react-native-picker/picker 含原生滚轮 ⇒ 需装鸿蒙版或用桩Picker Picker.Item# 换页启动测试页 —— force-stop 不能省hdc shell aa force-stop com.rnoh084.demo hdc shell aa start-bcom.rnoh084.demo-aEntryAbility--psrnAppKey PhoneInputTestApp判据的复现方式# ① 契约含 243 与首项hdc shellhilog -x | grep phi-test|Select-StringPASS|FAIL|NOTE# ⇒ CountryList 恰好 243 个国家[0].iso2af / dialCode93# ⇒ prototype 上缺失 6 个它们是箭头类字段# ② ★ 最硬证据输入框里就是我设的号码devecocli ui layout|Select-StringTextInput## ⇒ TextInput#… [183,1059,1257,1179] 8613800138000验证环境项版本React Native0.84.1React19.2.3RNOHnpm / ohpmreact-native-oh/react-native-harmony/rnoh/react-native-openharmony0.84.3Node.js / npmv24.14.0/11.9.0DevEco Studio26.0.0.621HarmonyOS SDKAPI 2626.0.0.32设备HarmonyOS 7.0.0(26.0.0) Beta2 模拟器Pura X Viewohos-x641320×2232density 3本机状态栏高度117px 39vp宿主依赖⚠️ 补装google-libphonenumber 1 个纯 JS 桩picker宿主 HAP 产物entry-default-signed.hap本次构建assembleHap3 分 56 秒⇒ 一次成功打包与构建退出码都检查了验证规模契约 15 PASS / 1 FAILFAIL 是6 个箭头类字段不在 prototype 上这本身是有价值的发现CountryList精确 243 项setValue后TextInput值为8613800138000最硬证据LogBox无通知4 张截图互异⚠️基线说明交付包spec.json声明RN 0.88.0 RNOH 0.84.4两字段自相矛盾本次验证在RN 0.84.1 RNOH 0.84.3上完成。本库Platform.OS零命中、无原生代码⇒结论跨基线成立✓相关社区与项目地址项目地址React Native 鸿蒙官方社区CPF-RNhttps://atomgit.com/CPF-RNRNOH 框架仓库https://atomgit.com/openharmony-RN/ohos_react_nativeRNOH 三方库使用文档https://atomgit.com/OpenHarmony-RN/usage-docsReact Native for OpenHarmony 组织https://atomgit.com/oh-react-nativeRN 三方库鸿蒙适配清单https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview欢迎加入 CPF-RN 鸿蒙社区https://atomgit.com/CPF-RNReact Native for OpenHarmony 组织https://atomgit.com/oh-react-nativeRN 三方库鸿蒙适配清单https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview