本文记录把 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;

两个设计要点:

  1. 退化成"把选中项显示成一行文字" —— 这样"选了哪个国家"在布局 dump 里也能看到;
  2. 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 ✓✓
⇒ 这说明三件事同时成立:

  1. setValue() 方法存在且被正确调用(它是箭头类字段,在实例上 ✓)
  2. google-libphonenumber 在鸿蒙上正常工作(setValue 内部要做解析/格式化)
  3. 库的内部状态真的流到了渲染层(不是"调用成功但界面没变")

⇒ ⇒ 这种"值出现在 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 本次验证的边界

  1. ★★★ 未验证"点旗帜弹出国家选择器"(需要真实触摸 + picker 桩的实际交互);
  2. ★★★ 未验证真实键盘输入(onChangePhoneNumber 未被用户输入触发,只验证了 setValue 编程路径);
  3. ★★ 未验证 240+ 张旗帜 PNG 在鸿蒙上的加载(只确认了文件存在,未逐张渲染);
  4. ★★ 未验证 isValidNumber() / getNumberType() 的返回值(B 段日志本轮未完整抓到);
  5. ★★ 未验证 format() 的各种格式参数;
  6. 未验证 initialCountry 切换(只用 us);
  7. 未验证 setCustomCountriesData() 替换数据源;
  8. 未在 RN 0.88.0 / RNOH 0.84.4 上复验;
  9. 未测其它屏幕宽度;未做跨平台对照。

库代码未改动(新增的是 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(含原生滚轮,用桩隔离)。

最值得记下来的是这几条:

  1. ★★★★★ 「精确数字判据」的正确做法:先在设备外拿到真值,再在设备上断言同一个值。
    我在 PC 端把 countries.json 解析出来:数组长度 243、[0] = {iso2:'af', dialCode:'93'};
    然后在设备上断言 CountryList.length === 243、CountryList[0].iso2 === 'af' ⇒ 一次全中 ✓✓
    ⇒ 这比"断言 length > 0"强得多 —— 精确值能抓到"数据被截断/替换"这类问题。

  2. ★★★★★ .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 的发现合起来,成员归属三档规则现在已经全部实测过。
  3. ★★★★★ 最硬的渲染证据是"值出现在 TextInput 里"。
    setValue('+8613800138000') 之后,dump 里 TextInput 的标签就是 "+8613800138000" ✓✓
    这一条同时证明了:方法存在、google-libphonenumber 在鸿蒙上能跑、内部状态真的流到了渲染层。
    ⇒ 比"方法没抛错"强得多。

  4. ★★★★ placeholder 不显示是"因为有值",不是缺陷。
    差一点把"找不到 placeholder"记成问题 —— 实际上 TextInput 里已经是我设进去的号码 ✓

  5. ★★★ peer 是"原生滚轮"时,桩只要两个组件就够。
    Picker + Picker.Item(30 行),并且把选中项退化成一行文字 ⇒ 判据也能看到"选了哪个国家" ✓

  6. ★★★ 本库连 Platform.OS 都是零命中 —— 这在三方库适配里很少见,
    说明它把平台差异完全交给 react-native 的 TextInput/Image 了 ⇒ 跨基线风险最低 ✓

  7. ★★ 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(含原生滚轮)⇒ 需装或写桩
导出形态默认导出 PhoneInput(class) + 具名 CountryPicker(class)/ CountryList(243 项数组)/ PhoneNumber(object)
⚠️ scripts.preparenpm 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 Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3
Node.js / npmv24.14.0 / 11.9.0
DevEco Studio26.0.0.621
HarmonyOS SDKAPI 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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐