【HarmonyOS】RN of HarmonyOS实战项目:TextInput自动完成功能实现

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
项目概述:本文详细介绍在HarmonyOS平台上使用React Native实现智能自动完成功能,涵盖本地数据源、远程API、模糊搜索、缓存策略等完整技术方案,提供从基础实现到生产级应用的完整解决方案。
一、项目背景
自动完成(Autocomplete)是提升用户体验的重要功能,广泛应用于搜索、表单填写、地址选择等场景。在HarmonyOS平台上实现高效的自动完成需要考虑:
- 数据源管理:本地数据与远程API的混合使用
- 搜索算法:模糊匹配、拼音搜索、智能排序
- 性能优化:防抖处理、结果缓存、虚拟列表
- 用户体验:加载状态、空状态、高亮显示
二、技术架构
2.1 自动完成流程
命中
未命中
本地数据
远程API
成功
失败
用户输入
防抖处理
检查缓存
显示缓存结果
检查数据源
本地搜索
发起请求
显示结果
响应
更新缓存
显示错误
2.2 数据结构定义
typescript
/**
* 自动完成项
*/
interface AutocompleteItem {
id: string;
title: string;
subtitle?: string;
icon?: string;
image?: string;
score?: number; // 匹配分数
highlight?: string[]; // 高亮字段
}
/**
* 自动完成配置
*/
interface AutocompleteConfig {
debounceMs?: number; // 防抖延迟(毫秒)
minChars?: number; // 最小触发字符数
maxResults?: number; // 最大结果数
cacheEnabled?: boolean; // 是否启用缓存
cacheTTL?: number; // 缓存有效期(毫秒)
highlightEnabled?: boolean; // 是否高亮匹配
}
三、核心实现代码
3.1 模糊搜索工具类
typescript
/**
* 模糊搜索工具类
* 提供高效的字符串匹配和评分算法
*
* @platform HarmonyOS 2.0+
* @react-native 0.72+
*/
export class FuzzySearch {
/**
* 模糊匹配计算分数
* 返回0-1之间的分数,1表示完全匹配
*/
static score(query: string, target: string): number {
if (!query) return 0;
if (!target) return 0;
const q = query.toLowerCase();
const t = target.toLowerCase();
// 完全匹配
if (t === q) return 1;
// 前缀匹配
if (t.startsWith(q)) return 0.8;
// 包含匹配
if (t.includes(q)) return 0.6;
// 模糊匹配(字符顺序)
let score = 0;
let qIndex = 0;
let consecutive = 0;
for (let i = 0; i < t.length && qIndex < q.length; i++) {
if (t[i] === q[qIndex]) {
qIndex++;
consecutive++;
score += consecutive * 0.1;
} else {
consecutive = 0;
}
}
// 如果所有字符都匹配上了
if (qIndex === q.length) {
// 归一化分数
return Math.min(score / q.length, 0.5);
}
return 0;
}
/**
* 高亮匹配文本
*/
static highlight(query: string, target: string): {
text: string;
isMatch: boolean;
}[] {
const result: { text: string; isMatch: boolean }[] = [];
const q = query.toLowerCase();
const t = target.toLowerCase();
let lastIndex = 0;
let qIndex = 0;
for (let i = 0; i < t.length && qIndex < q.length; i++) {
if (t[i] === q[qIndex]) {
// 添加匹配前的文本
if (i > lastIndex) {
result.push({
text: target.substring(lastIndex, i),
isMatch: false,
});
}
// 添加匹配的文本
result.push({
text: target.substring(i, i + 1),
isMatch: true,
});
lastIndex = i + 1;
qIndex++;
}
}
// 添加剩余文本
if (lastIndex < target.length) {
result.push({
text: target.substring(lastIndex),
isMatch: false,
});
}
return result;
}
/**
* 搜索并排序
*/
static search<T extends AutocompleteItem>(
items: T[],
query: string,
options: {
threshold?: number;
maxResults?: number;
scoreField?: string;
} = {}
): T[] {
const { threshold = 0.1, maxResults = 10, scoreField = 'title' } = options;
if (!query) {
return items.slice(0, maxResults);
}
// 计算分数
const scored = items.map(item => {
const titleScore = this.score(query, item.title);
const subtitleScore = item.subtitle
? this.score(query, item.subtitle)
: 0;
return {
...item,
score: Math.max(titleScore, subtitleScore),
};
});
// 过滤并排序
return scored
.filter(item => item.score! >= threshold)
.sort((a, b) => (b.score || 0) - (a.score || 0))
.slice(0, maxResults);
}
}
3.2 缓存管理器
typescript
/**
* 自动完成缓存管理器
* 管理搜索结果的缓存
*/
interface CacheEntry {
results: AutocompleteItem[];
timestamp: number;
}
export class AutocompleteCache {
private cache: Map<string, CacheEntry> = new Map();
private ttl: number;
constructor(ttl: number = 5 * 60 * 1000) {
this.ttl = ttl;
}
/**
* 获取缓存
*/
get(query: string): AutocompleteItem[] | null {
const entry = this.cache.get(query);
if (!entry) {
return null;
}
// 检查是否过期
const now = Date.now();
if (now - entry.timestamp > this.ttl) {
this.cache.delete(query);
return null;
}
return entry.results;
}
/**
* 设置缓存
*/
set(query: string, results: AutocompleteItem[]): void {
this.cache.set(query, {
results,
timestamp: Date.now(),
});
}
/**
* 清空缓存
*/
clear(): void {
this.cache.clear();
}
/**
* 删除特定查询的缓存
*/
delete(query: string): void {
this.cache.delete(query);
}
/**
* 清理过期缓存
*/
cleanup(): void {
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > this.ttl) {
this.cache.delete(key);
}
}
}
}
3.3 自动完成Hook
typescript
/**
* 自动完成Hook
* 提供完整的自动完成功能
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import { FuzzySearch } from './FuzzySearch';
import { AutocompleteCache } from './AutocompleteCache';
interface UseAutocompleteOptions {
data?: AutocompleteItem[];
fetchData?: (query: string) => Promise<AutocompleteItem[]>;
config?: AutocompleteConfig;
}
export function useAutocomplete(options: UseAutocompleteOptions) {
const {
data: localData,
fetchData,
config = {},
} = options;
const {
debounceMs = 300,
minChars = 1,
maxResults = 10,
cacheEnabled = true,
cacheTTL = 5 * 60 * 1000,
highlightEnabled = true,
} = config;
const [query, setQuery] = useState('');
const [results, setResults] = useState<AutocompleteItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const debounceTimer = useRef<NodeJS.Timeout>();
const cache = useRef(new AutocompleteCache(cacheTTL));
const abortController = useRef<AbortController | null>(null);
/**
* 执行搜索
*/
const search = useCallback(async (searchQuery: string) => {
if (searchQuery.length < minChars) {
setResults([]);
setError(null);
return;
}
setIsLoading(true);
setError(null);
// 取消之前的请求
if (abortController.current) {
abortController.current.abort();
}
try {
// 检查缓存
if (cacheEnabled) {
const cached = cache.current.get(searchQuery);
if (cached) {
setResults(cached);
setIsLoading(false);
return;
}
}
let searchResults: AutocompleteItem[] = [];
// 本地数据搜索
if (localData) {
searchResults = FuzzySearch.search(localData, searchQuery, {
threshold: 0.1,
maxResults,
});
}
// 远程数据搜索
if (fetchData) {
abortController.current = new AbortController();
try {
const remoteResults = await fetchData(searchQuery);
searchResults = [...searchResults, ...remoteResults]
.sort((a, b) => (b.score || 0) - (a.score || 0))
.slice(0, maxResults);
} catch (err: any) {
if (err.name !== 'AbortError') {
throw err;
}
}
}
// 更新缓存
if (cacheEnabled && searchResults.length > 0) {
cache.current.set(searchQuery, searchResults);
}
setResults(searchResults);
} catch (err: any) {
setError(err.message || '搜索失败');
setResults([]);
} finally {
setIsLoading(false);
}
}, [localData, fetchData, minChars, maxResults, cacheEnabled, cacheTTL]);
/**
* 处理输入变化
*/
const handleQueryChange = useCallback((text: string) => {
setQuery(text);
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
debounceTimer.current = setTimeout(() => {
search(text);
}, debounceMs);
}, [debounceMs, search]);
/**
* 清空查询
*/
const clearQuery = useCallback(() => {
setQuery('');
setResults([]);
setError(null);
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
}, []);
/**
* 选择结果
*/
const selectItem = useCallback((item: AutocompleteItem) => {
setQuery(item.title);
setResults([]);
}, []);
/**
* 清理
*/
useEffect(() => {
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
if (abortController.current) {
abortController.current.abort();
}
};
}, []);
return {
query,
setQuery: handleQueryChange,
results,
isLoading,
error,
clearQuery,
selectItem,
};
}
3.4 自动完成组件
typescript
/**
* 自动完成输入组件
* 提供完整的自动完成UI
*/
import React, { useRef, useEffect } from 'react';
import {
View,
TextInput,
Text,
TouchableOpacity,
StyleSheet,
FlatList,
ActivityIndicator,
ScrollView,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import { useAutocomplete } from './useAutocomplete';
import { FuzzySearch } from './FuzzySearch';
interface AutocompleteInputProps {
placeholder?: string;
data?: AutocompleteItem[];
fetchData?: (query: string) => Promise<AutocompleteItem[]>;
config?: AutocompleteConfig;
onSelect?: (item: AutocompleteItem) => void;
renderItem?: (item: AutocompleteItem, highlight: boolean) => React.ReactNode;
}
export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
placeholder = '搜索...',
data,
fetchData,
config,
onSelect,
renderItem,
}) => {
const inputRef = useRef<TextInput>(null);
const listRef = useRef<FlatList>(null);
const {
query,
setQuery,
results,
isLoading,
error,
clearQuery,
selectItem,
} = useAutocomplete({
data,
fetchData,
config,
});
/**
* 渲染结果项
*/
const renderItemDefault = ({ item }: { item: AutocompleteItem }) => {
if (renderItem) {
return renderItem(item, true);
}
const highlighted = FuzzySearch.highlight(query, item.title);
return (
<TouchableOpacity
style={autoStyles.item}
onPress={() => {
selectItem(item);
onSelect?.(item);
}}
>
<View style={autoStyles.itemContent}>
{item.image && (
<View style={autoStyles.imageContainer}>
<Text style={autoStyles.imagePlaceholder}>{item.image}</Text>
</View>
)}
{item.icon && !item.image && (
<Text style={autoStyles.icon}>{item.icon}</Text>
)}
<View style={autoStyles.textContainer}>
<View style={autoStyles.titleRow}>
<FlatList
horizontal
data={highlighted}
keyExtractor={(item, index) => `${index}-${item.text}`}
renderItem={({ item: h }) => (
<Text
style={[
autoStyles.title,
h.isMatch && autoStyles.titleHighlight,
]}
>
{h.text}
</Text>
)}
/>
</View>
{item.subtitle && (
<Text style={autoStyles.subtitle} numberOfLines={1}>
{item.subtitle}
</Text>
)}
</View>
{item.score !== undefined && (
<Text style={autoStyles.score}>
{Math.round(item.score * 100)}%
</Text>
)}
</View>
</TouchableOpacity>
);
};
/**
* 渲染列表头部
*/
const renderListHeader = () => {
if (isLoading) {
return (
<View style={autoStyles.loadingContainer}>
<ActivityIndicator size="small" color="#007AFF" />
<Text style={autoStyles.loadingText}>搜索中...</Text>
</View>
);
}
if (error) {
return (
<View style={autoStyles.errorContainer}>
<Text style={autoStyles.errorText}>{error}</Text>
</View>
);
}
if (query.length > 0 && results.length === 0) {
return (
<View style={autoStyles.emptyContainer}>
<Text style={autoStyles.emptyText}>未找到结果</Text>
</View>
);
}
return null;
};
return (
<KeyboardAvoidingView
style={autoStyles.container}
keyboardVerticalOffset={Platform.OS === 'ios' ? 100 : 0}
behavior="padding"
>
{/* 输入框 */}
<View style={autoStyles.inputContainer}>
<Text style={autoStyles.searchIcon}>🔍</Text>
<TextInput
ref={inputRef}
style={autoStyles.input}
value={query}
onChangeText={setQuery}
placeholder={placeholder}
placeholderTextColor="#999"
returnKeyType="search"
/>
{query.length > 0 && (
<TouchableOpacity onPress={clearQuery} style={autoStyles.clearButton}>
<Text style={autoStyles.clearIcon}>×</Text>
</TouchableOpacity>
)}
</View>
{/* 结果列表 */}
{results.length > 0 && (
<View style={autoStyles.resultsContainer}>
<FlatList
ref={listRef}
data={results}
renderItem={renderItemDefault}
keyExtractor={(item) => item.id}
ListHeaderComponent={renderListHeader}
ItemSeparatorComponent={() => <View style={autoStyles.separator} />}
keyboardShouldPersistTaps="handled"
/>
</View>
)}
</KeyboardAvoidingView>
);
};
const autoStyles = StyleSheet.create({
container: {
width: '100%',
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F5F5F5',
borderRadius: 12,
paddingHorizontal: 16,
height: 48,
},
searchIcon: {
fontSize: 16,
marginRight: 10,
},
input: {
flex: 1,
fontSize: 15,
color: '#333',
},
clearButton: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: '#E0E0E0',
justifyContent: 'center',
alignItems: 'center',
},
clearIcon: {
fontSize: 18,
color: '#666',
fontWeight: 'bold',
},
resultsContainer: {
marginTop: 12,
backgroundColor: '#fff',
borderRadius: 12,
maxHeight: 300,
overflow: 'hidden',
borderWidth: 1,
borderColor: '#E5E5E5',
},
item: {
paddingHorizontal: 16,
paddingVertical: 12,
},
itemContent: {
flexDirection: 'row',
alignItems: 'center',
},
imageContainer: {
width: 40,
height: 40,
borderRadius: 8,
backgroundColor: '#F5F5F5',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
imagePlaceholder: {
fontSize: 20,
},
icon: {
fontSize: 24,
marginRight: 12,
},
textContainer: {
flex: 1,
},
titleRow: {
flexDirection: 'row',
alignItems: 'center',
},
title: {
fontSize: 15,
color: '#333',
},
titleHighlight: {
color: '#007AFF',
fontWeight: '600',
},
subtitle: {
fontSize: 13,
color: '#999',
marginTop: 2,
},
score: {
fontSize: 12,
color: '#999',
marginLeft: 8,
},
separator: {
height: 1,
backgroundColor: '#F5F5F5',
marginLeft: 68,
},
loadingContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
padding: 16,
},
loadingText: {
marginLeft: 8,
fontSize: 14,
color: '#999',
},
errorContainer: {
padding: 16,
alignItems: 'center',
},
errorText: {
fontSize: 14,
color: '#FF3B30',
},
emptyContainer: {
padding: 24,
alignItems: 'center',
},
emptyText: {
fontSize: 14,
color: '#999',
},
});
3.5 使用示例
typescript
/**
* 自动完成组件使用示例
*/
import React from 'react';
import {
View,
StyleSheet,
ScrollView,
SafeAreaView,
Text,
} from 'react-native';
import { AutocompleteInput } from './AutocompleteInput';
import { AutocompleteItem } from './types';
// 模拟本地数据
const MOCK_DATA: AutocompleteItem[] = [
{ id: '1', title: 'React Native', subtitle: '跨平台移动应用开发框架', icon: '📱' },
{ id: '2', title: 'React', subtitle: '用于构建用户界面的JavaScript库', icon: '⚛️' },
{ id: '3', title: 'Redux', subtitle: 'JavaScript应用的可预测状态容器', icon: '🔄' },
{ id: '4', title: 'TypeScript', subtitle: 'JavaScript的超集', icon: '📘' },
{ id: '5', title: 'JavaScript', subtitle: 'Web编程语言', icon: '📜' },
{ id: '6', title: 'HarmonyOS', subtitle: '分布式操作系统', icon: '🌐' },
{ id: '7', title: 'OpenHarmony', subtitle: '开源鸿蒙系统', icon: '🔓' },
{ id: '8', title: 'ArkUI', subtitle: 'HarmonyOS声明式UI框架', icon: '🎨' },
];
// 模拟远程搜索API
const mockFetchData = async (query: string): Promise<AutocompleteItem[]> => {
await new Promise(resolve => setTimeout(resolve, 500));
return [
{ id: `remote-1`, title: `${query} 教程`, subtitle: '学习使用' },
{ id: `remote-2`, title: `${query} 文档`, subtitle: '官方文档' },
{ id: `remote-3`, title: `${query} 示例`, subtitle: '代码示例' },
];
};
const AutocompleteDemo: React.FC = () => {
const handleSelect = (item: AutocompleteItem) => {
console.log('Selected:', item);
};
return (
<SafeAreaView style={demoStyles.container}>
<ScrollView contentContainerStyle={demoStyles.content}>
<Text style={demoStyles.title}>自动完成功能</Text>
{/* 本地数据自动完成 */}
<View style={demoStyles.section}>
<Text style={demoStyles.sectionTitle}>本地数据搜索</Text>
<AutocompleteInput
placeholder="搜索技术栈..."
data={MOCK_DATA}
config={{
debounceMs: 200,
minChars: 1,
maxResults: 5,
}}
onSelect={handleSelect}
/>
</View>
{/* 远程数据自动完成 */}
<View style={demoStyles.section}>
<Text style={demoStyles.sectionTitle}>远程数据搜索</Text>
<AutocompleteInput
placeholder="搜索教程..."
fetchData={mockFetchData}
config={{
debounceMs: 500,
minChars: 2,
maxResults: 8,
cacheEnabled: true,
}}
onSelect={handleSelect}
/>
</View>
{/* 混合数据源 */}
<View style={demoStyles.section}>
<Text style={demoStyles.sectionTitle}>混合数据源</Text>
<AutocompleteInput
placeholder="搜索任何内容..."
data={MOCK_DATA}
fetchData={mockFetchData}
config={{
debounceMs: 300,
minChars: 1,
maxResults: 10,
}}
onSelect={handleSelect}
/>
</View>
</ScrollView>
</SafeAreaView>
);
};
const demoStyles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
content: {
padding: 16,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
marginBottom: 30,
textAlign: 'center',
},
section: {
marginBottom: 30,
},
sectionTitle: {
fontSize: 14,
color: '#999',
marginBottom: 8,
},
});
export default AutocompleteDemo;
四、最佳实践
| 功能 | 实现方式 | 效果 |
|---|---|---|
| 模糊搜索 | 字符匹配评分算法 | 更智能的搜索结果 |
| 缓存策略 | TTL缓存管理 | 减少重复请求 |
| 防抖处理 | 300ms延迟输入 | 优化性能 |
| 高亮显示 | 匹配字符高亮 | 清晰的视觉反馈 |
| 虚拟列表 | FlatList渲染 | 高效的长列表显示 |
五、总结
本文详细介绍了在HarmonyOS平台上实现自动完成功能的完整方案,涵盖:
- 核心算法:模糊搜索、匹配评分
- 数据管理:本地/远程数据源、缓存策略
- 性能优化:防抖处理、结果缓存
- 用户体验:高亮显示、加载状态、空状态处理
完整项目代码 :AtomGitDemos