请添加图片描述

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

承上启下

上一篇讲了养护指南的列表页,用户点击某个知识点后会跳转到详情页。这篇就来聊聊详情页的实现,重点是路由参数的接收自定义导航器的设计

详情页本身很简单,但背后的导航系统值得深入讲讲。为什么要自己写导航器?怎么实现页面栈管理?这些问题在这篇文章里都会涉及到。

详情页的完整代码

先看 CareDetailPage 的全貌:

import React from 'react';
import {View, Text, ScrollView, StyleSheet} from 'react-native';
import {Header, Card} from '../../components';
import {useRoute} from '../../hooks';

引入很简洁,就四个:React、RN 组件、自定义组件、自定义 Hook。

没有引入 useStateuseEffect,因为这个页面没有内部状态,数据全靠路由参数。

内容数据的组织

详情内容用字典结构存储:

const CONTENT: Record<string, string> = {
  '幼犬喂养': '幼犬需要高蛋白、高能量的食物来支持快速生长。建议每天喂食3-4次,选择专门的幼犬粮。',
  '成犬饮食': '成年犬每天喂食1-2次即可。根据体型选择合适的狗粮,保持新鲜饮水。',

Record<string, string> 是 TypeScript 的工具类型,表示一个对象,key 和 value 都是字符串。

用知识点标题做 key,这样查找很方便:CONTENT['幼犬喂养'] 直接拿到内容。

继续看其他内容:

  '老年犬营养': '老年犬新陈代谢减慢,需要低热量、高纤维的食物。可以选择老年犬专用粮。',
  '禁忌食物': '狗狗不能吃:巧克力、葡萄、洋葱、大蒜、木糖醇、酒精、咖啡因等。',

老年犬和禁忌食物,都是养狗必须知道的。

日常护理相关:

  '洗澡技巧': '一般每2-4周洗一次澡。使用温水和狗狗专用沐浴露,注意保护眼睛和耳朵。',
  '毛发护理': '根据毛发类型选择合适的梳子,长毛犬需要每天梳理,短毛犬每周2-3次。',
  '指甲修剪': '每2-4周检查一次指甲长度。使用专用指甲钳,注意不要剪到血线。',
  '耳朵清洁': '每周检查耳朵,使用专用清洁液和棉球轻轻擦拭。',

这些都是日常护理的基本操作,新手铲屎官特别需要。

健康医疗部分:

  '疫苗接种': '幼犬需要在6-8周开始接种疫苗,包括犬瘟热、细小病毒、狂犬病等。',
  '常见疾病': '常见疾病包括皮肤病、肠胃问题、关节炎等。定期体检可以早期发现问题。',
  '驱虫指南': '体内驱虫每3个月一次,体外驱虫每月一次。选择正规品牌的驱虫药。',
  '急救知识': '学习基本的急救技能,如人工呼吸、止血包扎等。准备急救箱。',

疫苗和驱虫是必须的,急救知识关键时刻能救命。

训练教育部分:

  '基础训练': '从简单的命令开始,如"坐下"、"趴下"。使用正向强化,用零食奖励。',
  '行为纠正': '对于不良行为,要及时纠正但不要惩罚。通过转移注意力和正向引导来改变。',
  '社会化': '在幼犬期进行社会化训练非常重要。让狗狗接触不同的人、动物和环境。',
  '技能训练': '在掌握基础命令后,可以教授更多技能,如握手、翻滚、捡球等。',

强调正向强化,这是现代训犬的核心理念。

居家环境和情感陪伴:

  '狗窝选择': '选择大小合适、材质舒适的狗窝。放置在安静、通风的位置。',
  '安全防护': '收好电线、化学品、小物件等危险物品。安装宠物门栏。',
  '理解狗狗': '学习狗狗的肢体语言,如摇尾巴、耳朵位置、眼神等。',
  '分离焦虑': '逐渐训练狗狗独处能力。离开时不要过度告别,回来时也不要过度兴奋。',
};

分离焦虑是很多狗狗都有的问题,处理方法很重要。

组件函数的实现

export function CareDetailPage() {
  const {params} = useRoute<{title: string; category: string}>();

useRoute 获取路由参数。泛型 <{title: string; category: string}> 指定参数类型,这样 params.titleparams.category 都有类型提示。

获取内容:

  const content = CONTENT[params.title] || '内容整理中...';

从字典里取内容,用 || 做兜底。如果 params.title 在字典里找不到,就显示"内容整理中…"。

这个兜底很重要。万一列表页和详情页的数据没对上,页面不会崩,用户也能看到提示。

页面渲染结构

  return (
    <View style={s.container}>
      <Header title={params.title} />

Header 的标题用参数里的 title,每个详情页标题都不一样。比如点击"幼犬喂养"进来,Header 就显示"幼犬喂养"。

内容区域:

      <ScrollView style={s.content}>
        <Card>
          <Text style={s.category}>{params.category}</Text>
          <Text style={s.title}>{params.title}</Text>
          <Text style={s.text}>{content}</Text>
        </Card>
      </ScrollView>
    </View>
  );
}

Card 里面三层内容:

  • category:分类名,如"饮食营养",用主题色显示
  • title:知识点标题,如"幼犬喂养",字号大,加粗
  • text:正文内容

详情页的样式

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

容器撑满屏幕,背景浅灰。

文字样式:

  category: {fontSize: 13, color: '#D2691E', marginBottom: 6},
  title: {fontSize: 20, fontWeight: '600', color: '#333', marginBottom: 14},
  text: {fontSize: 15, color: '#666', lineHeight: 26},
});
  • category 字号 13,主题色,像个小标签
  • title 字号 20,加粗,是视觉重点
  • text 字号 15,行高 26,阅读舒适

lineHeight: 26 比默认行高大,多行文本读起来不累。

自定义导航器的设计

详情页能正常工作,离不开背后的导航系统。来看看 navigator.ts 是怎么实现的。

类型定义:

type Params = Record<string, any>;
type Listener = (route: string, params?: Params) => void;
  • Params 是参数类型,key 是字符串,value 可以是任何类型
  • Listener 是监听函数类型,接收路由名和参数

Navigator 类的结构

class Navigator {
  private route = 'Home';
  private stack: Array<{route: string; params?: Params}> = [{route: 'Home'}];
  private listeners = new Set<Listener>();
  private params: Params = {};

四个私有属性:

  • route:当前路由名,初始是 ‘Home’
  • stack:页面栈,数组结构,存储历史记录
  • listeners:监听器集合,用 Set 存储
  • params:当前路由的参数

页面栈初始有一个元素 {route: 'Home'},表示首页。

订阅机制

  subscribe(fn: Listener): () => void {
    this.listeners.add(fn);
    return () => {
      this.listeners.delete(fn);
    };
  }

subscribe 方法添加监听器,返回取消订阅的函数。

用 Set 存储监听器有两个好处:

  • 自动去重:同一个函数不会被添加两次
  • 删除高效:Set 的 delete 是 O(1) 复杂度

通知所有监听器:

  private notify() {
    this.listeners.forEach(fn => fn(this.route, this.params));
  }

遍历 Set,调用每个监听函数,传入当前路由和参数。

导航方法

前进导航:

  navigate(route: string, params?: Params) {
    this.route = route;
    this.params = params || {};
    this.stack.push({route, params});
    this.notify();
  }

四步操作:

  1. 更新当前路由名
  2. 更新当前参数(没传就用空对象)
  3. 把新页面压入栈
  4. 通知所有监听器

返回导航:

  goBack(): boolean {
    if (this.stack.length > 1) {
      this.stack.pop();
      const prev = this.stack[this.stack.length - 1];
      this.route = prev.route;
      this.params = prev.params || {};
      this.notify();
      return true;
    }
    return false;
  }

先检查栈长度,大于 1 才能返回(不能把首页也弹出去)。

弹出当前页面,取栈顶元素作为新的当前页面,更新状态并通知。

返回 boolean 表示是否成功返回,调用方可以据此判断。

重置导航:

  reset(route: string) {
    this.route = route;
    this.params = {};
    this.stack = [{route}];
    this.notify();
  }

清空栈,只保留指定的页面。用于登录后跳转首页这种场景,不希望用户能返回到登录页。

辅助方法

  getRoute() { return this.route; }
  getParams<T>() { return this.params as T; }
  canGoBack() { return this.stack.length > 1; }
}
  • getRoute 获取当前路由名
  • getParams 获取当前参数,支持泛型指定类型
  • canGoBack 判断能否返回,Header 组件用这个决定是否显示返回按钮

导出单例:

export const navigator = new Navigator();

整个 App 共用一个 navigator 实例。

useRoute Hook 的实现

组件里用 useRoute 获取参数,看看它的实现:

export function useRoute<T = any>() {
  const [route, setRoute] = useState(navigator.getRoute());
  const [params, setParams] = useState<T>(navigator.getParams<T>());

两个 state 分别存储路由名和参数。初始值从 navigator 获取。

订阅变化:

  useEffect(() => {
    const unsubscribe = navigator.subscribe((r, p) => {
      setRoute(r);
      setParams((p || {}) as T);
    });
    return unsubscribe;
  }, []);

组件挂载时订阅 navigator,路由变化时更新 state。

return unsubscribe 是清理函数,组件卸载时取消订阅,防止内存泄漏。

返回值:

  return {route, params};
}

返回对象,组件里解构使用:const {params} = useRoute()

为什么自己写导航器

你可能会问,React Navigation 不香吗,为什么要自己造轮子?

几个原因:

兼容性问题。试了几个主流导航库,在 OpenHarmony 上多少都有问题。与其花时间排查,不如自己写一个简单的。

需求简单。狗狗之家的导航需求不复杂,就是前进、后退、重置,不需要 Tab 导航、Drawer 导航这些高级功能。

可控性强。自己写的代码,出了问题好排查,想加功能也方便。

当然,如果项目复杂,还是建议用成熟的导航库,自己写容易漏掉边界情况。

页面栈的可视化理解

假设用户的操作路径是:首页 → 养护指南 → 幼犬喂养

页面栈的变化:

初始:[{route: 'Home'}]
点击养护指南:[{route: 'Home'}, {route: 'Care'}]
点击幼犬喂养:[{route: 'Home'}, {route: 'Care'}, {route: 'CareDetail', params: {title: '幼犬喂养', category: '饮食营养'}}]

点击返回:

返回一次:[{route: 'Home'}, {route: 'Care'}]
再返回:[{route: 'Home'}]

栈是后进先出的结构,最后进入的页面最先被弹出。

参数传递的完整链路

再梳理一下参数从发送到接收的完整过程:

1. 列表页发送参数

navigate('CareDetail', {title: item, category: c.title})

2. navigator 存储参数

this.params = params || {};
this.stack.push({route, params});

3. 详情页接收参数

const {params} = useRoute<{title: string; category: string}>();

4. 使用参数渲染

<Text>{params.title}</Text>
<Text>{CONTENT[params.title]}</Text>

整个链路清晰明了,数据流向单一。

类型安全的好处

用 TypeScript 的泛型指定参数类型:

useRoute<{title: string; category: string}>()

好处是:

  • 编辑器提示:输入 params. 会自动提示 titlecategory
  • 类型检查:如果写错属性名,编译时就会报错
  • 代码可读:一眼就知道这个页面需要哪些参数

小结

养护详情页本身代码不多,但背后的导航系统值得深入理解。

这篇文章讲了:

  • 字典结构存储详情内容,查找方便
  • useRoute Hook 获取路由参数
  • Navigator 类的完整实现:订阅机制、页面栈管理、导航方法
  • 参数传递的完整链路

自己实现导航器虽然简单,但涵盖了发布订阅、栈数据结构、TypeScript 泛型等知识点,是个不错的练手项目。

下一篇讲品种列表页,会涉及到网络请求和列表渲染的优化。


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

Logo

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

更多推荐