案例开源地址:https://atomgit.com/nutpi/rn_openharmony_buy

写在前面

请添加图片描述

上一篇写了搜索页面,用户输入关键词后会跳转到搜索结果页。这篇就来实现搜索结果页,把匹配的商品展示出来。

搜索结果页的逻辑不复杂:拿到关键词,去商品列表里筛选,把匹配的商品显示出来。但有几个细节要处理好:加载状态、空结果、搜索匹配规则。

说实话,真正的搜索功能应该由后端来做,前端只负责展示。但我们用的 FakeStoreAPI 没有搜索接口,只能在前端做模糊匹配。虽然不够专业,但对于学习来说够用了。

引入依赖

import React, {useEffect, useState} from 'react';
import {View, Text, FlatList, StyleSheet, ActivityIndicator} from 'react-native';
import {Product} from '../types';
import {storeApi} from '../api/store';
import {ProductCard} from '../components/ProductCard';
import {useApp} from '../store/AppContext';
import {Header} from '../components/Header';
import {Empty} from '../components/Empty';

引入的东西不少。FlatList 用来渲染商品列表,ActivityIndicator 是加载动画,Empty 是空状态组件。

组件主体

export const SearchResultScreen = () => {
  const {screenParams, navigate} = useApp();
  const keyword = screenParams?.keyword || '';
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState(true);

screenParams 里拿到搜索关键词,这是上一个页面传过来的。如果没传就用空字符串兜底。

两个状态:products 存搜索结果,loading 控制加载动画。

为什么 loading 初始值是 true?

因为页面一打开就要去搜索,这时候应该显示加载动画。如果初始值是 false,页面会先闪一下空状态,然后才显示加载动画,体验不好。

触发搜索

useEffect(() => {
  searchProducts();
}, [keyword]);

useEffect 监听 keyword 的变化,关键词变了就重新搜索。

实际上这个页面的 keyword 不会变(除非用户返回搜索页重新搜索再进来),所以这个 effect 基本上就是在组件挂载时执行一次。

依赖数组里要不要放 keyword?

严格来说应该放。虽然当前场景 keyword 不会变,但如果以后需求变了(比如在结果页直接修改关键词重新搜索),不放 keyword 就会出 bug。养成好习惯,用到的变量都放进去。

搜索逻辑

const searchProducts = async () => {
  setLoading(true);
  try {
    const all = await storeApi.getProducts();

先把 loading 设为 true 显示加载动画,然后请求所有商品数据。

因为 FakeStoreAPI 没有搜索接口,我们只能先拿到全部商品,再在前端筛选。这种做法在商品数量少的时候没问题,商品多了就不行了,每次搜索都要下载全部数据,太浪费流量。

    const filtered = all.filter((p: Product) =>
      p.title.toLowerCase().includes(keyword.toLowerCase()) ||
      p.category.toLowerCase().includes(keyword.toLowerCase())
    );
    setProducts(filtered);

筛选逻辑:商品标题或分类名包含关键词就算匹配。

toLowerCase() 把字符串转成小写,这样搜索就不区分大小写了。用户搜 “PHONE” 和 “phone” 能得到一样的结果。

includes vs indexOf

includes 返回布尔值,indexOf 返回位置(找不到返回 -1)。这里只需要知道"有没有",不需要知道"在哪里",用 includes 更语义化。

  } catch (error) {
    console.error('Search failed:', error);
  } finally {
    setLoading(false);
  }
};

错误处理和关闭加载动画。finally 保证不管成功还是失败,loading 都会被设为 false。

页面渲染

return (
  <View style={styles.container}>
    <Header title={`搜索: ${keyword}`} />

头部显示搜索关键词,让用户知道当前搜的是什么。Header 组件自带返回按钮,点击可以回到搜索页。

三种状态的处理

搜索结果页有三种状态:加载中、无结果、有结果。用条件渲染来处理:

    {loading ? (
      <ActivityIndicator size="large" color="#3498db" style={styles.loader} />
    ) : products.length === 0 ? (
      <Empty icon="🔍" title="未找到相关商品" subtitle="换个关键词试试吧" />
    ) : (

加载中显示菊花图,搜索完没结果显示空状态组件。

嵌套三元表达式

这种写法有人觉得难读,但我觉得还好,逻辑很清晰:先判断 loading,再判断有没有结果。如果嵌套更多层就该拆成函数了,两层还能接受。

空状态组件给了友好的提示:“未找到相关商品”,还有个副标题"换个关键词试试吧",引导用户下一步操作。

商品列表

      <FlatList
        data={products}
        numColumns={2}
        keyExtractor={item => item.id.toString()}
        contentContainerStyle={styles.list}
        columnWrapperStyle={styles.row}
        renderItem={({item}) => (
          <ProductCard product={item} onPress={() => navigate('productDetail', {product: item})} />
        )}
      />
    )}
  </View>
);
};

有结果时用 FlatList 渲染商品列表。

几个关键属性:

  • numColumns={2}: 两列布局,和首页、分类页保持一致
  • keyExtractor: 用商品 ID 作为 key
  • columnWrapperStyle: 控制每行的样式,让两列商品左右分开
  • renderItem: 渲染每个商品,用 ProductCard 组件

点击商品跳转到详情页,把商品对象传过去。

FlatList vs ScrollView + map

搜索结果可能有很多商品,用 FlatListScrollView + map 性能好。FlatList 只渲染屏幕上可见的项目,滑动时动态加载,内存占用小。

样式

const styles = StyleSheet.create({
  container: {flex: 1, backgroundColor: '#f5f5f5'},
  loader: {flex: 1, justifyContent: 'center'},
  list: {padding: 16},
  row: {justifyContent: 'space-between'},
});

样式很简单。loaderflex: 1 让加载动画垂直居中。rowjustifyContent: 'space-between' 让每行的两个商品左右分开。

搜索匹配的优化思路

当前的搜索逻辑比较简单,只是看标题和分类是否包含关键词。实际项目中可以做得更智能:

1. 分词搜索

用户搜"红色连衣裙",可以拆成"红色"和"连衣裙"两个词,只要商品包含其中一个词就算匹配。这样搜索结果会更多。

2. 拼音搜索

用户输入拼音"shouji",也能搜到"手机"。需要引入拼音转换库。

3. 搜索权重

标题匹配的权重高于分类匹配,完全匹配的权重高于部分匹配。根据权重排序,最相关的商品排在前面。

4. 搜索高亮

在搜索结果中,把匹配的关键词高亮显示。比如搜"手机",商品标题里的"手机"两个字变成红色。

这些优化要不要做?

看需求。如果是学习项目,当前的实现够用了。如果是正式产品,这些优化能显著提升搜索体验。不过最好的方案还是让后端来做搜索,后端可以用 Elasticsearch 这类专业的搜索引擎,效果比前端筛选好得多。

加载更多

当前实现是一次性加载所有结果。如果结果很多,可以做分页加载:

<FlatList
  // ...其他属性
  onEndReached={loadMore}
  onEndReachedThreshold={0.5}
  ListFooterComponent={hasMore ? <ActivityIndicator /> : null}
/>

onEndReached 在滑动到底部时触发,可以在这里加载下一页数据。onEndReachedThreshold 控制触发的时机,0.5 表示距离底部还有 50% 时就触发。

不过因为我们是前端筛选,数据已经全部拿到了,分页加载意义不大。如果后端支持分页搜索接口,这个功能就很有用了。

排序功能

搜索结果页通常还有排序功能:按价格排序、按销量排序、按评分排序。

实现思路是在页面顶部加一排排序按钮,点击后对 products 数组重新排序:

const sortByPrice = (asc: boolean) => {
  const sorted = [...products].sort((a, b) => asc ? a.price - b.price : b.price - a.price);
  setProducts(sorted);
};

注意要用 [...products] 创建新数组再排序,不能直接 products.sort(),因为 React 状态是不可变的,直接修改不会触发重新渲染。

写在最后

搜索结果页的核心就是:拿关键词、筛选商品、展示结果。三种状态(加载中、无结果、有结果)都要处理好,给用户清晰的反馈。

前端搜索只是权宜之计,正式项目应该用后端搜索。后端可以做更复杂的匹配逻辑、支持分页、返回搜索建议,前端只负责展示就行。

下一篇写收藏功能,包括收藏列表页面。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐