尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

OpenHarmony上React Native SearchBar组件封装与避坑实践

OpenHarmony上React Native SearchBar组件封装与避坑实践 1. 项目背景与场景拆解1.1 为什么在 OpenHarmony 上用 React Native 写 SearchBarReact Native 在 OpenHarmony 上的实战应用最近问的人越来越多了。尤其是 SearchBar 这种看起来简单、实际上到处都是细节的搜索栏组件几乎每个 App 都要用但真放到鸿蒙的 RN 适配环境里跑问题就一个一个冒出来了。我们团队的 App 从立项开始就走的是跨平台路线一套 React Native 业务代码同时出 Android、iOS 和 OpenHarmony 三个版本。这么定的原因很实际——前端业务团队就一套人马如果 OpenHarmony 版本再单独用 ArkUI 写一遍业务代码以后每次改需求就要改两遍维护成本直接翻倍而且两端逻辑一旦不同步产品体验也会慢慢跑偏。RN 的 OpenHarmony 社区适配方案本质上是把 RN 组件层映射到 ArkUI 渲染层让 JS 业务代码不用大改就能跑在新的系统上。这个 SearchBar 就是我在接这个项目后第一个从零开始啃的组件。当时我心想一个输入框加两个按钮能有多少坑结果在模拟器上跑起来之后从白屏到渲染异常再到软键盘弹不出来一路踩过来前后花了两天时间才把整个交互链路理顺。所以这篇文章的核心不是教你怎么写一个普通 SearchBar而是把我在 OpenHarmony React Native 环境下验证过的组件设计、代码实现和排坑过程完整梳理一遍给同样在这条路上爬坑的人做个参考。1.2 SearchBar 的需求形态分析先别急着写代码把需求拆清楚比什么都重要。SearchBar 在业务里其实不是一个孤立的组件而是一组交互状态搜索入口形态首页常驻一个搜索框展示占位文案点击后跳转到搜索页搜索页形态跳转后搜索框自动聚焦软键盘直接弹出来用户在输入过程中可以清空输入、实时触发联想搜索按回车直接发起搜索点取消回到上一页中间态输入过程中展示联想词、历史搜索记录这些内容跟 SearchBar 本身没有直接耦合但需要 SearchBar 把当前输入词稳定地抛给上层使用。三种形态里最复杂的就是搜索页里的 SearchBar。我基于实际业务整理了一份需求清单支持受控和非受控两种用法方便不同业务场景直接套用输入过程中出现清空按钮输入为空时必须隐藏支持防抖搜索避免每敲一个字符就发一次网络请求支持键盘回车键直接触发搜索returnKeyType 为 search进入搜索页自动聚焦软键盘立即弹出点击取消按钮能收回键盘并触发返回逻辑在 OpenHarmony 环境下全链路可用包括软键盘、输入框光标、点击事件、文字渲染。这七条里每一条在 Android 和 iOS 上都有成熟稳定的写法但在鸿蒙的 RN 适配层上成熟稳定四个字要打不少折扣。后面我会逐条展开讲。2. 整体方案设计组件长什么样2.1 组件功能分区与数据流SearchBar 从视觉上可以分成两块左边一个圆角输入框内含搜索图标、TextInput、清空按钮右边一个可选的取消按钮。从职责上则可以分成四层第一层是 UI 层负责把输入框、图标、按钮渲染出来这层不关心业务逻辑第二层是事件层负责把输入、提交、清空、取消这些用户动作转换成回调事件抛给上层第三层是状态层管理输入内容和防抖定时器第四层是配置层通过 props 接收占位文案、默认值、按钮显隐、防抖时长这些配置项。四个层次用一句话概括就是配置驱动状态、状态驱动 UI、事件驱动回调。这样拆分之后组件内部逻辑清晰上层接入方也只需要关心 props 和回调不需要了解内部实现。这个设计思路同样适用于其他高频复用组件比如筛选条、底部弹层后面你写类似组件的时候可以直接复用这套分层方式。2.2 为什么不用 ArkUI 单独实现一个搜索栏在 OpenHarmony 上如果只用 ArkUI 写SearchBar 确实更简单官方组件库里的 Search 组件拿来就能用。但问题在于我们整个项目的业务代码是 React Native 的如果 SearchBar 单独用 ArkUI 写就需要通过混合栈的方式把原生组件嵌进 RN 页面里。混合栈方案不是不行但代价很直接搜索栏在很多页面都会被复用每接入一个页面都要写一遍原生侧和 JS 侧的桥接代码业务上搜索词可能要在不同页面间共享ArkUI 组件和 RN 组件之间的数据同步要多写很多胶水代码后续如果需求变化比如换样式、加搜索建议改动要跨两个技术栈排期直接翻倍。所以最终我选择全部走 React Native 方案SearchBar 用 RN 的 View、TextInput、Pressable 这些基础组件拼出来。这样做最大的好处是组件在同一套代码里可以自由复用到 Android、iOS、OpenHarmony 三个平台后续改样式也只需要改一处。2.3 受控与非受控的设计取舍SearchBar 内部一定要处理好受控和非受控两种模式。受控模式是指输入值完全由父组件管理父组件通过 value 传进来、通过 onChange 收回去非受控模式是组件自己管理输入值父组件只在需要的时候读取。我在封装的时候采用的策略是如果 props 里传了 value就走受控模式没传就走非受控模式。这个判断在组件代码里就是一个三元表达式的事但非常关键——很多业务场景里搜索关键词需要在搜索页和结果页之间共享这时候父组件必须控制输入内容。比如用户点击历史搜索词时父组件要主动把词条塞进搜索框里如果 SearchBar 自己管状态父组件塞进去的词就不会立即生效。判断逻辑是这样写的const isControlled value ! undefined; const currentValue isControlled ? value : innerValue;这段代码虽然是三行的事但背后决定了整个组件的使用方式。封装通用组件的时候我建议都按这个模式处理受控和非受控兼容性最好。3. SearchBar 核心功能实现3.1 TextInput 在鸿蒙 RN 环境的关键 PropsTextInput 是整个 SearchBar 的心脏。在 OpenHarmony 的 RN 适配层上TextInput 大部分常用 props 是可用的但有几个点需要特别注意。第一个是 autoFocus。Android 和 iOS 上 autoFocus 都能让页面加载出来之后自动聚焦并弹出软键盘但在鸿蒙的适配层上我遇到的情况是 autoFocus 偶尔会失效尤其当页面还有进场动画或者异步加载逻辑时。稳妥的做法是在页面请求完成、组件挂载稳定之后通过 ref 主动调用 focus() 方法。这个我在后面接入部分的代码里会展示。第二个是 returnKeyType 和 onSubmitEditing。这两个是配合使用的returnKeyTypesearch 可以把键盘右下角的回车键变成搜索样式onSubmitEditing 在用户点击这个键时触发搜索逻辑。在鸿蒙上这两个 props 基本可用但要注意 onSubmitEditing 的触发时机偶尔会滞后防抖逻辑和 loading 状态要处理好避免用户连续点击产生重复搜索。第三个是 selectionColor。这个属性控制光标颜色和选区高亮颜色。在 OpenHarmony 的默认渲染里TextInput 的光标颜色不一定跟随主题色需要显式设置。实测下来 selectionColor 是生效的建议在组件里把它设成主题色否则光标默认颜色跟 App 的视觉风格不搭。第四个是 placeholderTextColor。这个同样是易被忽略的样式点不设置的话在部分鸿蒙字体渲染环境下占位符文字的颜色会非常浅用户几乎看不到提示。为了让你看得更清楚我把这几个 props 在 OpenHarmony 上的实测表现整理成了一个表Prop效果鸿蒙适配表现注意事项autoFocus自动聚焦偶发失效用 ref.focus() 兜底returnKeyTypesearch回车键变成搜索稳定可用配合 onSubmitEditingonSubmitEditing键盘搜索触发回调基本稳定注意防抖和 loadingselectionColor光标颜色生效建议显式设置主题色placeholderTextColor占位符颜色部分环境过浅必须显式设置maxLength最大输入长度稳定可用按业务需求设置3.2 清空按钮与取消按钮的交互细节清空按钮的交互逻辑比较直白输入内容长度大于 0 就显示点击之后清空输入内容并且把焦点还给输入框。这个需求点本身不难但有几个实现细节值得说。第一清空按钮用 Pressable 而不是 TouchableOpacity。鸿蒙适配层对 Touchable 系列组件的支持相比 Pressable 要粗糙一些Pressable 的按下态、点击事件在鸿蒙上更稳定。我在实测中遇到过 TouchableOpacity 点击响应区域不准确的情况换成 Pressable 之后就好了。第二清空按钮的点击区域要放大。按钮本身视觉尺寸只有 18 左右如果按视觉大小去做点击区域用户很难精准点到。我用 hitSlop 把点击范围扩大了 10 个像素实际体验会好很多。第三清空时机。点击清空按钮时除了清空输入内容还要把焦点重新给回输入框。这个交互在 iOS 上尤其被用户习惯虽然鸿蒙上没有这种强预期但保持一致总是好的实现方式就是 inputRef.current?.focus()。取消按钮的逻辑就两件事收回焦点、触发 onCancel 回调。收回焦点用 inputRef.current?.blur()onCancel 由父组件决定是返回上一页还是做其他事情。这里要提醒一下不要试图在 SearchBar 组件内部用 navigation.goBack()组件不应该感知路由这会让组件失去复用性。3.3 防抖搜索与事件去抖实现SearchBar 的搜索场景通常有两种触发方式一是用户按回车或点搜索按钮这是即时触发二是输入过程中的实时搜索比如联想词。第二种如果不做任何处理用户输入鸿蒙开发四个字就可能触发四次请求网络开销和服务器压力都很明显。我采用的防抖方案是把计时器存在 useRef 里在 onChangeText 回调中每次都清掉上一个定时器重新设置一个新的定时器只有用户停止输入超过指定时长默认 300ms才会触发搜索回调。这样既保证了搜索的实时性又不会因为用户连续输入而狂发请求。这里有个容易被踩的坑防抖定时器一定要在组件卸载时清理否则组件已经销毁了定时器还在跑回调触发后上层拿到一个已卸载组件的状态轻则报 warning重则引发状态更新异常。正确做法是在 useEffect 的清理函数里 clearTimeout。不要用实例变量去存定时器useRef 在 React 函数组件里更干净。为什么用 useRef 而不是直接在函数里声明变量因为 useRef 返回的对象在整个组件生命周期内是同一个引用定时器 ID 可以跨多次渲染被正确读取和清理而普通局部变量每次渲染都会重新创建防抖逻辑就会失效。3.4 样式适配与安全区处理SearchBar 的样式看起来简单但放到不同平台上就是另一回事了。首先是字体。鸿蒙系统的默认字体跟其他平台不同text-align、font-weight 这些基础样式在 RN 适配层上基本能生效但个别字重渲染出来后差异很大。如果视觉稿要求比较严格建议给关键文字显式指定 fontSize 和 color不要依赖系统默认值。其次是圆角。inputWrapper 使用 borderRadius: 20 在鸿蒙上可以正常渲染这一点比某些定制 Android ROM 还要稳定。但要注意如果输入框背景色是白色、容器背景是灰色圆角内不能有其他元素漏出否则视觉上会有一圈杂色。第三是安全区。如果 SearchBar 会被用在带刘海屏或者胶囊键的设备上顶部要预留安全区高度。RN 的 SafeAreaView 在鸿蒙适配层上有一定的支持但实测效果不太稳定我更推荐的做法是用封装好的 useSafeAreaInsets hook 去读取安全区高度手动给 SearchBar 容器设置 paddingTop。这个方案不依赖平台组件在三个平台上表现一致。4. 完整实操流程与代码实现4.1 工程环境准备在写 SearchBar 之前先把 React Native 的 OpenHarmony 工程跑起来。整个环境准备大致分四步第一步安装 DevEco Studio 并配置好 HarmonyOS SDK。注意选择支持 API 9 以上版本的 SDK因为 RN 的 OpenHarmony 适配包对 API 版本有要求。DevEco Studio 安装完成后还要配置好 hdc 工具后面真机调试会用到。第二步创建一个标准的 React Native 工程。这里版本选择很重要我当时用的是 react-native 0.72 以上版本配合社区维护的 react-native-harmony 适配层不要用太老的 RN 版本适配层对 Fabric 和 TurboModule 的支持在不同版本差异很大。第三步在 RN 工程里安装鸿蒙适配依赖并按照适配层文档在 harmony 目录下生成鸿蒙原生工程。这个原生工程会作为 App 的壳工程最终打包成 hap 文件安装到鸿蒙设备或模拟器上。这一步最容易出问题的是版本号对齐适配层要求的 RN 版本和你工程里的版本必须一致。第四步开发调试阶段用 Metro 提供 JS bundle。启动 Metro 之后在 DevEco Studio 里把工程跑起来模拟器会连接到 Metro 加载 bundle。这里有个要注意的点模拟器里访问宿主机 Metro 的地址要写电脑的局域网 IP而不是 localhost。我一开始就用 localhost结果白屏了很久才排查出来原因。4.2 SearchBar 完整代码实现下面直接给出我在项目中沉淀下来的 SearchBar 组件代码。已经经过了 OpenHarmony 真机和模拟器的双重验证可以直接复制到工程里用。// SearchBar.js import React, { useRef, useState, useCallback, useEffect } from react; import { View, TextInput, Text, Pressable, StyleSheet, } from react-native; const SearchBar ({ placeholder 请输入搜索关键词, value, defaultText , onChange, onSearch, onCancel, autoFocus false, showCancelButton true, debounceTime 300, containerStyle, }) { const [innerValue, setInnerValue] useState(defaultText); const inputRef useRef(null); const timerRef useRef(null); // 判断受控还是非受控 const isControlled value ! undefined; const currentValue isControlled ? value : innerValue; const handleChangeText useCallback( (text) { if (!isControlled) { setInnerValue(text); } if (onChange) { onChange(text); } if (timerRef.current) { clearTimeout(timerRef.current); } timerRef.current setTimeout(() { if (onSearch) { onSearch(text); } }, debounceTime); }, [isControlled, onChange, onSearch, debounceTime], ); // 组件卸载时清理定时器 useEffect(() { return () { if (timerRef.current) { clearTimeout(timerRef.current); } }; }, []); // 延迟聚焦解决鸿蒙 autoFocus 时序问题 useEffect(() { if (autoFocus) { const focusTimer setTimeout(() { inputRef.current?.focus(); }, 300); return () clearTimeout(focusTimer); } }, [autoFocus]); const handleClear useCallback(() { if (!isControlled) { setInnerValue(); } if (onChange) { onChange(); } inputRef.current?.focus(); }, [isControlled, onChange]); const handleSubmit useCallback(() { // 提交搜索时清掉防抖定时器避免重复触发 if (timerRef.current) { clearTimeout(timerRef.current); } if (onSearch) { onSearch(currentValue); } inputRef.current?.blur(); }, [currentValue, onSearch]); const handleCancel useCallback(() { if (timerRef.current) { clearTimeout(timerRef.current); } inputRef.current?.blur(); if (onCancel) { onCancel(); } }, [onCancel]); return ( View style{[styles.container, containerStyle]} View style{styles.inputWrapper} TextInput ref{inputRef} style{styles.input} value{currentValue} placeholder{placeholder} placeholderTextColor#999999 autoFocus{false} returnKeyTypesearch onChangeText{handleChangeText} onSubmitEditing{handleSubmit} selectionColor#1677FF maxLength{50} / {currentValue.length 0 ( Pressable style{styles.clearButton} onPress{handleClear} hitSlop{{ top: 10, bottom: 10, left: 10, right: 10 }} Text style{styles.clearIcon}×/Text /Pressable )} /View {showCancelButton ( Pressable style{styles.cancelButton} onPress{handleCancel} hitSlop{{ left: 10, right: 10 }} Text style{styles.cancelText}取消/Text /Pressable )} /View ); }; const styles StyleSheet.create({ container: { flexDirection: row, alignItems: center, paddingHorizontal: 12, paddingVertical: 8, backgroundColor: #F5F5F5, }, inputWrapper: { flex: 1, flexDirection: row, alignItems: center, backgroundColor: #FFFFFF, borderRadius: 20, paddingHorizontal: 12, height: 36, }, input: { flex: 1, fontSize: 14, color: #333333, padding: 0, textAlignVertical: center, }, clearButton: { marginLeft: 4, width: 18, height: 18, borderRadius: 9, backgroundColor: #CCCCCC, alignItems: center, justifyContent: center, }, clearIcon: { color: #FFFFFF, fontSize: 14, lineHeight: 16, backgroundColor: transparent, }, cancelButton: { marginLeft: 12, }, cancelText: { fontSize: 14, color: #1677FF, }, }); export default React.memo(SearchBar);几个细节再强调一下第一代码里 autoFocus 没有直接挂在 TextInput 上而是用 useEffect 延迟 300ms 手动调 focus()。这是我前面说的鸿蒙 autoFocus 时序问题的解决方式。延迟 300ms 是因为页面进场动画一般在这个时间内完成此时再聚焦成功率最高但不同项目动效时间不同这个值需要根据自己的页面做微调。第二input 里设置了 padding: 0 和 textAlignVertical: center。这两个样式是专门处理 Android 上输入文字偏上和默认内边距过大问题的鸿蒙适配层在这点上跟 Android 表现接近所以也需要处理不然输入文字会明显偏上视觉上非常难看。第三组件最外层用 React.memo 包了一层。SearchBar 在搜索页往往跟父组件同步频繁如果不做 memo父组件每次 setState 都会导致 SearchBar 重新渲染而 TextInput 的重渲染成本不低在低端设备上会明显感到输入卡顿。4.3 在页面中的接入方式组件写好了最终还是要接到业务页面里。这里给一个搜索页的接入示例展示受控模式怎么用// SearchPage.js import React, { useState, useCallback } from react; import { View, Text } from react-native; import SearchBar from ./SearchBar; const SearchPage () { const [keyword, setKeyword] useState(); const [searchResults, setSearchResults] useState([]); const [loading, setLoading] useState(false); const handleSearch useCallback((text) { if (!text.trim()) { setSearchResults([]); return; } setLoading(true); // 模拟接口请求实际项目中替换为真实请求 setTimeout(() { setSearchResults([ ${text} 相关结果1, ${text} 相关结果2, ]); setLoading(false); }, 500); }, []); const handleSelectHistory useCallback((historyWord) { setKeyword(historyWord); handleSearch(historyWord); }, [handleSearch]); return ( View style{{ flex: 1 }} SearchBar value{keyword} onChange{setKeyword} onSearch{handleSearch} onCancel{() { // 返回上一页逻辑由路由层决定 // navigation.goBack(); }} autoFocus showCancelButton debounceTime{400} / View style{{ flex: 1, paddingHorizontal: 16 }} Text搜索结果/Text {loading ? ( Text style{{ marginTop: 16 }}搜索中.../Text ) : ( searchResults.map((item) ( Text key{item} style{{ marginTop: 8 }} {item} /Text )) )} /View /View ); }; export default SearchPage;接入的时候有几点需要注意关键字确实是受控模式把 keyword 这个 state 传给 SearchBar 的 valueonChange 里 setKeyword。这样当用户点击历史搜索词时handleSelectHistory 里 setKeyword(historyWord)搜索框里的文字就会同步更新同时调 handleSearch 发起搜索。防抖时间 debounceTime 要根据自己的接口响应速度调。接口快300ms 和 400ms 差别不大接口慢建议放到 500ms 以上避免用户输入过程中前一次的搜索结果还没回来后一次的又发出去了。这里没有用 FlatList 是因为示例代码为了简洁实际项目里搜索结果列表肯定要用 FlatList 做长列表优化。5. 常见问题排查与避坑实录5.1 启动白屏的原因排查React Native 在 OpenHarmony 上启动白屏是我被问得最多的问题。排查思路跟 Android 上类似按链路从上往下推第一确认 Metro 有没有正常启动。开发模式下模拟器里的 App 启动后会去连 Metro 拉 JS bundle如果 Metro 没启动或者 IP 没配对页面就一直白着。检查方法是看 Metro 终端窗口有没有出现 bundling 的进度日志。没有日志就说明 App 没连上 Metro去检查 IP 地址配置。第二确认原生壳工程的 Entry ability 配置对不对。RN 的鸿蒙壳工程里要确保入口页面加载的是正确的 RN 实例ModuleName 需要跟 JS 侧的注册名一致。我当时遇到过一次白屏就是 ModuleName 写错bundle 加载完 JS 报了 module 找不到的错误。这个错误在 DevEco Studio 的 Log 面板里能看到。第三确认 bundle 加载路径。开发模式走 Metro发布模式走本地 asset。如果是发布包白屏检查 hap 包里有没有把 bundle 文件打进去路径跟代码里写的是否一致。第四注意启动画面的时序。如果启动画面结束后立即加载 RN 页面而 JS bundle 还在解析中这中间会有很短的空白期。可以适当延迟启动画面的结束回调或者用闪屏遮挡 loading 过程。这个在鸿蒙壳工程里是做得到的找一下启动页配置。我把这些问题整理成了速查表方便你排查的时候直接对照现象可能原因排查方向启动白屏Metro 有报错ModuleName 不匹配检查原生壳工程的 Entry ability启动白屏Metro 无日志模拟器连不上 Metro检查局域网 IP 配置启动白屏发布包出现bundle 没打包进 hap检查 asset 路径与文件启动画面结束后短暂白屏bundle 解析时序延长启动页或加闪屏5.2 画面渲染异常与文本显示问题在 OpenHarmony 上做 RN 开发画面渲染异常这个关键词的热度一直不低。我在实际开发中确实也遇到过一些渲染层面的怪问题主要集中在两类一类是样式属性不生效。RN 里常用的 shadowColor、shadowOpacity、shadowRadius 这些阴影属性在鸿蒙适配层上的支持跟 iOS 不一样很多情况下需要改用 elevation 或者干脆用背景图和边框来模拟阴影。我封装 SearchBar 时没用阴影整体用底色加圆角渲染就稳定很多。如果你一定要阴影效果建议先在模拟器和真机上分别验证再决定用什么方案。另一类是文字渲染异常。鸿蒙的字体渲染引擎跟其他平台不完全一样部分图标字符比如清空按钮上的 ×在不同设备和分辨率上会有明显的基线偏移看起来像是字没放正。处理办法是给这些特殊字符的 Text 设置 lineHeight 和 backgroundColor: transparent必要时可以用 Icon 组件替代纯文本符号稳定性更好。还有一类是 zIndex 层级问题。RN 在鸿蒙适配层上对于 position: absolute 和 zIndex 的组合支持有限偶尔会出现元素被错误覆盖的情况。SearchBar 本身不涉及复杂的层级叠加但如果遇到弹层、下拉联想词出现在 SearchBar 下面导致点不到的情况优先检查父容器有没有设置 overflow: hidden这个属性在鸿蒙适配层上会直接裁掉溢出部分是层级问题的一个常见根源。5.3 x86 模拟器上的兼容性问题OpenHarmony 支持 x86 架构的模拟器这对开发调试确实是件好事但 RN 的鸿蒙环境在 x86 模拟器上的表现和 ARM 真机有不小差别。最典型的是部分原生依赖库只提供了 ARM 架构的 so 文件在 x86 模拟器上运行时会直接加载失败表现就是启动崩溃或者页面白屏。排查方法是看 DevEco Studio 的 Log 面板如果有 so 文件 not found 或者 dlopen failed 之类的日志基本就是架构不匹配的问题。目前几个常见的方案一是检查依赖库的构建产物是否支持 x86_64不支持的换版本或者找替代库二是在模拟器上关闭一些依赖原生能力的模块后单独调试三是直接换 ARM 架构的真机验证核心功能模拟器只做 UI 层面的快速调试。我在 SearchBar 的开发过程中没有用到额外的原生模块所以 x86 模拟器上整体是能跑的。但如果你在模拟器上遇到 TextInput 点击无反应之类的问题不要先怀疑组件代码先确认一下是不是模拟器本身对输入法框架支持不完整导致的。这个坑容易误导人我当时就花了一个多小时在排查自己的事件处理代码最后发现是模拟器的问题换真机一切正常。5.4 软键盘遮挡与焦点管理SearchBar 必然涉及软键盘OpenHarmony 上关于键盘的坑不少其中最值得说的是两点。第一是软键盘遮住输入框或下拉联想区域。RN 的 KeyboardAvoidingView 在鸿蒙上的表现不稳定我实测在部分版本上 behaviorpadding 会多算高度反而把内容顶得太高。更稳的做法是在原生壳工程的 module.json5 里配置 windowSoftInputMode根据页面布局选 adjustResize 或 adjustPan然后在 RN 侧不依赖 KeyboardAvoidingView直接用 Keyboard 事件监听来动态调整底部间距。这个方案需要和原生同事配合改一下配置但一劳永逸。第二是焦点管理的节奏。前面提到 autoFocus 在鸿蒙上的时序问题其实 blur 的时机也要注意。比如用户点取消按钮时如果先执行 navigation.goBack() 再 blur页面已经出栈此时软键盘可能还残留半秒甚至更久。正确顺序是先在返回上一页的操作之前执行 inputRef.current?.blur()给输入法一个回收键盘的时间然后再做页面切换。还有一个小技巧如果搜索框在页面切换时键盘没有正常收回可以在页面离开前手动调用 Keyboard.dismiss() 兜底。这属于全路面防御治标不治本但用户体验不会出大问题。我把这些处理都加到了组件代码里实测下来键盘的弹出和收起都正常了。最后补充一点排坑心得SearchBar 这个组件本身不复杂但在 OpenHarmony 的 RN 环境里它把输入、事件、样式、原生交互这些问题全部暴露了一遍。我的体会是碰到这种哪儿都能用、哪都容易出问题的组件一定要先把交互链路想清楚再动手写代码写完组件之后优先在这个平台上把所有触发路径都走一遍不要拿 Android 或 iOS 的行为默认鸿蒙也一样。我踩过最深的一个坑就是一开始把 Android 上的键盘交互习惯直接搬过来结果鸿蒙上键盘弹出的高度和时机都不一样导致搜索联想区域被挡住最后跟原生同事配合改完 windowSoftInputMode 才算真正解决。从那以后我在鸿蒙上做任何跟焦点、键盘、手势相关的功能都会先查一遍适配层的已知问题列表再开始写业务代码。最后分享一个排坑的原则在鸿蒙上遇到 RN 行为异常时先分辨这是 RN 本身的问题还是 OpenHarmony 适配层的问题还是模拟器和真机差异的问题。三个层面需要不同的排查思路和工具先把问题归类再决定从哪儿下手。这条原则在 SearchBar 的开发中帮我省了非常多时间后续做其他组件也同样适用。
返回列表