请添加图片描述

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

换个角度看品种

品种列表页是按字母顺序展示所有品种,但用户可能想按类型来浏览。比如想看工作犬有哪些、运动犬有哪些。品种分组页就是为这个需求设计的。

这个页面的核心是数据分组。API 返回的是扁平的品种数组,我们需要按 breed_group 字段把它们分成若干组。这是个很常见的数据处理场景,值得好好聊聊。

API 返回的数据结构

先回顾一下品种数据的结构:

export interface Breed {
  id: number;
  name: string;
  bred_for?: string;
  breed_group?: string;
  life_span: string;
  temperament?: string;
  origin?: string;
  weight: { metric: string };
  height: { metric: string };
  reference_image_id?: string;
  image?: { url: string };
}

关键字段是 breed_group,表示品种所属的分组,比如:

  • Working:工作犬,如哈士奇、萨摩耶
  • Sporting:运动犬,如金毛、拉布拉多
  • Herding:牧羊犬,如边牧、德牧
  • Toy:玩具犬,如吉娃娃、博美

注意这个字段是可选的(带 ?),有些品种没有分组信息。

组件引入

import React, {useState, useEffect} from 'react';
import {View, Text, ScrollView, TouchableOpacity, StyleSheet} from 'react-native';
import {Header, Loading} from '../../components';
import {api} from '../../api';
import type {Breed} from '../../types';

这个页面比较简单,没有用到 useNavigation,因为当前只展示分组列表,点击暂时没有跳转。

状态定义

export function BreedGroupsPage() {
  const [loading, setLoading] = useState(true);
  const [groups, setGroups] = useState<Record<string, Breed[]>>({});

两个状态:

  • loading:加载状态
  • groups:分组后的数据

groups 的类型是 Record<string, Breed[]>,这是 TypeScript 的工具类型,表示一个对象,key 是字符串,value 是品种数组。

比如:

{
  "Working": [哈士奇, 萨摩耶, ...],
  "Sporting": [金毛, 拉布拉多, ...],
  "其他": [没有分组的品种, ...]
}

数据加载和分组

  useEffect(() => {
    api.getBreeds().then(res => {
      const g: Record<string, Breed[]> = {};
      res.forEach(b => {
        const key = b.breed_group || '其他';
        if (!g[key]) g[key] = [];
        g[key].push(b);
      });
      setGroups(g);
      setLoading(false);
    });
  }, []);

这段代码是核心,逐步分析:

调用 APIapi.getBreeds() 获取所有品种,返回 Promise。

初始化空对象const g: Record<string, Breed[]> = {} 用来存储分组结果。

遍历品种数组res.forEach(b => {...}) 对每个品种进行处理。

确定分组 keyconst key = b.breed_group || '其他',没有分组的归入"其他"。

初始化数组if (!g[key]) g[key] = [],如果这个分组还不存在,先创建空数组。

添加品种g[key].push(b) 把品种加入对应分组。

更新状态:遍历完成后,setGroups(g) 更新状态,setLoading(false) 结束加载。

分组算法的时间复杂度

上面的分组算法是 O(n),只需要遍历一次数组。

每个品种只处理一次,对象的属性访问和数组 push 都是 O(1) 操作。

如果用 filter 来分组,比如:

// 不推荐的写法
const working = breeds.filter(b => b.breed_group === 'Working');
const sporting = breeds.filter(b => b.breed_group === 'Sporting');
// ...

每个分组都要遍历一次数组,时间复杂度是 O(n * m),m 是分组数量。数据量大时差别明显。

加载状态的处理

  if (loading) return (
    <View style={s.container}>
      <Header title="品种分组" />
      <Loading full />
    </View>
  );

这里用了提前返回的写法。如果还在加载,直接返回 Loading 界面,不执行后面的渲染逻辑。

这种写法比在 JSX 里写三元表达式更清晰,特别是当加载状态的 UI 比较复杂时。

分组列表的渲染

  return (
    <View style={s.container}>
      <Header title="品种分组" />
      <ScrollView style={s.content}>
        {Object.entries(groups).map(([name, list]) => (

Object.entries(groups) 把对象转成数组,每个元素是 [key, value] 的形式。

比如 {Working: [...], Sporting: [...]} 会变成:

[
  ['Working', [...]],
  ['Sporting', [...]]
]

然后用 map 遍历,解构出 name(分组名)和 list(品种数组)。

分组项的渲染

          <TouchableOpacity key={name} style={s.item}>
            <View style={s.info}>
              <Text style={s.name}>{name}</Text>
              <Text style={s.count}>{list.length} 个品种</Text>
            </View>
            <Text style={s.arrow}>›</Text>
          </TouchableOpacity>
        ))}
      </ScrollView>
    </View>
  );
}

每个分组项显示:

  • 分组名称:如 “Working”、“Sporting”
  • 品种数量list.length 获取该分组的品种数
  • 箭头:提示可以点击(虽然当前没有实现跳转)

key={name} 用分组名作为 key,因为分组名是唯一的。

样式设计

const s = StyleSheet.create({
  container: {flex: 1, backgroundColor: '#f5f5f5'},
  content: {flex: 1, backgroundColor: '#fff'},

容器背景浅灰,内容区背景白色,形成层次感。

列表项样式:

  item: {
    flexDirection: 'row', 
    alignItems: 'center', 
    padding: 16, 
    borderBottomWidth: 1, 
    borderBottomColor: '#f0f0f0'
  },
  • 横向排列,垂直居中
  • 内边距 16
  • 底部有分隔线

信息区和箭头:

  info: {flex: 1},
  name: {fontSize: 16, fontWeight: '500', color: '#333'},
  count: {fontSize: 13, color: '#999', marginTop: 2},
  arrow: {fontSize: 20, color: '#ccc'},
});
  • infoflex: 1 占满剩余空间
  • 分组名字号 16,加粗
  • 数量字号 13,灰色,作为次要信息
  • 箭头浅灰色,不抢眼

Object.entries vs Object.keys

遍历对象有几种方式:

Object.keys:返回 key 数组

Object.keys(groups).map(name => {
  const list = groups[name];
  // ...
});

Object.entries:返回 [key, value] 数组

Object.entries(groups).map(([name, list]) => {
  // ...
});

Object.values:返回 value 数组

Object.values(groups).map(list => {
  // 拿不到 key
});

这个场景需要同时用到 key 和 value,所以 Object.entries 最合适。

分组顺序的问题

当前分组的顺序是不确定的。JavaScript 对象的属性顺序在大多数情况下是按添加顺序,但不保证。

如果想要固定顺序,可以这样处理:

const ORDER = ['Working', 'Sporting', 'Herding', 'Hound', 'Toy', 'Terrier', 'Non-Sporting', '其他'];

const sortedEntries = Object.entries(groups).sort(([a], [b]) => {
  return ORDER.indexOf(a) - ORDER.indexOf(b);
});

先定义顺序数组,然后按这个顺序排序。

当前没有实现,因为对用户体验影响不大。

可以扩展的功能

点击跳转:点击分组后跳转到该分组的品种列表。

onPress={() => navigate('GroupBreeds', {group: name, breeds: list})}

展开收起:点击分组展开显示品种列表,再点收起。类似手风琴效果。

搜索过滤:在顶部加搜索框,可以搜索分组名或品种名。

分组图标:每个分组加一个代表性的图标或图片。

这些功能后续可以加,当前先实现基础版本。

数据分组的通用方法

分组是个常见需求,可以封装成通用函数:

function groupBy<T>(arr: T[], keyFn: (item: T) => string): Record<string, T[]> {
  const result: Record<string, T[]> = {};
  arr.forEach(item => {
    const key = keyFn(item);
    if (!result[key]) result[key] = [];
    result[key].push(item);
  });
  return result;
}

// 使用
const groups = groupBy(breeds, b => b.breed_group || '其他');

keyFn 是一个函数,接收数组元素,返回分组的 key。这样就可以按任意字段分组了。

当前页面代码量小,没必要抽取。但如果多处用到分组,封装成工具函数是个好选择。

Promise 的 then 写法

数据加载用的是 .then() 而不是 async/await

api.getBreeds().then(res => {
  // 处理数据
});

两种写法都可以,效果一样。.then() 在简单场景下代码更紧凑。

如果有多个异步操作需要顺序执行,async/await 可读性更好:

const loadData = async () => {
  const breeds = await api.getBreeds();
  const images = await api.getImages();
  // ...
};

类型安全的好处

groups 的类型是 Record<string, Breed[]>,这带来几个好处:

编辑器提示:访问 groups.xxx 时,编辑器知道值是 Breed[] 类型。

类型检查:如果不小心写成 groups[name].push('string'),编译时就会报错。

代码可读:一眼就知道这个变量存的是什么结构的数据。

TypeScript 的类型系统在大型项目中特别有价值,能减少很多运行时错误。

小结

品种分组页涉及的知识点:

  • 数据分组算法:O(n) 时间复杂度的分组实现
  • Record 类型:TypeScript 的工具类型,表示对象结构
  • Object.entries:把对象转成可遍历的数组
  • 提前返回:加载状态的简洁处理方式
  • 分组顺序:JavaScript 对象属性顺序的特点

数据处理是前端开发的基本功,掌握好分组、排序、过滤这些操作,很多需求都能轻松应对。

下一篇讲品种对比页面,会涉及到多选和数据对比展示。


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

Logo

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

更多推荐