React Native for OpenHarmony 实战:三方库 react-native-otp-inputs 的鸿蒙化适配指南
本文记录把 react-native-otp-inputs(OTP / 验证码分格输入框)适配到 HarmonyOS 的完整过程。
它做的事很专一:把一串验证码(4 位、6 位…)拆成几个独立的输入格,输入一位自动跳到下一格、退格回到上一格,并可选地从剪贴板自动填充。
结论:可以正常使用,判据 19/19 全部通过。 但它有一个"藏在入口里"的原生前置,以及一个会在控制台刷警告的库设计问题 —— 这两点都比"能不能跑"更值得写下来。
★★★★ 一、"5 个文件里只有 1 个用剪贴板"是没用的:入口无条件 import
库的入口第一行就是:
// src/index.tsx L1
import Clipboard from '@react-native-clipboard/clipboard';
而它真正的用点只有两处:
L110: Clipboard.setString(''); // 在 ref.reset() 里
L205: const copiedText = await Clipboard.getString(); // 在剪贴板自动填充轮询里
看起来"不做自动填充就不需要剪贴板"——但打包层面不行:这个 import 在入口,Metro 必须解析整个模块图,不装 @react-native-clipboard/clipboard 就 Unable to resolve module。
而它是原生库:上游 @react-native-clipboard/clipboard@1.16.3,鸿蒙适配版是 @react-native-ohos/clipboard@1.17.0(存在 ✓),但接入它要接 HAR —— 而用同样方式接入原生库 HAR 时,曾出现过 BUILD FAILED(Error Code: 00308018,2.5 秒即失败,属于配置/解析期错误而非编译错误)。
★★★★ 我的做法:打一个带调用计数的空桩
// metro.config.js 的 resolver.extraNodeModules(★ 必须写在传给 mergeConfig 的对象里)
'@react-native-clipboard/clipboard': path.resolve(__dirname, 'stubs/react-native-clipboard.js'),
// stubs/react-native-clipboard.js —— 关键:计数 + 打日志
let getStringCalls = 0;
let setStringCalls = 0;
const Clipboard = {
getString: () => { getStringCalls += 1;
console.log(`[clipboard-stub] NOTE | getString 被调用(累计 ${getStringCalls})`); return Promise.resolve(''); },
setString: () => { setStringCalls += 1;
console.log(`[clipboard-stub] NOTE | setString 被调用(累计 ${setStringCalls})`); },
…
__stubStats: () => ({getStringCalls, setStringCalls}),
};
★ 为什么"计数"很重要:它把"桩被不被调用"变成了可断言的证据:
| 时刻 | getString | setString | 说明 |
|---|---|---|---|
| 挂载后(默认路径) | 0 | 0 | 库默认不碰剪贴板 ✓ |
调 ref.reset() 之后 | 0 | 1 | 桩确实接管了原生调用 ✓ |
NOTE | ★ clipboard 桩统计 = {"getStringCalls":0,"setStringCalls":0}
PASS | ★★★ 默认路径下 getString 调用 0 次(autofillFromClipboard=false)
(hilog 里 clipboard-stub 的日志:一条都没有)
点击「调 E1 的 reset()」之后:
NOTE | setString 被调用(累计 1)—— 只有 ref.reset() 会走到这里
⇒ 一个"两阶段"的行为判据:默认 0 → 主动调用后 1 ✓
⇒ 代价(如实记录):ref.reset() 与剪贴板自动填充实际上不工作(桩只返回空串)——见"已知限制"。
★★★★ 二、为什么"默认不碰剪贴板":一处平台判断的精确推算
// src/index.tsx L30-31
const supportAutofillFromClipboard =
Platform.OS === 'android' || parseInt(Platform.Version as string, 10) < 14;
…
L61: autofillFromClipboard = supportAutofillFromClipboard, // 默认值
…
L221: if (autofillFromClipboard) { interval = setInterval(() => listenOnCopiedText(), …); }
在鸿蒙上逐项推算:
Platform.OS === 'android' ⇒ false
Platform.Version ⇒ "OpenHarmony-7.0.0.32(Beta2)"
parseInt("OpenHarmony-7.0.0.32(Beta2)", 10) ⇒ NaN
NaN < 14 ⇒ false
⇒ supportAutofillFromClipboard = false ★ 默认不启用剪贴板轮询
实测(页面上复算 + 桩计数双重证实):
NOTE | Platform.OS = harmony
NOTE | Platform.Version = OpenHarmony-7.0.0.32(Beta2)
NOTE | ★ 复算 supportAutofillFromClipboard = false
PASS | ★★★ 鸿蒙上 supportAutofillFromClipboard 应为 false(⇒ 默认不轮询剪贴板) | 期望=false | 实际=false
PASS | ★★★ 默认路径下 getString 调用 0 次(autofillFromClipboard=false) | 期望=0 | 实际=0
⇒ "NaN < 14 为假"这个链条被完整验证 ✓ —— 这也说明:凡是 parseInt(Platform.Version) 参与的平台判断,在鸿蒙上都会得到 NaN,写这类判断时要留意。
顺带看完整库的 27 处平台判断:Platform.OS 18 处、Platform.select 9 处。
逐处看过之后,只有 2 处会得到 undefined(且都不崩):
| 位置 | 代码 | 鸿蒙上的结果 |
|---|---|---|
src/index.tsx L264 | maxLength={Platform.select({android: 1, ios: …})} | undefined ⇒ 不崩,但失去"整串粘贴进第一格"的能力 |
src/OtpInput.tsx L78 | selectTextOnFocus={Platform.select({ios: …, android: true})} | undefined ⇒ 用 TextInput 默认值 |
src/OtpInput.tsx L57 | Platform.select({default: rest, web: {…}}) | ✅ 带了 default ⇒ 返回 rest(正面写法) |
src/index.tsx L149/L164 | Platform.OS === 'android' && … / === 'ios' && … | 全 false ⇒ 分支不进入 ✓ |
src/OtpInput.tsx L26 | Platform.OS === 'ios' && majorVersionIOS >= 12 | false ⇒ textContentType='none' ✓ |
⇒ 一个"第四平台"(既非 iOS 也非 Android 也非 Web)的典型走法:二元判断天然落到 else,安全;没有 default 的 Platform.select 会得到 undefined,只要接收方容得下 undefined 就不崩 ✓
★★★★★ 三、13 条纯函数判据:全部精确命中
交付包同时给了 src/(TS 源码)和 lib/(编译产物),而 lib/commonjs/ 里的两个纯函数可以直接用子路径 require 单测:
const {fillOtpCode} = require('react-native-otp-inputs/lib/commonjs/helpers');
const reducer = require('react-native-otp-inputs/lib/commonjs/reducer').default;
3.1 fillOtpCode(numberOfInputs, value)
const fillOtpCode = (numberOfInputs, value) => {
const otpCode = {};
for (let i = 0; i < numberOfInputs; i++) {
otpCode[`${i}`] = value?.[i] || ''; // ★ 不足位补空串
}
return otpCode;
};
| 调用 | 预期 | 实测 |
|---|---|---|
fillOtpCode(4) | {"0":"","1":"","2":"","3":""} | ✅ |
fillOtpCode(4, '1234') | {"0":"1","1":"2","2":"3","3":"4"} | ✅ |
fillOtpCode(4, '12') | {"0":"1","1":"2","2":"","3":""} | ✅ ★ 覆盖"不足位补空串" |
fillOtpCode(0, '1234') | {} | ✅ ★ 覆盖 numberOfInputs=0 |
3.2 reducer(state, {type, payload})
它有 5 个 action,且每个 action 内部都会调用一次 state.handleChange(...):
case ACTION_TYPES.setOtpTextForIndex: {
const otpCode = {...state.otpCode, [`${payload.index}`]: payload.text};
state.handleChange(Object.values(otpCode).join('')); // ★ 副作用
return {...state, otpCode};
}
…
default: return state; // ★ 返回同一引用
实测(9 条断言全部通过):
PASS | ★★ setOtpTextForIndex 写入指定下标 | {"0":"","1":"7","2":"","3":""}
PASS | ★★ 且回调参数是 Object.values().join("") | "7"
PASS | ★ 返回新对象(不是同一引用)
PASS | ★★ setOtpCode 走 fillOtpCode | {"0":"9","1":"8","2":"7","3":"6"}
PASS | ★★ setOtpCode 回调 | "9876"
PASS | ★★ clearOtp 清空(fillOtpCode(payload)) | {"0":"","1":"","2":"","3":""}
PASS | ★★ clearOtp 回调为空串 | ""
PASS | ★★ setHandleChange 替换回调(同一引用)
PASS | ★★ setHasKeySupport 置位
PASS | ★★ 未知 action 返回【同一引用】 ← ★ 用 === 断言
⇒ 纯函数是最便宜的硬判据:不需要渲染、不需要设备交互,而且能覆盖边界(numberOfInputs=0、不足位、未知 action)✓
★★★★ 四、accessibilityLabel 让它"天然可数",还顺手验证了 isRTL
组件给每个输入框设了可预测的 accessibilityLabel:
accessibilityLabel={`${testIDPrefix}-${inputIndex}`} // testIDPrefix 默认 "otpInput"
⇒ 布局 dump 里直接读得到,于是"有几个输入框、每个多大、什么顺序"全都可验证 ✓
实测(E1:4 格,每格 inputStyles 60vp):
E1(60vp ⇒ 180px)去重后节点数 = 4 ← 恰好 4 个 ✓
TextInput#otpInput-0 [66,1110,246,1290] ⇒ 180×180 ✓ = 60vp×3 ✓
TextInput#otpInput-1 [402,1110,582,1290]
TextInput#otpInput-2 [738,1110,918,1290]
TextInput#otpInput-3 [1074,1110,1254,1290]
实测(E2:6 格 + isRTL={true},每格 44vp):
E2(44vp ⇒ 132px)去重后节点数 = 6 ← 恰好 6 个 ✓
TextInput#otpInput-5 [66,1459,198,1591] ⇒ 132×132 ✓ = 44vp×3 ✓
TextInput#otpInput-4 [277,…]
TextInput#otpInput-3 [488,…]
TextInput#otpInput-2 [700,…]
TextInput#otpInput-1 [911,…]
TextInput#otpInput-0 [1122,…]
⇒ ★★★ 注意 E2 的标签从左到右是 5,4,3,2,1,0 —— 这正是源码里:
let inputIndex = index;
if (isRTL) { inputIndex = numberOfInputs - 1 - index; } // src/index.tsx L237-239
的效果 ✓✓ isRTL 被直接证实(一个判据同时验证了"格数"、"尺寸"和"RTL 反转"三件事)✓
★★★★ 五、库的一个真实设计问题:reducer 在渲染期调了回调
页面上出现了这样一条 LogBox 通知(不是崩溃,是警告):
"Cannot update a component (`OtpInputsTestApp`) while rendering a different component (`ForwardRef`).
To locate the bad setState() call inside `ForwardRef`, follow the sta…"
根因在源码里一目了然:reducer 的每个 action 分支都调用 state.handleChange(...),
而 useReducer 的 reducer 是在渲染阶段执行的 ⇒ handleChange 在渲染期被调用 ⇒
如果调用方在 handleChange 里做 setState(我页面上就是这么写的),React 就会报"不能在渲染另一个组件时更新组件"。
// src/index.tsx L67:默认值是 console.log
handleChange = console.log,
⇒ ★ 一个实用的规避建议:如果不需要在输入变化时立刻 setState,就别传 handleChange(默认的 console.log 不会有这个问题);
或者把 setState 包进 setTimeout(…, 0) / queueMicrotask 之类延后一拍。
这一条和第三节的 A3 判据是互相印证的:判据证明"每个 action 都会调
handleChange",
而运行时警告证明"这个调用发生在渲染期" —— 源码事实和运行时行为对上了 ✓




六、接入与预检
| 步 | 做法 | 结果 |
|---|---|---|
| ① | harmony.autolinking / harmony/ / ios/ / android/ | 都没有 |
| ② | 原生痕迹预扫 | ✅ 无原生源文件(NativeModules/requireNativeComponent/TurboModule 全空) |
| ③ | ★ 老写法九项预扫 | ✅✅ 全部为空(见下) |
| ④ | measure() / onLayout / useNativeDriver / findNodeHandle | ✅ 全空(没有那类环境坑) |
| ⑤ | 平台分支 | ⚠️ Platform.OS 18 处 + Platform.select 9 处 ⇒ 逐处看过,只有 2 处得 undefined、都不崩(见第二节) |
| ⑥ | 依赖 | ✅ dependencies 为空;⚠️ peer 里有 @react-native-clipboard/clipboard,且入口无条件 import ⇒ 硬前置 |
| ⑦ | scripts 扫 prepare 家族 | ⚠️ 有("prepare": "bob build")⇒ 必须 --ignore-scripts |
老写法九项逐项结果:
| 项 | 结果 |
|---|---|
React.createClass | ✅ 空 |
React.PropTypes | ✅ 空 |
从 react 具名导入 PropTypes | ✅ 空 |
ViewPropTypes / TextPropTypes | ✅ 空 |
字符串 ref="x" / this.refs.x | ✅ 空 |
mixins: | ✅ 空 |
componentWillReceiveProps | ✅ 空 |
componentWillMount | ✅ 空 |
measure( / measureInWindow | ✅ 空 |
它还用了很现代的写法:forwardRef + useImperativeHandle + useReducer + React.memo ✓
# 从适配仓库安装(零 dependencies)
npm install "git+https://atomgit.com/oh-react-native/react-native-otp-inputs.git#7.4.0-ohos-1.0.0" --ignore-scripts
# ⚠️ 还必须解决 @react-native-clipboard/clipboard(入口无条件 import 的原生前置):
# 正式做法:npm install @react-native-ohos/clipboard 并接入其 HAR
# 隔离验证:在 metro.config.js 的 resolver.extraNodeModules 里把它指向空桩
七、交付包质量与基线说明
| 类别 | 结果 |
|---|---|
| 交付包内容 | src/(TS 源码)+ lib/(commonjs/module/typescript)都在 ✓ 源码可读 ✓ |
spec.json | ⚠️ 缺 upstreamCommit |
validation | ⚠️ "build-pass-device-pending" |
| ⚠️ 声明的基线 | reactNative: "0.88.0" + rnoh: "0.84.4"(verification: evidence/batch50/…)⇒ 两字段自相矛盾(rnoh 0.84.4 是配 RN 0.84.x 的),且既不是当前宿主(0.84.1/0.84.3)也不是权威 0.88 基线 |
关于基线:本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。这个库没有任何已移除的写法,
行为只依赖 forwardRef / useReducer / TextInput / Platform.select ⇒ 结论跨基线成立 ✓
(唯一需要复验的是第二节那条 parseInt(Platform.Version) 的推算 —— 它取决于 Platform.Version 的字符串形态。)
八、已知限制与本次验证边界
8.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
★★★ @react-native-clipboard/clipboard 是硬前置 | 入口无条件 import ⇒ 不装就 Unable to resolve module;它是原生库(鸿蒙适配版为 @react-native-ohos/clipboard) |
★★★ reducer 在渲染期调 handleChange | 若你在 handleChange 里 setState,会出现 Cannot update a component while rendering a different component 警告 ⇒ 不必要时别传 handleChange(默认 console.log) |
★★ 鸿蒙上 supportAutofillFromClipboard 恒为 false | 因为 parseInt("OpenHarmony-…") 是 NaN ⇒ 剪贴板自动填充默认不启用 |
★★ maxLength 在鸿蒙上是 undefined | Platform.select({android:1, ios:…}) 没有 default ⇒ 失去"整串粘贴进第一格"的能力(可自行传 maxLength 覆盖) |
★ accessibilityLabel 可预测 | otpInput-0、otpInput-1…(前缀可用 testIDPrefix 改)⇒ 自动化测试很好用 |
★ isRTL 会把标签顺序反转 | inputIndex = numberOfInputs - 1 - index(实测标签从左到右是 5,4,3,2,1,0) |
★ 每格尺寸由你的 inputStyles 决定 | 实测:width/height: 60 ⇒ 180px;44 ⇒ 132px(density 3) |
★ ref 暴露 reset() 与 focus() | 可编程调用(reset() 会走一次 Clipboard.setString('')) |
8.2 本次验证的边界
- ★★ 剪贴板功能未真正验证(用空桩隔离;
ref.reset()实际不清空剪贴板、自动填充也不工作); - ★ 未做真机键盘输入验证(本轮全部用
ref编程调用与布局 dump,没有用uitest inputText输入 —— 该手段曾对 RNOH 输入框无效); - 未验证逐位输入时的自动跳格/退格回退行为(需要真实键盘事件);
- 未验证
autofillFromClipboard={true}时轮询能找到剪贴板里的验证码; - 未验证
secureTextEntry/placeholder/autoFocus/clearTextOnFocus/focusStyles的实际效果; - 未验证
keyboardType各取值; - 未验证
maxLength为undefined时的实际输入行为(只从源码推断"失去整串粘贴"); - 未验证
onKeyPress相关分支(hasKeySupport在鸿蒙上恒false); - 未在
RN 0.88.0/RNOH 0.84.4上复验(交付包声明的基线); - 未测其它屏幕宽度的布局(只验证了 440vp);
- 未做跨平台对照。
未改动库代码。
九、常见问题
Q1:它在 RN 0.84 上能跑吗?
能。 本次 19/19 条判据全部通过:契约、两个纯函数(13 条精确断言)、平台推算、渲染几何、ref 编程调用都正确。
Q2:我只用 OtpInputs,一定要装 clipboard 吗?
要。 入口 src/index.tsx 第一行就 import Clipboard from '@react-native-clipboard/clipboard' ⇒ Metro 必须解析到它。
两条路:装鸿蒙适配版 @react-native-ohos/clipboard 并接 HAR;或在 metro.config.js 的 resolver.extraNodeModules 里指向空桩(本轮做法,代价是剪贴板功能不可用)。
Q3:为什么我传了 handleChange 就报警告?
因为库的 reducer 在每个 action 内部都调用 state.handleChange(...),而 useReducer 的 reducer 在渲染期执行 ⇒
渲染期触发了你的 setState ⇒ React 报 Cannot update a component while rendering a different component。
不必要时别传 handleChange(默认是 console.log),或把 setState 延后一拍。
Q4:剪贴板自动填充为什么没生效?
看这段:
const supportAutofillFromClipboard = Platform.OS === 'android' || parseInt(Platform.Version, 10) < 14;
鸿蒙上 Platform.OS 不是 android,而 parseInt("OpenHarmony-7.0.0.32(Beta2)") 是 NaN ⇒ NaN < 14 为 false ⇒ 默认不启用。
需要的话显式传 autofillFromClipboard={true}。
Q5:为什么粘一整串验证码进不去?
maxLength 是 Platform.select({android: 1, ios: …}),没有 default ⇒ 鸿蒙上得到 undefined。
显式传 maxLength 即可绕过(或自己实现粘贴处理)。
Q6:怎么在不点屏幕、不敲键盘的情况下测试它?
用 ref:
const ref = useRef(null);
<OtpInputs ref={ref} numberOfInputs={4} handleChange={c => {}} />
ref.current.focus(); // 聚焦第一格
ref.current.reset(); // 清空(会调一次 Clipboard.setString(''))
Q7:怎么在自动化里核对"有几个格子"?
用 accessibilityLabel(otpInput-0、otpInput-1…),布局 dump 里能直接读到;
也可以按你的 inputStyles 算像素尺寸(density 3:60 ⇒ 180px)。
⚠️ isRTL 会让标签顺序反转(index = n-1-i),核对位置时注意。
小结
react-native-otp-inputs 是零 dependencies、老写法九项全空的现代函数组件库(forwardRef + useReducer + useImperativeHandle),
设备侧 19/19 条判据全部通过,主体可以正常使用。
最值得记下来的是这几条:
-
★★★★ "只有个别文件用到的原生库"在打包层面依然是硬前置。
Clipboard只在ref.reset()与自动填充里用,但import写在入口第一行 ⇒ 不装就Unable to resolve module✓
⇒ 判断"要不要装"不能看用点数量,要看 import 在不在入口的模块图里。 -
★★★★ 用"带调用计数的空桩"把"依赖能不能绕开"变成可断言的证据。
getString/setString计数 + 每次调用打日志 ⇒
默认路径 0 次 / 调reset()后setString变 1 次 ✓ —— 两阶段行为判据,比"我觉得不影响"强得多 ✓ -
★★★★ 一处平台判断的完整推算 + 双重证实。
Platform.OS === 'android' || parseInt(Platform.Version, 10) < 14
在鸿蒙上 =false || (NaN < 14 = false)=false⇒ 默认不轮询剪贴板 ✓
页面上复算它、又用桩计数证实"确实没有被调用" ✓
⇒ 凡parseInt(Platform.Version)参与的平台判断,在鸿蒙上都会得到NaN—— 这是一个通式。 -
★★★★
accessibilityLabel让"格数/尺寸/顺序"三件事同时可验证。
实测 E1 = 4 格 × 180px(60vp×3)、E2 = 6 格 × 132px(44vp×3),
而且 E2 的标签从左到右是5,4,3,2,1,0⇒ 直接把isRTL的n-1-index证实了 ✓ -
★★★★ 纯函数是最便宜的硬判据(13 条全部精确命中)。
lib/commonjs/helpers与lib/commonjs/reducer都能子路径 require ⇒
fillOtpCode4 条(含numberOfInputs=0、不足位补空串)、reducer9 条(5 个 action + 未知 action 返回同一引用)。
⚠️ 并发现reducer的每个 action 都会调state.handleChange(...)—— 这条源码事实后来被运行时警告印证了 ✓ -
★★★
reducer在渲染期调回调 ⇒ React 警告。
Cannot update a component (…) while rendering a different component (…)
⇒ 规避:不必要时别传handleChange(默认console.log),或延后一拍再setState。 -
★★ 27 处平台判断里只有 2 处得
undefined、且都不崩。
无default的Platform.select返回undefined(maxLength、selectTextOnFocus);
而带default的那一处是正面写法(Platform.select({default: rest, web: …}))✓ -
★ 原生库 HAR 的接入不是"照抄既有一行就行"。
用同样方式接入原生库 HAR 时曾遇到BUILD FAILED(Error Code: 00308018,2.5 秒即失败 ⇒ 配置/解析期错误);
⇒ 所以本轮选择打桩隔离这条零原生接线的路径,把结论做扎实、并把"未验证项"如实列出 ✓
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-otp-inputs(上游 7.4.0 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-otp-inputs |
| 适配 TAG | 7.4.0-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | ⚠️ 库本身不需要;但它硬依赖 @react-native-clipboard/clipboard(原生库)⇒ 该库需接 HAR(或打桩隔离) |
| 库类型 | OTP / 验证码分格输入框 |
| 运行时依赖 | ✅ 无 dependencies;⚠️ peer 里的 @react-native-clipboard/clipboard 被入口无条件 import |
prepare / postinstall | ⚠️ 有("prepare": "bob build")⇒ 安装加 --ignore-scripts |
| 许可证 | MIT |
| 上游仓库 | https://github.com/dsznajder/react-native-otp-inputs(★248) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = OtpInputsTestApp) |
# 从适配仓库安装
npm install "git+https://atomgit.com/oh-react-native/react-native-otp-inputs.git#7.4.0-ohos-1.0.0" --ignore-scripts
# ⚠️ 还必须解决 @react-native-clipboard/clipboard(原生前置):
# npm install @react-native-ohos/clipboard + 接入其 harmony 下的 HAR
# 或:metro.config.js 的 resolver.extraNodeModules 指向空桩(见正文第一节)
import OtpInputs from 'react-native-otp-inputs';
const ref = useRef(null);
<OtpInputs
ref={ref}
numberOfInputs={4} // ⇒ 4 个输入格(accessibilityLabel: otpInput-0..3)
handleChange={c => { /* ⚠️ 别在这里 setState —— reducer 在渲染期调用它 */ }}
inputStyles={{width: 60, height: 60}} // ⇒ 60vp × 3 = 180px
inputContainerStyles={{marginHorizontal: 2}}
keyboardType="phone-pad"
// maxLength={1} // ⚠️ 自己传,否则鸿蒙上是 undefined
// autofillFromClipboard={false} // 鸿蒙上默认就是 false
// isRTL={false} // 打开会把 accessibilityLabel 顺序反转
/>
// 编程控制
// ref.current.focus(); // 聚焦第一格
// ref.current.reset(); // 清空(会调一次 Clipboard.setString(''))
// ⚠️ 入口 src/index.tsx L1 无条件 import '@react-native-clipboard/clipboard'(原生库)
// ⚠️ 不必要时别传 handleChange(默认是 console.log)⇒ 可避免渲染期 setState 警告
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey OtpInputsTestApp
判据的复现方式
# ① 契约 / 两个纯函数 / 平台推算:逐条 PASS/FAIL 日志
hdc shell "hilog -x | grep otp-test" | Select-String 'PASS|FAIL|SUMMARY|NOTE'
# ⇒ fillOtpCode 4 条 + reducer 9 条 + supportAutofillFromClipboard=false
# ② 空桩的行为判据(两阶段)
hdc shell "hilog -x | grep clipboard-stub"
# ⇒ 默认:【一条日志都没有】;调 ref.reset() 后 ⇒ setString 累计 1
# ③ 几何 + 格数(accessibilityLabel 可直接读)
devecocli ui layout | Select-String 'otpInput-\d'
# ⇒ E1: otpInput-0..3 各 180×180;E2: otpInput-5..0 各 132×132(isRTL 反转)
验证环境
| 项 | 版本 |
|---|---|
| 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 |
Platform.OS / Platform.Version(实测值) | harmony / OpenHarmony-7.0.0.32(Beta2) |
| 本机状态栏高度 | 117px = 39vp |
| 宿主依赖 | ⚠️ @react-native-clipboard/clipboard 用带调用计数的空桩隔离 |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 3 分 44 秒 ⇒ 一次成功(打包与构建退出码都检查了) |
| 验证规模 | 19 条判据全部通过;13 条纯函数精确断言(fillOtpCode 4 + reducer 9);几何 2 组精确命中(4 格×180px / 6 格×132px);isRTL 标签反转证实;空桩两阶段计数(0 → 1);4 张截图互异 |
⚠️ 基线说明:交付包
spec.json声明RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在RN 0.84.1 + RNOH 0.84.3上完成。
本库无任何已移除写法 ⇒ 契约与纯函数结论跨基线成立;但第二节那条parseInt(Platform.Version)的推算建议换基线时复验(它取决于Platform.Version的字符串形态)。
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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)