RN for OpenHarmony英雄联盟助手App实战:英雄搜索实现

案例开源地址:https://atomgit.com/nutpi/rn_openharmony_lol
筛选功能适合按条件缩小范围,但如果用户已经知道想找哪个英雄,直接输入名字搜索会更快。英雄搜索页提供一个输入框,用户输入关键词后实时显示匹配的英雄。
这篇文章我们来实现英雄搜索功能,重点是搜索框组件的封装、防抖优化、以及模糊匹配的实现。
搜索的用户体验
好的搜索体验应该是这样的:
- 用户输入时实时显示结果,不需要点"搜索"按钮
- 输入太快时不要每个字符都触发搜索,会卡顿
- 支持模糊匹配,输入"盖"能找到"盖伦",输入"德玛"也能找到(因为称号是"德玛西亚之力")
- 没有结果时给出友好提示
带着这些目标,我们来实现搜索功能。
页面结构
import React, {useState, useMemo} from 'react';
import {View, StyleSheet} from 'react-native';
import {colors} from '../../styles/colors';
import {useApp} from '../../context/AppContext';
import {useNavigation} from '../../context/NavigationContext';
import {championApi} from '../../api';
import {ChampionGrid} from '../../components/champion';
import {SearchBar} from '../../components/common';
import {useDebounce} from '../../hooks';
import type {Champion} from '../../models/Champion';
这个页面用到了几个关键的模块:
SearchBar:搜索框组件,我们自己封装的useDebounce:防抖 Hook,用于优化搜索性能ChampionGrid:英雄网格组件,复用之前实现的
export function ChampionSearchPage() {
const {state} = useApp();
const {navigate} = useNavigation();
const [searchText, setSearchText] = useState('');
const debouncedSearch = useDebounce(searchText, 300);
searchText 是用户输入的原始文本,每次按键都会更新。debouncedSearch 是防抖后的文本,用户停止输入 300ms 后才会更新。
为什么需要防抖?假设用户输入"盖伦"两个字,会触发两次 onChange:输入"盖"触发一次,输入"伦"触发一次。如果每次都执行搜索,会有两次搜索操作。用户输入快的话,可能输入"德玛西亚"会触发五六次搜索,造成不必要的性能开销。
防抖的作用是:用户停止输入一段时间后才执行搜索。300ms 是一个经验值,既不会让用户感觉到延迟,又能有效减少搜索次数。
搜索逻辑
// 搜索英雄
const searchResults = useMemo(() => {
if (!debouncedSearch.trim()) {
return [];
}
return championApi.searchChampions(state.champions, debouncedSearch);
}, [state.champions, debouncedSearch]);
搜索逻辑用 useMemo 包裹,只有当 debouncedSearch 变化时才重新计算。
如果搜索词为空(或只有空格),直接返回空数组。这样用户刚进入页面时不会显示所有英雄,而是显示空状态,提示用户输入关键词。
championApi.searchChampions 是我们封装的搜索函数:
// api/champion.ts
export function searchChampions(champions: Champion[], keyword: string): Champion[] {
const lowerKeyword = keyword.toLowerCase();
return champions.filter(c =>
c.name.toLowerCase().includes(lowerKeyword) ||
c.title.toLowerCase().includes(lowerKeyword) ||
c.id.toLowerCase().includes(lowerKeyword)
);
}
搜索会匹配三个字段:name(中文名)、title(称号)、id(英文名)。全部转成小写后比较,实现大小写不敏感的搜索。
比如搜索"garen"能找到盖伦(id 是 Garen),搜索"德玛"能找到盖伦(称号是"德玛西亚之力")。
页面渲染
const handleChampionPress = (champion: Champion) => {
navigate('ChampionDetail', {championId: champion.id});
};
const SearchHeader = (
<View style={styles.searchContainer}>
<SearchBar
value={searchText}
onChangeText={setSearchText}
placeholder="输入英雄名称、称号搜索..."
/>
</View>
);
return (
<View style={styles.container}>
<ChampionGrid
champions={searchResults}
version={state.version}
onChampionPress={handleChampionPress}
ListHeaderComponent={SearchHeader}
/>
</View>
);
}
搜索框放在 ListHeaderComponent 中,会随着列表一起滚动。如果搜索结果很多,用户滚动到下面后搜索框会滚出屏幕。
如果你希望搜索框固定在顶部,可以把它放在 ChampionGrid 外面。两种方案各有优劣,取决于产品需求。
搜索框组件的实现
SearchBar 是一个通用的搜索框组件,可以在多个页面复用。
import React from 'react';
import {View, TextInput, TouchableOpacity, Text, StyleSheet} from 'react-native';
import {colors} from '../../styles/colors';
interface SearchBarProps {
value: string;
onChangeText: (text: string) => void;
placeholder?: string;
onClear?: () => void;
}
Props 设计遵循受控组件的模式:value 和 onChangeText 由父组件控制。这样父组件可以完全掌控输入框的状态,便于实现防抖等逻辑。
export function SearchBar({
value,
onChangeText,
placeholder = '搜索...',
onClear,
}: SearchBarProps) {
const handleClear = () => {
onChangeText('');
onClear?.();
};
return (
<View style={styles.container}>
<Text style={styles.icon}>🔍</Text>
<TextInput
style={styles.input}
value={value}
onChangeText={onChangeText}
placeholder={placeholder}
placeholderTextColor={colors.textMuted}
returnKeyType="search"
/>
{value.length > 0 && (
<TouchableOpacity onPress={handleClear} style={styles.clearButton}>
<Text style={styles.clearText}>✕</Text>
</TouchableOpacity>
)}
</View>
);
}
搜索框由三部分组成:左边的搜索图标、中间的输入框、右边的清除按钮。
清除按钮只在有输入内容时显示。点击后清空输入框,并调用 onClear 回调(如果有的话)。onClear?.() 是可选链调用,如果 onClear 是 undefined 就不会执行。
returnKeyType="search" 让键盘的回车键显示为"搜索",虽然我们没有处理回车事件(因为是实时搜索),但这个视觉提示能让用户知道这是一个搜索框。
搜索框样式
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.backgroundCard,
borderRadius: 8,
paddingHorizontal: 12,
height: 44,
borderWidth: 1,
borderColor: colors.border,
},
icon: {
fontSize: 16,
marginRight: 8,
},
input: {
flex: 1,
fontSize: 16,
color: colors.textPrimary,
padding: 0,
},
clearButton: {
padding: 4,
},
clearText: {
fontSize: 14,
color: colors.textMuted,
},
});
高度固定为 44px,这是 iOS 推荐的最小可点击区域高度。圆角 8px 让搜索框看起来比较柔和。
输入框的 padding: 0 是为了去掉 TextInput 的默认内边距,让文字和图标对齐。
防抖 Hook 的实现
useDebounce 是一个通用的防抖 Hook,可以用于任何需要防抖的场景。
import {useState, useEffect} from 'react';
export function useDebounce<T>(value: T, delay: number = 300): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
这个 Hook 的原理是:
- 当 value 变化时,设置一个定时器,delay 毫秒后更新 debouncedValue
- 如果在定时器触发前 value 又变化了,清除旧的定时器,设置新的定时器
- 只有当 value 稳定 delay 毫秒后,debouncedValue 才会更新
useEffect 的返回函数是清理函数,会在下一次 effect 执行前调用。这里用它来清除旧的定时器,避免内存泄漏。
泛型 <T> 让这个 Hook 可以用于任何类型的值,不仅仅是字符串。比如你也可以用它来防抖一个对象或数组。
搜索优化建议
当前实现已经能满足基本需求,但还有一些可以优化的地方。
搜索历史:记住用户最近搜索过的关键词,下次打开搜索页时显示出来。可以用 AsyncStorage 存储:
// 保存搜索历史
const saveHistory = async (keyword: string) => {
const history = await AsyncStorage.getItem('searchHistory');
const list = history ? JSON.parse(history) : [];
const newList = [keyword, ...list.filter(k => k !== keyword)].slice(0, 10);
await AsyncStorage.setItem('searchHistory', JSON.stringify(newList));
};
热门搜索:在搜索框下方显示热门英雄,方便用户快速选择。可以根据英雄的热度数据(如果有的话)或者固定显示几个常用英雄。
拼音搜索:支持用拼音搜索中文名,比如输入"gailun"能找到"盖伦"。这需要引入拼音转换库,会增加包体积。
高亮匹配:在搜索结果中高亮显示匹配的关键词,让用户知道为什么这个英雄被搜出来。实现起来需要把名称拆分成匹配部分和非匹配部分,分别渲染。
页面样式
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
searchContainer: {
marginBottom: 16,
},
});
页面样式很简单,因为主要的 UI 都在 SearchBar 和 ChampionGrid 组件中。searchContainer 的 marginBottom 让搜索框和下面的结果列表之间有一点间距。
空状态处理
当前实现中,如果搜索没有结果,ChampionGrid 会显示一个空列表。可以加一个更友好的空状态提示:
// 在 ChampionGrid 组件中
ListEmptyComponent={
searchText ? (
<View style={styles.emptyState}>
<Text style={styles.emptyText}>没有找到"{searchText}"相关的英雄</Text>
<Text style={styles.emptyHint}>试试其他关键词?</Text>
</View>
) : (
<View style={styles.emptyState}>
<Text style={styles.emptyText}>输入关键词开始搜索</Text>
</View>
)
}
区分两种空状态:没有输入时提示"输入关键词开始搜索",有输入但没结果时提示"没有找到相关英雄"。
小结
英雄搜索页涉及到几个实用的技术点:
- 受控组件:搜索框的值由父组件控制,便于实现防抖等逻辑
- 防抖优化:用 useDebounce Hook 减少不必要的搜索操作
- 模糊匹配:搜索多个字段,提高搜索的命中率
- 组件复用:SearchBar 和 ChampionGrid 都是可复用的组件
下一篇我们来实现装备大全页面,展示游戏中所有装备的列表。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)