React Native for OpenHarmony 实战:三方库 react-native-phone-input 的鸿蒙化适配指南
本文记录把 react-native-phone-input(带国家选择的手机号输入框)适配到 HarmonyOS 的完整过程。
它做的事:一个输入框 + 国家旗帜 + 下拉选国家,内部用 google-libphonenumber 做号码解析/校验/格式化。
结论:可用,而且是这几轮里"代码最干净、判据最扎实"的一次。
预检的九项检查(老写法 + ListView + ViewPropTypes + Text.propTypes + measure + useNativeDriver + NativeModules + Platform.OS + isRequired)全部为空;
只缺一个 peer(@react-native-picker/picker,含原生滚轮)⇒ 我用纯 JS 桩隔离;
CountryList 实测恰好 243 个国家、CountryList[0] 的 iso2='af' / dialCode='93' 全部命中;
最硬的一条是 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.js 31262 B / CountryPicker.js 14345 B / PhoneNumber.js 12790 B / countries.json 39 KB / resources/flags/*.png + 全套 .d.ts |
⇒ ⇒ 这是至今"预检最干净"的库:没有任何已移除 API,也没有平台分支。
★★★★ 二、唯一的 peer:用 30 行纯 JS 桩隔离原生滚轮
@react-native-picker/picker 的用法只有两处(dist/CountryPicker.js L34 / L99):
const PickerItem = require('@react-native-picker/picker').Picker.Item; // L34
<Picker style={…} selectedValue={…} onValueChange={…}>{items}</Picker> // L99
⇒ 桩只需提供 Picker(组件) 与 Picker.Item(组件):
const PickerItem = ({label, value}) => <Text style={{display: 'none'}}>{label}</Text>;
const Picker = React.forwardRef(({children, selectedValue, style, ...rest}, ref) => {
// 把 children 里与 selectedValue 匹配的那一项的 label 找出来
const selectedLabel = /* 遍历 children 找 props.value === selectedValue */;
return <View ref={ref} style={[{minHeight: 24, justifyContent: 'center'}, style]} {...rest}>
<Text>{selectedLabel == null ? 'PICKER 桩(未选中)' : 'PICKER 桩:' + selectedLabel}</Text>
</View>;
});
Picker.Item = PickerItem;
两个设计要点:
- 退化成"把选中项显示成一行文字" —— 这样"选了哪个国家"在布局 dump 里也能看到;
Picker上额外挂了focus/blur两个空方法 —— 避免调用方一调用就崩。
⇒ 通过 metro.config.js 的 extraNodeModules 顶掉,库代码零改动 ✓
★★★★★ 三、契约:15 条 PASS,含两个"精确数字"
NOTE | 默认导出 PhoneInput 的 typeof = function
PASS | ★★ 默认导出 PhoneInput 是 class(function)
PASS | ★ PhoneInput 有 prototype.render
NOTE | CountryPicker 的 typeof = function
PASS | ★★ CountryPicker 是 class(function)
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.json ConvertFrom-Json 数出来的(数组长度 = 243)✓
⇒ ⇒ CountryList[0] 的 iso2='af' / dialCode='93' 也是从那一项读出来的 ✓
⇒ 这就是"精确数字判据"的正确做法:先在设备外拿到真值,再在设备上断言同一个值 ✓
★★★★★ 四、一个方法论上的收获:.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 / format |
constructor 里 bind | ✅ 有 | ✅ 有(own key) | rn-68 实测 |
箭头类字段 x = () => {} | ❌ 没有 | ✅ 有(own key) | 本轮 6 个 + rn-72 的 lightbox |
⇒ 教训:从 .d.ts 抄清单时,看 name: (…) => … 还是 name(…): … —— 前者是实例字段、后者是原型方法。
⇒ 更稳的做法:清单照抄,断言时同时查 prototype[name] 与实例 inst[name](inst[name] !== undefined 对两者都成立)。
★★★★★ 五、最硬的证据:setValue 真的把号码写进了输入框
我在时间轴里调用 inst.setValue('+8613800138000'),然后读布局 dump 里的 TextInput:
TextInput#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 | 只有 react + react-native(真实依赖 google-libphonenumber / lodash 在 dist 里按需 require) |
| 资源 | dist/resources/countries.json(39 KB)+ dist/resources/flags/images/*.png(240+ 张旗帜图) |
PhoneNumber 导出 | 是 object(google-libphonenumber 的封装) |
static setCustomCountriesData | ✅ 存在(可替换国家数据源) |




八、接入与预检(要点)
# ★ 必须 --ignore-scripts:它的 scripts.prepare 是 "npm run build"(tsc)
npm install "git+https://atomgit.com/oh-react-native/react-native-phone-input.git#1.4.1-ohos-1.0.0" --ignore-scripts
# ★ 显式装它的运行时依赖(file: 接入不会自动装)
npm install --ignore-scripts google-libphonenumber@^3.2.32
# ★ peer:@react-native-picker/picker 含原生 ⇒ 用纯 JS 桩隔离(见正文第二节)
九、交付包质量与基线说明
| 类别 | 结果 |
|---|---|
| 交付包内容 | dist/ 完整:4 个组件的 .js + 全套 .d.ts + countries.json(39 KB)+ resources/flags/(240+ 张 png)✓(270 文件) |
spec.json | ⚠️ 缺 upstreamCommit |
validation | ⚠️ "build-pass-device-pending" |
| ⚠️ 声明的基线 | reactNative: "0.88.0" + rnoh: "0.84.4" ⇒ 两字段自相矛盾 |
| 备注 | 库代码零改动 |
关于基线:本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。
本库没有任何平台分支(Platform.OS 零命中)⇒ 结论跨基线成立 ✓
十、已知限制与本次验证边界
10.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
★★★★★ @react-native-picker/picker 是 peerDep | 含原生滚轮 ⇒ 要装官方鸿蒙化版本,或用纯 JS 桩隔离(桩需提供 Picker + Picker.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/*.png(240+ 张),打包时需一并处理 |
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 的值真的变成了 +8613800138000;LogBox 无通知。
Q2:Cannot find module '@react-native-picker/picker'?
它是 peerDependency 且含原生滚轮。两个选择:
① 装官方鸿蒙化版本;② 用纯 JS 桩隔离(只需 Picker 与 Picker.Item 两个组件,正文第二节有完整代码)。
Q3:setValue / getISOCode 在 prototype 上找不到?
它们是箭头类字段(setValue = (number) => {…}),挂在实例上、不在 prototype 上。
判断方法:看 .d.ts 里是 name: (…) => …(字段)还是 name(…): …(原型方法)。
断言时直接查实例:typeof inst.setValue === 'function'。
Q4:placeholder 不显示?
因为输入框里已经有值了。本轮的 setValue('+8613800138000') 让 TextInput 的值变成该号码,
placeholder 自然不显示 —— 这是正确行为,不是缺陷。
Q5:getDialCode() 和 CountryList[i].dialCode 为什么不一致?
CountryList[i].dialCode 是裸数字(如 "93"),getDialCode() 返回带 +(如 "+93")。
拼接 E.164 号码时要注意这一点。
Q6:国家列表能换吗?
能,用静态方法:
import PhoneInput from 'react-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 是"原生滚轮"时,桩只要两个组件就够。
Picker+Picker.Item(30 行),并且把选中项退化成一行文字 ⇒ 判据也能看到"选了哪个国家" ✓ -
★★★ 本库连
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 |
| 适配 TAG | 1.4.1-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS) |
| 库类型 | 手机号输入(国家选择 + 号码解析/校验/格式化) |
| 运行时依赖 | ⚠️ google-libphonenumber(必须显式装)+ lodash(宿主已有) |
| peerDependencies | ⚠️ @react-native-picker/picker >= 2.0(含原生滚轮)⇒ 需装或写桩 |
| 导出形态 | 默认导出 PhoneInput(class) + 具名 CountryPicker(class)/ CountryList(243 项数组)/ PhoneNumber(object) |
⚠️ scripts.prepare | npm run build(tsc)⇒ 安装需 --ignore-scripts |
| 许可证 | MIT |
| 上游仓库 | https://github.com/rili-live/react-native-phone-input |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = PhoneInputTestApp) |
npm install "git+https://atomgit.com/oh-react-native/react-native-phone-input.git#1.4.1-ohos-1.0.0" --ignore-scripts
npm install --ignore-scripts google-libphonenumber@^3.2.32
# peer:@react-native-picker/picker 含原生 ⇒ 装官方鸿蒙版,或用纯 JS 桩
import PhoneInput, {CountryList, CountryPicker, PhoneNumber} from 'react-native-phone-input';
const ref = useRef(null);
<PhoneInput
ref={ref}
initialCountry="us"
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-scripts(prepare = 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 -b com.rnoh084.demo -a EntryAbility --ps rnAppKey PhoneInputTestApp
判据的复现方式
# ① 契约(含 243 与首项)
hdc shell "hilog -x | grep phi-test" | Select-String 'PASS|FAIL|NOTE'
# ⇒ CountryList 恰好 243 个国家;[0].iso2=af / dialCode=93
# ⇒ prototype 上缺失 6 个(它们是箭头类字段)
# ② ★ 最硬证据:输入框里就是我设的号码
devecocli ui layout | Select-String 'TextInput#'
# ⇒ TextInput#… [183,1059,1257,1179] "+8613800138000"
验证环境
| 项 | 版本 |
|---|---|
| React Native | 0.84.1 |
| React | 19.2.3 |
| RNOH(npm / ohpm) | @react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3 |
| Node.js / npm | v24.14.0 / 11.9.0 |
| DevEco Studio | 26.0.0.621 |
| HarmonyOS SDK | API 26(26.0.0.32) |
| 设备 | HarmonyOS 7.0.0(26.0.0) Beta2 模拟器 Pura X View(ohos-x64),1320×2232,density 3 |
| 本机状态栏高度 | 117px = 39vp |
| 宿主依赖 | ⚠️ 补装 google-libphonenumber + 1 个纯 JS 桩(picker) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 3 分 56 秒 ⇒ 一次成功(打包与构建退出码都检查了) |
| 验证规模 | 契约 15 PASS / 1 FAIL(FAIL 是"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-RN) | https://atomgit.com/CPF-RN |
| RNOH 框架仓库 | https://atomgit.com/openharmony-RN/ohos_react_native |
| RNOH 三方库使用文档 | https://atomgit.com/OpenHarmony-RN/usage-docs |
| React Native for OpenHarmony 组织 | https://atomgit.com/oh-react-native |
| RN 三方库鸿蒙适配清单 | https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview |
欢迎加入 CPF-RN 鸿蒙社区:https://atomgit.com/CPF-RN
React Native for OpenHarmony 组织:https://atomgit.com/oh-react-native
RN 三方库鸿蒙适配清单:https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview
更多推荐



所有评论(0)