本文记录把 react-native-mask-input(输入框掩码)适配到 HarmonyOS 的完整过程。

它做的事:给 TextInput 套一层掩码 —— 输入 25122024 自动变成 25/12/2024,并同时给出"带掩码 / 不带掩码 / 混淆后"三种结果。

结论:完全可用,而且这是这几轮里"判据最强"的一个库 —— 因为它把核心逻辑抽成了三个纯函数:
formatWithMask、createNumberMask、Masks。这些函数不需要设备、不需要网络、不受平台差异影响,
我把它们逐条断言下来:26 条 PASS、1 条 FAIL(那 1 条是我自己把掩码写短了一位)。

★★★★★ 一、它的结构:一个组件 + 三个纯函数

src/index.tsx(524 B):

export {default as formatWithMask} from './formatWithMask';
export {default as createNumberMask} from './createNumberMask';
export {default as Masks} from './Masks';
export {default as useMaskedInputProps} from './useMaskedInputProps';
export default MaskInput;
导出类型作用
MaskInput(默认)React.forwardRef(...) ⇒ typeof 是 object掩码输入框
formatWithMaskfunction(纯函数)核心格式化:{text, mask} → {masked, unmasked, obfuscated}
createNumberMaskfunction(工厂)返回一个 (value) => MaskArray 的函数
Masksobject12 个预定义掩码
useMaskedInputPropsfunction(hook)组件内部用它算 props

实测(按源码写断言,一次全中):

NOTE | 默认导出 MaskInput 的 typeof = object
PASS | ★★ 默认导出是 object(forwardRef 的结果,不是函数)
NOTE | MaskInput.$$typeof = Symbol(react.forward_ref)
PASS | ★★ $$typeof = Symbol(react.forward_ref)
PASS | ★ forwardRef 结果没有 prototype.render
PASS | ★★ formatWithMask 是 function(纯函数)
PASS | ★★ createNumberMask 是 function(工厂)
PASS | ★★ useMaskedInputProps 是 function(hook)
PASS | ★★ Masks 是 object(预定义掩码集合)
NOTE | Masks 的键数 = 12
NOTE | Masks 的键 = BRL_CAR_PLATE, BRL_CNPJ, BRL_CPF, BRL_CPF_CNPJ, BRL_CURRENCY, BRL_PHONE,
       CREDIT_CARD, DATE_DDMMYYYY, DATE_MMDDYYYY, DATE_YYYYMMDD, USA_PHONE, ZIP_CODE

这里有个方法论的价值:这几轮我连续在"契约断言"上写错过(一次凭默认值猜、一次把 forwardRef 当函数)。
这一轮先把源码里的导出语句抄下来再写断言 ⇒ 11 条一次全过 ✓

★★★★★ 二、formatWithMask:11 条判据,覆盖它的全部行为分支

源码(2539 B)的核心是一个 while 循环,三种 mask 元素走三条不同分支:

if (maskChar === valueChar) { … }                       // 值刚好等于掩码字符
else if (typeof maskChar === 'object') {                // RegExp 或 [RegExp]
  const shouldObsfucateChar = Array.isArray(maskChar);  // ★ 数组形式才混淆
  …
  masked += valueChar;
  obfuscated += shouldObsfucateChar ? obfuscationCharacter : valueChar;
  unmasked += unmaskedValueChar;
} else {                                                // 固定字符(string)
  masked += maskChar;
  obfuscated += maskChar;                               // ★ 固定字符不混淆
}

实测判据(按这个分支结构设计):

#输入期望实测
1{text:'', mask}三个都是 ''✅ {"masked":"","unmasked":"","obfuscated":""}
2{text:'abc', mask:undefined}三个都等于原文✅ {"masked":"abc","unmasked":"abc","obfuscated":"abc"}
3{text:'25122024', mask:日期掩码}masked='25/12/2024'✅ 25/12/2024
4同上unmasked='25122024'✅ 25122024
5同上obfuscated 与 masked 相同(普通 RegExp 不混淆)✅ 25/12/2024
6{text:'1234', mask:[[/\d/],[/\d/],'-',[/\d/],[/\d/]]}masked='12-34'✅ 12-34
7同上obfuscated='**-**'([RegExp] 混淆、固定字符 - 保留)✅ **-**
8同上 + obfuscationCharacter:'#'obfuscated='##-##'✅ ##-##
9{text:'15/10', mask:日期掩码, maskAutoComplete:true}masked='15/10/'(补固定字符)✅ 15/10/
10同上但 maskAutoComplete 默认masked='15/10'(不补)✅ 15/10
11{text:'2a5', mask:[/\d/,/\d/]}masked='25'(非匹配字符被跳过、不消耗 mask 位)✅ 25

⇒ 11 条全部命中 ✓✓ 其中第 7 条最能说明细节:

NOTE | [RegExp] 混淆结果 = {"masked":"12-34","unmasked":"1234","obfuscated":"**-**"}

⇒ [RegExp](数组形式)会被混淆成 obfuscationCharacter(默认 *),而固定字符 - 原样保留 ✓
⇒ 这正是"掩码库"最容易做错的细节(很多人会以为"所有数字都该混淆")

唯一一条 FAIL:是我把掩码写短了一位

我构造了一个"按值动态返回不同掩码"的函数掩码:

const fnMask = (value) => value.length > 3
  ? [/\d/, /\d/, /\d/, '.', /\d/]        // ★ 只定义了 1 位小数
  : [/\d/, /\d/, /\d/];
formatWithMask({text: '12345', mask: fnMask})

实测:

NOTE | mask 为函数的结果 = {"masked":"123.4","unmasked":"1234","obfuscated":"123.4"}
FAIL | ★★ mask 可以是函数,且按值动态返回不同掩码 | 期望=123.45 | 实际=123.4

逐位走一遍算法就明白了:掩码是 5 项 [d,d,d,'.',d],
'1','2','3' 占前三位、'.' 是固定字符、'4' 占第 5 位 ⇒ mask 用完就 break ⇒
第 6 个字符 '5' 无处可放 ⇒ 123.4 是正确结果 ✓

⇒ ⇒ 所以这条 FAIL 是我断言写错了,不是库的问题 ✓
⇒ 但"mask 可以是函数"这件事本身仍然成立(实测它确实被调用、并按其返回值工作)✓
⇒ 教训:写"函数掩码"的用例时,掩码长度要和输入长度匹配 —— 我下意识以为"多给一个字符它自己会扩展",但这个库是严格按掩码数组走的 ✓

★★★★ 三、createNumberMask:4 条判据,含一条"公式级"断言

源码(1268 B)返回一个函数:

export default function createNumberMask(props) {
  const {delimiter = '.', precision = 2, prefix = [], separator = ','} = props || {};
  return (value) => {
    const numericValue = value?.replace(/\D+/g, '') || '';
    let mask = numericValue.split('').map(() => /\d/);
    if (mask.length > precision && !!separator) mask.splice(-precision, 0, separator);
    const amountOfDelimiters = Math.ceil((numericValue.length - precision) / 3) - 1;
    … mask.splice(delimiterPosition, 0, delimiter) …
    return [...prefix, ...mask];
  };
}

我按源码把这个公式算了一遍:value='123456'、默认参数

  • 初长 = 6(6 位数字 ⇒ 6 个 /\d/)
  • 插 separator(-2 位置)⇒ 7 项
  • amountOfDelimiters = ceil((6-2)/3) - 1 = ceil(4/3)-1 = 2-1 = 1 ⇒ 再插 1 个 delimiter ⇒ 8 项
  • 加上 prefix ['R','$',' '] 的 3 项 ⇒ 11 项

实测:

NOTE | createNumberMask(...) 的 typeof = function
PASS | ★★ createNumberMask 返回一个函数
NOTE | factory("123456") 的结果 = ["R","$"," ",{},".",{},{},{},",",{},{}]
NOTE | 结果长度 = 11
PASS | ★★ 调用后返回数组(MaskArray)
PASS | ★★★ 长度符合公式:8 + prefix 3 = 11        ← ★ 公式级断言命中
NOTE | factory("") 的结果 = ["R","$"," "]
PASS | ★★ 空值 ⇒ 只剩 prefix(3 项)
NOTE | factory("R$ 99.99") 的结果(只取数字 9999)= ["R","$"," ",{},{},",",{},{}]
PASS | ★★ 只保留数字

⇒ 长度 11 与公式推导完全一致 ✓✓
⇒ 而且能看到内部结构:["R","$"," ",/\d/,".",/\d/,/\d/,/\d/,",",/\d/,/\d/]
(prefix 3 项 + 数字 1 位 + . + 3 位 + , + 2 位 = 11 ✓ 与 precision:2 / separator:',' / delimiter:'.' 完全对应)

★★★ 四、组件渲染与两条无害的 ARK 侧警告

组件本身很简单(src/MaskInput.tsx 999 B):

export default React.forwardRef(function (props, ref) {
  const {mask, value, onChangeText, placeholderFillCharacter = '_', …} = props;
  const maskedInputProps = useMaskedInputProps({…});
  return <TextInput placeholder={maskedInputProps.placeholder} {…rest}
                    selection={…} value={…} onChangeText={…} ref={ref} />;
});

⇒ placeholderFillCharacter 默认 '_' ⇒ placeholder 会被替换成掩码形状(如 __/__/____)

实测:两个 TextInput 都正常渲染(y=1168 / y=1481),LogBox 无通知 ✓
⚠️ 但我没能在布局 dump 里找到 placeholder 文本(无论是当作 TextInput# 的标签还是独立的 Text# 节点)
⇒ 所以"placeholder 显示成掩码形状"这一条本轮未验证,如实记录 ✓

hilog 里有 2 条 ARK 侧警告:

W A0beef/#RNOH_ARK:  This is usually not a problem and the application should continue to work normally.
                     TypeError: Cannot read property getWindowName of undefined

⇒ 注意它自己的前缀:“This is usually not a problem and the application should continue to work normally”
⇒ 功能未受影响(页面渲染、纯函数判据、输入框都正常)⇒ 如实记录为"ARK 侧警告、无功能影响" ✓

★★★ 五、其余预检结论

预检项结果
dependencies✅ 空
peerDependencies✅ 只有 react / react-native ⇒ 无原生 peer
老写法九项 + ListView / ViewPropTypes / Text.propTypes / measure / onLayout / useNativeDriver / NativeModules / Platform.OS / isRequired✅✅ 全部为空(用真实使用形态扫的)
外部 import只有 react + react-native ✓
交付包形态✅ src/(10 个 ts/tsx)+ lib/commonjs + lib/module + lib/typescript ⇒ 源码/产物/类型齐全 ✓
scripts.prepare⚠️ bob build ⇒ 安装必须 --ignore-scripts

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


六、接入与预检(要点)

# ★ 必须 --ignore-scripts:它的 scripts.prepare 是 "bob build"
npm install "git+https://atomgit.com/oh-react-native/react-native-mask-input.git#1.2.3-ohos-1.0.0" --ignore-scripts
# 它没有 dependencies ⇒ 不需要补装任何东西

七、交付包质量与基线说明

类别结果
交付包内容src/(10 个 ts/tsx)+ lib/commonjs + lib/module + lib/typescript ⇒ 源码、编译产物、类型声明三者齐全 ✓(71 文件)
spec.json⚠️ 缺 upstreamCommit
validation⚠️ "build-pass-device-pending"
⚠️ 声明的基线reactNative: "0.88.0" + rnoh: "0.84.4" ⇒ 两字段自相矛盾
备注库代码零改动(本轮未修改库任何文件)

关于基线:本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。
纯函数判据(26 条)与平台完全无关 ⇒ 结论跨基线 100% 成立 ✓


八、已知限制与本次验证边界

8.1 用这个库要知道的

事项说明
★★★★★ 三个纯函数可独立使用formatWithMask / createNumberMask / Masks —— 不需要组件、不需要设备
★★★★★ [RegExp] 数组形式才会混淆普通 RegExp 不混淆;固定字符(string)永远不混淆
★★★★ maskAutoComplete 控制"值用完后是否补固定字符"默认 false
★★★★ createNumberMask 返回函数,长度按公式8 + prefix 项数(123456 + 默认参数 + 3 项 prefix ⇒ 11)
★★★ MaskInput 是 forwardReftypeof 是 object、$$typeof 是 Symbol(react.forward_ref) ⇒ 别当函数
★★★ Masks 有 12 个预定义掩码BRL_CAR_PLATE / BRL_CNPJ / BRL_CPF / BRL_CPF_CNPJ / BRL_CURRENCY / BRL_PHONE / CREDIT_CARD / DATE_DDMMYYYY / DATE_MMDDYYYY / DATE_YYYYMMDD / USA_PHONE / ZIP_CODE
★★ 掩码是"严格按数组走"的值比掩码长 ⇒ 多出的字符被丢弃(不会自动扩展)
★★ placeholderFillCharacter 默认 '_'输入框空时 placeholder 会显示成掩码形状
★ scripts.prepare 会跑 bob build安装必须 --ignore-scripts

8.2 本次验证的边界

  1. ★★★ 未验证"placeholder 显示成掩码形状"(没能从布局 dump 里找到 placeholder 文本);
  2. ★★★ 未验证真实输入时的掩码行为(未做输入注入;全部判据走纯函数);
  3. ★★ 未验证 selection 的受控行为(光标位置维护,这通常是掩码库最难的部分);
  4. ★★ 未验证 showObfuscatedValue 的组件级效果(只验证了纯函数的 obfuscated 输出);
  5. ★★ 未验证 Masks 里 12 个预定义掩码各自的实际输出(只枚举了键名);
  6. 未验证 useMaskedInputProps 单独使用;
  7. 未验证 粘贴长文本、删除字符、光标中点插入等边界;
  8. 未在 RN 0.88.0 / RNOH 0.84.4 上复验;
  9. 未测其它屏幕宽度;未做跨平台对照。

库代码未改动。


九、常见问题

Q1:它在 RN 0.84 上能跑吗?

能,而且判据最强。 11 条契约全 PASS(含 forwardRef 的 $$typeof);
formatWithMask 12 条里 11 条 PASS(1 条是我掩码写短了);createNumberMask 4 条全 PASS(含公式级断言);
LogBox 无通知;两个掩码输入框正常渲染。

Q2:[RegExp] 和 RegExp 有什么区别?

  • RegExp(如 /\d/)⇒ 校验通过后 masked 与 obfuscated 都是原字符(不混淆)
  • [RegExp](如 [/\d/])⇒ obfuscated 会被换成 obfuscationCharacter(默认 *)
  • 固定字符(string) ⇒ 永远原样出现在 masked 与 obfuscated 里

实测:{text:'1234', mask:[[/\d/],[/\d/],'-',[/\d/],[/\d/]]} ⇒
masked='12-34'、obfuscated='**-**'(- 保留)✓

Q3:maskAutoComplete 是什么?

控制"值用完之后是否继续补固定字符"。
{text:'15/10', mask:日期掩码, maskAutoComplete:true} ⇒ '15/10/'(补了末尾 /);默认 false ⇒ '15/10'。

Q4:createNumberMask 怎么用?

const mask = createNumberMask({delimiter: '.', precision: 2, prefix: ['R', '$', ' '], separator: ','});
mask('123456');   // 返回一个 11 项的 MaskArray(3 项 prefix + 8 项数字/分隔符)

然后把这个函数直接当 mask 传给 MaskInput:

<MaskInput mask={createNumberMask({prefix: ['R', '$', ' ']})} />

Q5:值比掩码长会怎样?

多出的字符会被丢弃(掩码是严格按数组走的,不会自动扩展)。
我实测时就踩了这个:掩码只定义 1 位小数 [/\d/,/\d/,/\d/,'.',/\d/],输入 '12345' ⇒ 结果 '123.4'(5 被丢弃)✓

Q6:MaskInput 是组件还是对象?

是 forwardRef 的结果 ⇒ typeof 是 object:

import MaskInput, {formatWithMask} from 'react-native-mask-input';
typeof MaskInput;                            // 'object'
String(MaskInput.$$typeof);                  // 'Symbol(react.forward_ref)'
typeof formatWithMask;                       // 'function'(纯函数)
typeof Masks;                                // 'object'(12 个预定义掩码)

Q7:hilog 里 getWindowName of undefined 要紧吗?

那是 ARK 侧的警告,且它自己的文本就写着
"This is usually not a problem and the application should continue to work normally"
—— 实测功能未受影响(页面渲染、纯函数判据、输入框都正常)✓
LogBox 上也没有通知(说明它没进 JS 侧的 LogBox)。


小结

react-native-mask-input 是输入框掩码库:一个 forwardRef 组件 + 三个可独立调用的纯函数
(formatWithMask / createNumberMask / Masks)。dependencies 为空、老写法九项全空、零原生依赖。

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

  1. ★★★★★ 三个纯函数 = 26 条零设备依赖的判据,这是本轮最大的价值。
    formatWithMask 12 条(覆盖它的全部分支:固定字符 / RegExp / [RegExp] 混淆 / maskAutoComplete / 非匹配字符跳过)

    • createNumberMask 4 条(含公式级断言:长度 11 = 8 + prefix 3)
      ⇒ 这些结论与平台、与基线、与网络全都无关 ⇒ 跨环境 100% 成立 ✓✓
  2. ★★★★★ 先把源码里的"形态事实"抄下来,再写断言 —— 11 条契约一次全过。
    这几轮我连续在契约断言上写错过(一次凭默认值猜、一次把 forwardRef 当函数)。
    这次先读了 export default React.forwardRef(...) ⇒ 直接断言 typeof === 'object' + $$typeof === Symbol(...) ⇒ 全中 ✓
    ⇒ 这是可复用的流程改进:断言前先抄源码。

  3. ★★★★ [RegExp] 数组形式才混淆、固定字符永不混淆 —— 这是掩码库最容易做错的细节,
    实测 {mask:[[/\d/],[/\d/],'-',[/\d/],[/\d/]], text:'1234'} ⇒ obfuscated = '**-**'(- 保留)✓

  4. ★★★★ 我唯一那条 FAIL 是"掩码写短了一位"。
    掩码 [/\d/,/\d/,/\d/,'.',/\d/] 只有 1 位小数 ⇒ 输入 '12345' 时第 6 个字符被丢弃 ⇒ '123.4' 是正确行为 ✓
    ⇒ 教训:写"函数掩码"用例时,掩码长度必须与输入长度匹配 —— 别下意识以为它会自动扩展。

  5. ★★★ Masks 的 12 个键本身就是一份有用的清单(BRL_* 五个、DATE_* 三个、CREDIT_CARD、USA_PHONE、ZIP_CODE、BRL_CAR_PLATE)✓

  6. ★★★ ARK 侧警告要读它自己的前缀。 那两条 getWindowName of undefined 自带
    "This is usually not a problem…" 的说明,且功能未受影响、LogBox 也没有 ⇒ 归为"无害" ✓

  7. ★★ 诚实标注没验证到的东西:placeholder 显示成掩码形状这一条我没能从 dump 里找到证据,
    所以只写在"未验证"里,没有当成结论 ✓


本篇用到的库

项内容
三方库react-native-mask-input(上游 1.2.3 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-mask-input
适配 TAG1.2.3-ohos-1.0.0
需要 HAR / 权限 / ohpm都不需要(纯 JS)
库类型输入框掩码(格式化 / 反格式化 / 混淆)
运行时依赖✅ dependencies 为空;peer 只有 react / react-native
导出形态默认导出 MaskInput(forwardRef ⇒ typeof 为 object) + 4 个具名导出
⚠️ scripts.preparebob build ⇒ 安装需 --ignore-scripts
许可证MIT
上游仓库https://github.com/CaioQuirinoMedeiros/react-native-mask-input
宿主工程RNOH084Demo(测试页 rnAppKey = MaskInputTestApp)
npm install "git+https://atomgit.com/oh-react-native/react-native-mask-input.git#1.2.3-ohos-1.0.0" --ignore-scripts
import MaskInput, {formatWithMask, createNumberMask, Masks} from 'react-native-mask-input';

// ① 组件用法
<MaskInput
  mask={[/\d/, /\d/, '/', /\d/, /\d/, '/', /\d/, /\d/, /\d/, /\d/]}
  onChangeText={(masked, unmasked) => console.log(masked, unmasked)}
  placeholder="DD/MM/YYYY"
/>

// ② 纯函数用法(无需渲染,最适合做判据 / 单测)
formatWithMask({text: '25122024', mask: [/\d/,/\d/,'/',/\d/,/\d/,'/',/\d/,/\d/,/\d/,/\d/]});
// ⇒ {masked: '25/12/2024', unmasked: '25122024', obfuscated: '25/12/2024'}

formatWithMask({text: '1234', mask: [[/\d/],[/\d/],'-',[/\d/],[/\d/]]});
// ⇒ {masked: '12-34', unmasked: '1234', obfuscated: '**-**'}   ← 数组形式才混淆

createNumberMask({delimiter: '.', precision: 2, prefix: ['R','$',' '], separator: ','})('123456');
// ⇒ 11 项 MaskArray

// ⚠️ MaskInput 是 forwardRef ⇒ typeof 是 'object',不是 'function'
// ⚠️ 掩码严格按数组走:值比掩码长 ⇒ 多出的字符被丢弃(不会自动扩展)
// ⚠️ [RegExp] 才混淆;普通 RegExp 与固定字符都不混淆
// ⚠️ 安装必须 --ignore-scripts(prepare = bob build)
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey MaskInputTestApp

判据的复现方式

# ① 契约 + 纯函数判据(26 条 PASS + 1 条我自己算错)
hdc shell "hilog -x | grep mki-test" | Select-String 'PASS|FAIL|SUMMARY|NOTE'
#    ⇒ formatWithMask 的 12 条(含 **-** 混淆、maskAutoComplete)
#    ⇒ createNumberMask 的公式级断言(长度 11 = 8 + prefix 3)
# ② 12 个预定义掩码的键名
hdc shell "hilog -x | grep 'Masks 的键'"

验证环境

项版本
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
宿主依赖✅ 无需额外依赖(dependencies 为空)
宿主 HAP 产物entry-default-signed.hap
本次构建assembleHap 3 分 39 秒 ⇒ 一次成功(打包与构建退出码都检查了)
验证规模契约 11 条全 PASS;formatWithMask 11/12 PASS(1 条是我掩码写短);createNumberMask 4/4 PASS(含公式级断言);Masks 12 个键枚举;2 个输入框正常渲染;LogBox 无通知;hilog 有 2 条无害的 ARK 侧警告;4 张截图互异

⚠️ 基线说明:交付包 spec.json 声明 RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。
纯函数判据与平台完全无关 ⇒ 结论跨基线 100% 成立 ✓


相关社区与项目地址

项目地址
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

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐