React Native for OpenHarmony 实战:三方库 react-native-confirmation-code-field 的鸿蒙化适配指南
本文记录把 react-native-confirmation-code-field(验证码 / PIN 输入框)适配到 HarmonyOS 的完整过程。
它是一个很典型的验证码输入组件:画一排格子,用户的输入落进格子里;同时提供了一个闪烁光标、一个掩码符号,以及几个配套 hook(填满自动失焦、点击某格清空到该格)。
它的设计有个特点:格子长什么样完全由使用者决定 —— 组件只负责"把每个格子该显示什么算出来",再回调 renderCell 让你自己渲染:
// 每个格子
renderCell({ index, symbol, isFocused: isFocused && isFirstEmptySymbol })
// ↑ 只对"第一个空格"为真
实测(cellCount={6}、value="12"):
CELL F1 idx=0 sym="1" - CELL F1 idx=1 sym="2" -
CELL F1 idx=2 sym="" FOCUS ← ★ 只有它带 FOCUS
CELL F1 idx=3 sym="" - CELL F1 idx=4 sym="" - CELL F1 idx=5 sym="" -
⇒ "焦点只落在第一个空格上"这条规则被精确证实 ✓✓(symbol 的切分同时印证了 getSymbols)
另外两个纯函数也可以直接验,不需要渲染:
isLastFilledCell('1234', 2) === true // 语义是"当前值的倒数第二格"
isLastFilledCell('1234', 3) === false
getSymbols('12', 4) → ['1','2','','']
设备侧断言 29 / 29 全部通过。

⚠️ 本轮我自己踩了两个 React 的坑(都不在库里,而在我的测试页代码里),其中一个直接把整页变成了红屏 —— 排查过程比结论更值得记,见第六节。
一、先说结论
| 项 | 结果 |
|---|---|
| 上游最新版 | 9.0.0;license: MIT;上游 star 1.2k |
| 库类型 | 纯 JS 组件库:CodeField + Cursor + MaskSymbol + 3 个 hook / 工具函数 |
| 是否需要原生适配 | 不需要(无 harmony/、无 NativeModules) |
| 运行时依赖 | ✅ 零(dependencies 为空) |
prepare / postinstall | ✅ 都没有(只有发布时的 prepublishOnly) |
main | esm/index.js(tsc 编译产物,交付包不带 src/) |
| peerDependencies | react >= 19.0.0、react-native >= 0.78.0 ⇒ 明确面向 React 19 ✓ |
| 导出形态 | 正好 6 个具名导出:CodeField / Cursor / MaskSymbol / useClearByFocusCell / useBlurOnFulfill / isLastFilledCell |
| 需要 HAR / 权限 / ohpm | ❌ 都不需要 |
| 设备侧断言 | ✅ 29 / 29 通过(A 22 + B 4 + C 3) |
| ★★ 焦点规则实测 | cellCount=6 + value="12" ⇒ 只有 index=2(第一个空格)带 FOCUS |
| ★ 纯函数直调 | isLastFilledCell 6 例、getSymbols 3 例全部符合 |
| ★ 隐藏输入框 | absoluteFill + opacity: 0.015 + maxLength={cellCount} + autoComplete 非 android 取 'one-time-code' |
| ★ 我自己的两个坑 | ① 事件对象读进 setState 的 updater ⇒ 红屏 ② 在 renderCell(渲染期回调)里 setState |
二、判定与接入
| 步 | 做法 | 结果 |
|---|---|---|
| ① | package.json 里有没有 harmony.autolinking | 没有 |
| ② | 仓库里有没有 harmony/ | 没有 |
| ③ | 代码里有没有 NativeModules / requireNativeComponent | 全无 |
| ④ | import 图体检 | 只有 react / react-native + 相对路径 ⇒ 零依赖 ✓ |
| ⑤ | scripts 扫 prepare 家族 | ✅ 没有 |
npm install "git+https://atomgit.com/oh-react-native/react-native-confirmation-code-field.git#9.0.0-ohos-1.0.0" --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-confirmation-code-field') ],
import CodeFieldTestApp from './CodeFieldTestApp';
AppRegistry.registerComponent('CodeFieldTestApp', () => CodeFieldTestApp);
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey CodeFieldTestApp
不需要 HAR、权限、ohpm、link-harmony,也不需要补装任何依赖。
三、读源码取到的真值
交付包只有
esm/**(tsc产物),所以以下引用都来自esm/**。
3.1 导出与形态
// esm/index.js
export { Cursor } from './Cursor';
export { CodeField } from './CodeField';
export { MaskSymbol } from './MaskSymbol';
export { useClearByFocusCell } from './useClearByFocusCell';
export { useBlurOnFulfill } from './useBlurOnFulfill';
export { isLastFilledCell } from './utils';
实测:命名空间正好 6 个 键;CodeField / Cursor / MaskSymbol 都是函数组件(箭头函数 ⇒ 没有 prototype.render),且都设了 displayName:
displayName = CodeField Cursor MaskSymbol
3.2 ★★ CodeField 的核心:把"每格该显示什么"算出来
export const CodeField = ({ rootStyle, textInputStyle, value, renderCell,
cellCount = DEFAULT_CELL_COUNT /* 4 */, RootProps, RootComponent = View,
InputComponent = TextInput, ref, ...rest }) => {
'use memo';
const [isFocused, onFocus, onBlur] = useFocusState(rest.onBlur, rest.onFocus);
const symbols = getSymbols(value || '', cellCount);
const cells = symbols.map((symbol, index, symbols) => {
const isFirstEmptySymbol = symbols.indexOf('') === index;
return renderCell({ index, symbol, isFocused: isFocused && isFirstEmptySymbol });
});
return (
<RootComponent {...RootProps} style={rootStyle ? [styles.root, rootStyle] : styles.root}>
{cells}
<InputComponent caretHidden spellCheck={false} autoCorrect={false} blurOnSubmit={false}
clearButtonMode="never" autoCapitalize="characters" underlineColorAndroid="transparent"
maxLength={cellCount} autoComplete={autoComplete} {...rest}
value={value} onBlur={onBlur} onFocus={onFocus}
style={textInputStyle ? [styles.textInput, textInputStyle] : styles.textInput} ref={ref}/>
</RootComponent>
);
};
两个关键点:
- ★
isFocused只对"第一个空格"为真(symbols.indexOf('') === index)⇒ 实测value="12"、6 格时,只有index=2带FOCUS✓ - ★★ 隐藏输入框是"透明铺满"的:
textInput: {
...StyleSheet.absoluteFill, // 铺满整个 root
opacity: 0.015, // 几乎完全透明(用来接收点击)
fontSize: 1, // 让光标/文字极小
},
⇒ 所以点格子区域就等于点那个输入框 ✓(这也是为什么"点某格清空"那种功能可以工作)
⇒ maxLength = cellCount;autoComplete:
const autoComplete = Platform.select({ android: 'sms-otp', default: 'one-time-code' });
⇒ 鸿蒙(不是 android)取 'one-time-code'(与 iOS 同档)
3.3 ★ 三个配套 hook
// useFocusState:自己维护 isFocused,并把事件透传给你
export const useFocusState = (onBlurFromProps, onFocusFromProps) => {
const [isFocused, setFocusFlag] = useState(false);
const onBlur = useCallback((e) => { setFocusFlag(false); onBlurFromProps?.(e); }, [onBlurFromProps]);
const onFocus = useCallback((e) => { setFocusFlag(true); onFocusFromProps?.(e); }, [onFocusFromProps]);
if (__DEV__) { useDebugValue(isFocused ? 'Focused' : 'Unfocused'); } // ★ 只在 __DEV__
return [isFocused, onFocus, onBlur];
};
// useBlurOnFulfill:填满就失焦
export const useBlurOnFulfill = ({ value, cellCount }) => {
const inputRef = useRef(null);
useEffect(() => { if (value?.length === cellCount) { inputRef.current?.blur(); } }, [value, cellCount]);
return inputRef;
};
// useClearByFocusCell:点哪一格,就把值截断到那一格之前
const clearCodeByCoords = useCallback((coords) => {
const index = findIndex(coords, cellsLayouts.current); // 用 locationX/locationY 命中格子
if (index !== -1) {
const { value, setValue } = paramsRef.current;
setValue((value || '').slice(0, index)); // ★ slice(0, index)
}
}, []);
⇒ useClearByFocusCell 返回 [inputProps, getCellOnLayoutHandler]:inputProps 里的 onPressOut 负责命中判断,getCellOnLayoutHandler(index) 要挂到每个格子的 onLayout 上(用来记录格子坐标)✓
3.4 ★★ 两个可直接断言的纯函数
// esm/utils.js
export const isLastFilledCell = (value, index) => value != null && value.length - 2 === index;
export const getSymbols = (codeValue, codeLength) => {
const symbols = new Array(codeLength);
for (let i = 0; i < codeLength; i++) { symbols[i] = codeValue[i] ?? ''; }
return symbols;
};
实测(不用渲染就能验):
getSymbols("12", 4) → ["1","2","",""]
getSymbols("", 3) → ["","",""]
getSymbols("123456", 3) → ["1","2","3"] ← 截断到 codeLength
isLastFilledCell("1234", 2) === true ← 语义是"倒数第二格"
isLastFilledCell("1234", 3) === false
isLastFilledCell("12", 0) === true ← 2-2=0
isLastFilledCell(null, 0) === false
3.5 闪烁与掩码
// Cursor:每 delay(默认 500ms) 在 "|" 与 "" 之间切换
useInterval(toggleVisibility, delay);
return <>{visibleFlag ? cursorSymbol : ''}</>;
// MaskSymbol:500ms 后翻一次面;isLastFilledCell 为真时立刻隐藏
useTimeout(toggleVisibility, delay);
useEffect(() => { if (isLastFilledCell) { setFlag(false); } }, [isLastFilledCell]);
return <>{visibleFlag ? symbol : maskSymbol}</>;
四、验证设计
| 判据 | 内容 |
|---|---|
| ① 纯函数直调 | getSymbols / isLastFilledCell 的若干输入输出(不依赖渲染) |
| ② 焦点规则 | renderCell 把它们全部打日志 ⇒ 核对"只有第一个空格带 FOCUS" |
| ③ 结构 | 6 个导出、三个组件是函数组件、displayName |
| ④ 时序(本轮未完成) | Cursor 闪烁 / MaskSymbol 掩码 ⇒ 计划用两次 dump 读文本比对 |
五、实测结果
5.1 断言 29 / 29
| 组 | 内容 | 结果 |
|---|---|---|
| A 契约 | 6 个导出、三个组件是函数组件(无 prototype.render)、displayName、isLastFilledCell 6 例、getSymbols 3 例 | 22/22 |
| B 渲染与几何 | 全部实例渲染未抛错、标题不与状态栏重合、renderCell 调用次数与 FOCUS 记录 | 4/4 |
| C 边界与交互 | renderCell 日志已产生、两个受控值、判据与边界记录 | 3/3 |
| 合计 | 29/29 |
关键观测值:
命名空间导出 = CodeField, Cursor, MaskSymbol, isLastFilledCell, useBlurOnFulfill, useClearByFocusCell
displayName = CodeField Cursor MaskSymbol
getSymbols("12", 4) = ["1","2","",""] getSymbols("", 3) = ["","",""] getSymbols("123456", 3) = ["1","2","3"]
★ F1 renderCell 调用次数(cellCount=6) = 18 (3 次渲染 × 6 格)
★ F1 被标记 focused 的格子 = F1 idx=2 sym="" FOCUS
各实例容器尺寸(vp) = {"f1":"416x64"}
UI 安全区:TITLE measureInWindow y=420 > 117 ✓
5.2 ★★ 焦点规则(核心证据)
<CodeField cellCount={6} value="12" /> 时 renderCell 的调用日志:
CELL F1 idx=0 sym="1" - CELL F1 idx=1 sym="2" - CELL F1 idx=2 sym="" FOCUS
CELL F1 idx=3 sym="" - CELL F1 idx=4 sym="" - CELL F1 idx=5 sym="" -
⇒ symbol 的切分符合 getSymbols(['1','2','','','',''])✓
⇒ FOCUS 只出现在 index=2(第一个空格) ✓✓ 源码里 isFocused && isFirstEmptySymbol 的语义被完整证实

5.3 其它
- F1 的 root 尺寸 =
416×64vp(格子 44vp + 上下 padding 10vp)⇒ 一排格子铺满可用宽度 ✓ - F1 的
renderCell共被调 18 次 = 3 次渲染 × 6 格 ✓(说明它是"每格每次渲染都回调")


六、★ 我自己踩的两个 React 坑(本篇最值得记的部分)
6.1 坑一:把事件对象读进 setState 的 updater ⇒ 整页红屏
我最初这样测量容器尺寸:
onLayout={e => setGeoms(p => ({...p, f1: `${Math.round(e.nativeEvent.layout.width)}x…`}))}
// ^^^^^^^^^^^^^^^ 在 updater 内部读事件对象
结果:页面启动后整屏被 LogBox 红屏盖住,报
Render Error
Cannot read property 'layout' of null
Component Stack: <CodeFieldTestApp /> → <View_withRef /> → <View_withRef />
Call Stack: anonymous → basicStateReducer → updateReducerImpl → updateReducer → useState
排查过程(值得记):
- 先看到"页面标题 dump 不到、日志一条没有";
- 用
devecocli ui layout打印整棵 dump ⇒ 最外层是Dialog,里面有logbox_message_title_text "Render Error"和logbox_message_contents_text "Cannot read property 'layout' of null",以及 Component Stack / Call Stack; - 调用栈里的
basicStateReducer直接指出"错误发生在useState的 updater 里" ⇒ 定位到我自己那行。
为什么错:setGeoms(p => …) 的 updater 函数不是在调用时执行,而是在下一次渲染的 reducer 阶段执行 ⇒ 那时再去读 e.nativeEvent,事件对象已经不保证可用(这里是 null)。
修法:先把值取出来,updater 里只用普通数值:
onLayout={e => {
const w = e?.nativeEvent?.layout?.width;
const h = e?.nativeEvent?.layout?.height;
const txt = `${Math.round(w ?? 0)}x${Math.round(h ?? 0)}`;
setGeoms(p => ({...p, f1: txt})); // ✅ updater 里只用 txt
}}
6.2 坑二:在 renderCell 里 setState ⇒ 渲染期更新别的组件
我为了把每格日志显示到页面上,在 renderCell 回调里调了 setCellLog(...)。LogBox 立刻报:
Cannot update a component (`CodeFieldTestApp`) while rendering a different component
为什么错:renderCell 是 CodeField 在渲染期间调用的(symbols.map(...) 就在 render 里)⇒ 此时调用 setState 属于 “渲染期更新另一个组件” ✗ React 明确禁止。
修法:renderCell 里只写 ref + console.log,不碰 state;等用户点按钮时再从 ref 同步到 state 用于展示:
const logCell = (msg: string) => {
logRef.current = [...logRef.current, msg].slice(-40); // ✅ 只写 ref
console.log(`[codefield-test] CELL ${msg}`); // ✅ 只打日志
};
教训:“回调"不等于"可以在里面随便改状态” —— 要分清这个回调是事件回调(点击、滚动,可以 setState)还是渲染期回调(
renderCell/renderItem/renderText这类由父组件在 render 里调用的,不能 setState)。
七、已知限制与本次验证边界
7.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
| 格子由你自己渲染 | 组件只给 renderCell({index, symbol, isFocused}),没有内置外观 |
isFocused 的含义 | 只对"第一个空格"为真(不是"当前光标格") |
| 隐藏输入框铺满 root | absoluteFill + opacity 0.015 ⇒ 点任意格子区域都会命中它 |
autoComplete 在非 android 平台是 'one-time-code' | 鸿蒙取这个分支 |
isLastFilledCell(value, index) 的语义 | value.length - 2 === index ⇒ “当前值的倒数第二格”,容易误读 |
useClearByFocusCell 是"截断" | 点第 N 格会把值截成前 N 位(slice(0, index)),不是"清空该格" |
useBlurOnFulfill 按长度判断 | value.length === cellCount 才失焦 |
useFocusState 的 useDebugValue 只在 __DEV__ | 生产环境不执行 |
7.2 本次验证的边界
Cursor的闪烁与MaskSymbol的掩码时序【未完成验证】 —— 原计划"两次 dump 读文本比对",两次文本提取都写错了(一次区间取到 0、一次匹配式不严谨),没有拿到有效结果 ⇒ 只做了源码级记录,没有实测;useClearByFocusCell的命中逻辑未做交互实测(需精确点到某格并观察值被截断;本轮只验证了受控值存在与 hook 可用);useBlurOnFulfill的失焦未实测(需真的输入到cellCount位);- 焦点交互复查未得到确定结论:我试过"点击 F1 区域后再看 FOCUS 日志",清日志后未再出现 FOCUS(点击前后语义不明确)⇒ 未把"点击使其聚焦"作为结论;
autoComplete='one-time-code'只是源码级结论(未在设备上验证实际自动填充行为);RootComponent/InputComponent自定义容器未验证;useClearByFocusCell的 web 变体(useClearByFocusCell.web.js)与CodeField.styles.web.js未验证;- 只在一台模拟器上验证(
Pura X View,density 3,1320×2232),没有真机;未做跨平台对照。
未改动库代码。
八、常见问题
Q1:怎么用?
import {CodeField, Cursor} from 'react-native-confirmation-code-field';
const [value, setValue] = useState('');
<CodeField
value={value}
onChangeText={setValue}
cellCount={6}
keyboardType="number-pad"
textContentType="oneTimeCode"
renderCell={({index, symbol, isFocused}) => (
<View key={index} style={[styles.cell, isFocused && styles.focusCell]}>
<Text>{symbol || (isFocused ? <Cursor /> : null)}</Text>
</View>
)}
/>
Q2:为什么"只有第一个空格"会显示光标?
因为组件给 renderCell 的 isFocused 是 isFocused && isFirstEmptySymbol(symbols.indexOf('') === index)—— 它的语义是"当前应该输入的位置",不是"光标所在格"。实测 value="12"、6 格时只有 index=2 为真。
Q3:怎么让填满后自动收起键盘?
const ref = useBlurOnFulfill({value, cellCount: 6});
<CodeField ref={ref} value={value} cellCount={6} … />
Q4:想"点第 3 格就从第 3 格开始重输"怎么做?
const [props, getCellOnLayoutHandler] = useClearByFocusCell({value, setValue});
<CodeField {...props} value={value} onChangeText={setValue} renderCell={({index, symbol, isFocused}) => (
<View key={index} onLayout={getCellOnLayoutHandler(index)} …>{symbol}</View>
)} />
⇒ 点第 N 格会把值截断成前 N 位(不是清空该格)。
Q5:isLastFilledCell 怎么用?
<MaskSymbol isLastFilledCell={isLastFilledCell(value, index)} maskSymbol="•">{symbol}</MaskSymbol>
⚠️ 它的判断是 value.length - 2 === index ⇒ “倒数第二格”(想表达"最后一个已填格"要留意这个差 2)。
Q6:为什么我这排格子点哪儿都能聚焦?
因为库在格子下面铺了一个 absoluteFill + opacity: 0.015 的 TextInput ⇒ 整块区域都是它的点击范围。
Q7:我想换掉外层的 View 或输入框?
用 RootComponent / InputComponent 两个 prop 替换(默认分别是 View 与 TextInput)。
Q8:这个库需要鸿蒙侧做什么特殊处理吗?
不需要。 纯 JS、零依赖、无原生模块、不需要 HAR / 权限 / ohpm。而且它的 peerDependencies 明确写了 react >= 19,属于面向新 React 的库。
小结
react-native-confirmation-code-field 是纯 JS 的验证码输入组件(零依赖、peerDependencies 明确要求 react >= 19)。设备侧 29 / 29 断言通过。
最值得记下来的是这几条:
-
★★ "渲染期回调"里不能
setState。 我在renderCell(由CodeField在 render 里调用)中调了setCellLog,LogBox 直接报Cannot update a component while rendering a different component。⇒renderCell/renderItem/renderText这类回调只应写 ref 或打日志,要改状态得等事件回调。 -
★★ 不要把事件对象读进
setState的 updater 里。 我写成setGeoms(p => …e.nativeEvent.layout…)⇒ updater 在下一次渲染的 reducer 阶段才执行,届时事件对象已不可用(null)⇒ 整页红屏(Cannot read property 'layout' of null)。
⇒ 修法:先把值取出来,updater 里只用普通数值。 -
★ 红屏也能用 dump 读出来。 这次页面标题 dump 不到、日志一条没有,靠
devecocli ui layout打印整棵树才发现最外层是Dialog、里面有logbox_message_title_text "Render Error"与 Component Stack / Call Stack,从basicStateReducer一眼定位到"错在useState的 updater 里"。⇒ 界面被弹窗盖住时,先去 dump 里找弹窗的文本,而不是盲猜。 -
★ 能直接调的纯函数优先直接调。
getSymbols/isLastFilledCell不需要渲染就能验:getSymbols('12',4) → ['1','2','','']、isLastFilledCell('1234',2) → true(语义是倒数第二格)。⇒ 这类断言最快、最稳、零环境依赖。 -
★ 借助
renderCell这类"每格都回调"的钩子做页内观测。 我把每格的(index, symbol, isFocused)全打进日志,于是**"焦点只落在第一个空格"这条规则被一次性证实**(实测只有idx=2带FOCUS)。 -
没做完的要如实写。
Cursor闪烁 /MaskSymbol掩码的时序判据本轮两次尝试都写错了,最终没有拿到实测结果 ⇒ 文章里明确标注"未完成验证",只留源码级记录。
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-confirmation-code-field(上游 9.0.0 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-confirmation-code-field |
| 适配 TAG | 9.0.0-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS,零依赖) |
| 库类型 | 纯 JS 组件库:验证码 / PIN 输入(CodeField + Cursor + MaskSymbol + 3 个 hook/工具) |
| 运行时依赖 | ✅ 零 |
| peerDependencies | react >= 19.0.0、react-native >= 0.78.0 |
prepare / postinstall | ✅ 都没有 |
| 许可证 | MIT |
| 上游仓库 | https://github.com/retyui/react-native-confirmation-code-field(★1.2k) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = CodeFieldTestApp) |
# 从适配仓库安装(零依赖,无需补装)
npm install "git+https://atomgit.com/oh-react-native/react-native-confirmation-code-field.git#9.0.0-ohos-1.0.0" --ignore-scripts
# 本地目录接入
npm install ../react-native-confirmation-code-field --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-confirmation-code-field') ],
import {CodeField, Cursor, MaskSymbol, useBlurOnFulfill, useClearByFocusCell, isLastFilledCell}
from 'react-native-confirmation-code-field';
const CELL_COUNT = 6;
const [value, setValue] = useState('');
// ① 基本用法(格子外观完全由你决定)
<CodeField
value={value}
onChangeText={setValue}
cellCount={CELL_COUNT}
keyboardType="number-pad"
renderCell={({index, symbol, isFocused}) => (
<View key={index} style={[styles.cell, isFocused && styles.focusCell]}>
<Text style={styles.txt}>{symbol || (isFocused ? <Cursor /> : null)}</Text>
</View>
)}
/>
// ② 填满自动失焦
const ref = useBlurOnFulfill({value, cellCount: CELL_COUNT});
<CodeField ref={ref} … />
// ③ 点某格从该格重输(会把值截断到该格之前)
const [cellProps, getCellOnLayoutHandler] = useClearByFocusCell({value, setValue});
<CodeField {...cellProps} value={value} onChangeText={setValue}
renderCell={({index, symbol, isFocused}) => (
<View key={index} onLayout={getCellOnLayoutHandler(index)} …>{symbol}</View>
)} />
// ④ 掩码(注意 isLastFilledCell 的语义是"倒数第二格")
<MaskSymbol isLastFilledCell={isLastFilledCell(value, index)} maskSymbol="•">{symbol}</MaskSymbol>
// ⚠️ 在 renderCell 里【不要】setState:它是渲染期回调
// ⚠️ 不要把事件对象读进 setState 的 updater 里
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey CodeFieldTestApp
判据的复现方式
# ① 纯函数(最快、零环境依赖):
# getSymbols('12', 4) → ['1','2','',''];isLastFilledCell('1234', 2) → true
# ② 焦点规则:在 renderCell 里 console.log(index, symbol, isFocused)
# 然后 grep 日志:cellCount=6、value='12' ⇒ 只应有 idx=2 带 FOCUS
hdc shell "hilog -x | grep codefield-test" | Select-String 'CELL F1.*FOCUS'
# ③ 页面被弹窗盖住时:先 dump 整棵树找弹窗文本(LogBox 会给出 Render Error 与调用栈)
devecocli ui layout
# ⇒ 找 logbox_message_contents_text / logbox_stack_frame_text
验证环境
| 项 | 版本 |
|---|---|
| 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(页面留白取 48vp) |
| 宿主依赖 | ✅ 无需补装(库零依赖) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 首轮 4 分 00 秒(红屏);修 bug 后 4 分 13 秒、4 分 06 秒 ⇒ 最终 29/29 |
| 验证规模 | 设备侧 29 / 29 断言;纯函数直调 9 例(getSymbols 3 + isLastFilledCell 6);焦点规则 1 组(6 格逐格日志,仅 idx=2 带 FOCUS);结构 6 项;UI 安全区 1 项;两个自定义 bug 的定位与修复 |
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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)