HarmonyOS应用《奇妙科学乐园》开发第66篇:页面跳转与参数传递——pushUrl/replaceUrl实战

一、引言
在"奇妙科学乐园"这款面向6-12岁儿童的纯离线科普教育应用中,页面间的跳转与数据传递是贯穿全局的基础能力。用户从首页点击文章卡片进入详情页、从实验室列表进入实验详情、从趣味问答跳转到答题结果页、底部Tab栏在各功能模块间切换——这些场景都依赖于路由跳转和参数传递。
HarmonyOS提供了@ohos.router模块作为原生路由能力,但在实际业务开发中直接使用系统API会面临以下问题:
(1)异常处理缺失
系统路由API(如router.pushUrl)在目标页面不存在、路由栈溢出等异常情况下会抛出错误。如果每个跳转点都手写try-catch,代码重复度极高,且容易遗漏。
(2)参数类型不安全
router.pushUrl的params参数类型为Record<string, Object>,这意味着传入任何参数都不会在编译期报错。但如果接收方期望topicId: number,而发送方传了topicId: string,问题只能在运行时暴露。
(3)跳转方式选择混乱
HarmonyOS路由提供了pushUrl(压栈)、replaceUrl(替换)、back(返回)、clear(清栈)等多种操作,开发者需要根据业务场景选择正确的跳转方式。错误的选择会导致路由栈堆积、用户无法返回等问题。
(4)缺少统一日志追踪
当用户报告"点击按钮没反应"或"跳转到了错误页面"时,如果没有统一的日志记录,排查问题需要逐个断点调试,效率极低。
针对以上问题,本应用设计了RouterUtil工具类和RouterParams参数接口,在系统路由API之上构建了一层类型安全、异常可控、日志可追踪的跳转封装。上一篇(第65篇)介绍了路由地址的中心化管理,本篇聚焦于跳转逻辑的封装与参数传递的实战。
二、学习目标
通过本章的学习,你将能够:
- 掌握
RouterParams接口设计,实现路由参数的类型安全约束 - 掌握
RouterUtil工具类封装,统一跳转异常处理与日志追踪 - 掌握
pushUrl与replaceUrl的选择策略与实战应用 - 掌握页面间参数传递的封装方法与接收方的防御性编程
- 理解日志追踪与异常处理机制的设计思路
三、需求分析
(1)参数接口的设计需求
- 所有字段可选:每个参数都标记为
optional(?),因为不同页面需要的参数不同。跳转到设置页面不需要任何参数,跳转到详情页则需要topicId。 - 类型明确:
topicId为number类型,labId为string类型,tabIndex为number类型。类型区分来自业务数据模型的设计——文章用数字ID,实验用字符串ID。 - 集中声明:全应用所有页面间传递的参数都集中在这一个接口中。新增页面需要传递新参数时,在这里添加字段即可。如果两个页面需要传递同名但不同类型的参数,则需要考虑是否应该拆分接口。
在RouterParams的基础上,需要进一步定义RouterOptions接口,将params的类型从宽泛的Record<string, Object>收窄为RouterParams。这是类型安全跳转的第一道防线。
通过RouterParams接口,开发者在编写跳转代码时就能获得类型提示和编译期检查,避免了"传错了参数类型但编译通过"的问题。例如:
// 场景一:跳转到科普详情页,需要传递文章ID
const params: RouterParams = { topicId: 1 }; // 正确:number 类型
const params2: RouterParams = { topicId: '1' }; // 编译错误:不能将 string 赋值给 number
// 场景二:跳转到主Tab页,指定切换到第2个Tab(科普知识)
const params3: RouterParams = { tabIndex: 1 }; // 正确:number 类型
// 场景三:跳转到实验详情页,需要传递实验ID
const params4: RouterParams = { labId: 'exp_001' }; // 正确:string 类型
// 场景四:同时传递多个参数(当前业务暂无此需求,但接口支持)
const params5: RouterParams = { topicId: 1, tabIndex: 0 }; // 正确:多参数组合
(2)工具类封装的设计需求
RouterUtil的每个跳转方法需要满足以下设计约束:
from参数:第二个参数from是一个字符串标记,用于在日志中标识跳转的发起页面。当排查跳转问题时,通过日志可以立即定位是哪个页面发起的跳转。- 异常不抛出:
catch块中只记录日志,不throw error。这是有意为之的设计——在儿童教育应用中,跳转失败不应该导致应用崩溃或弹出不友好的错误提示。 - 静态方法:所有路由方法都是静态方法,不需要实例化
RouterUtil即可调用,使用简洁。 - 安全返回:当
router.getParams()抛出异常时(比如在Previewer环境中无法获取参数),返回一个空对象而非null,确保调用方使用params.topicId时不会因为null访问而崩溃。
四、核心实现
步骤一:定义 RouterParams 与 RouterOptions 接口
在RouterUtil.ets文件中,首先定义了RouterParams接口,统一管理全应用的路由参数:
// 工具文件:entry/src/main/ets/utils/RouterUtil.ets
/*
* 文件用途:路由工具 - 统一路由跳转封装,处理异常和日志
* 创建时间:2026-07-14
* 兼容环境:macOS/Linux/Docker/TRAE 云端
* 版本:v1.0
* 风险提示:无
*/
import router from '@ohos.router';
import { Logger } from './Logger';
const TAG = 'RouterUtil';
// 路由参数接口——全应用统一的参数类型定义
export interface RouterParams {
topicId?: number; // 科普文章ID,用于 TopicDetail 页面
tabIndex?: number; // Tab索引,用于 MainTabs 页面切换指定Tab
categoryId?: string; // 分类ID,用于 Quiz 页面筛选分类
labId?: string; // 实验ID,用于 LabDetail 页面
}
在RouterParams的基础上,进一步定义了RouterOptions接口:
// 路由选项接口——封装跳转目标地址和参数
export interface RouterOptions {
url: string; // 目标页面路由地址(来自 RouteUrls 常量)
params?: RouterParams; // 路由参数(可选)
}
步骤二:封装 RouterUtil 工具类
pushUrl是最常用的跳转方式,将目标页面压入路由栈。用户可以通过返回按钮回到上一个页面:
export class RouterUtil {
/**
* 跳转到指定页面(压栈)
* @param options 路由选项,包含目标地址和可选参数
* @param from 来源标记,用于日志追踪,标识跳转发起方
*/
static async pushUrl(options: RouterOptions, from: string = ''): Promise<void> {
try {
// 记录跳转日志,包含来源页面标识
Logger.info(TAG, `${from ? `[${from}] ` : ''}pushUrl: ${options.url}`);
// 调用系统路由API执行跳转
await router.pushUrl(options);
} catch (error) {
// 捕获异常并记录错误日志,防止应用崩溃
Logger.error(TAG, `${from ? `[${from}] ` : ''}pushUrl failed: ${options.url}`, error);
}
}
// ...
}
replaceUrl用目标页面替换当前页面,不会增加路由栈深度。适用于"切换后不需要返回当前页"的场景:
/**
* 替换当前页面(不压栈)
* @param options 路由选项
* @param from 来源标记,用于日志追踪
*/
static async replaceUrl(options: RouterOptions, from: string = ''): Promise<void> {
try {
Logger.info(TAG, `${from ? `[${from}] ` : ''}replaceUrl: ${options.url}`);
await router.replaceUrl(options);
} catch (error) {
Logger.error(TAG, `${from ? `[${from}] ` : ''}replaceUrl failed: ${options.url}`, error);
}
}
返回上一页:
/**
* 返回上一页
* @param from 来源标记,用于日志追踪
*/
static async back(from: string = ''): Promise<void> {
try {
Logger.info(TAG, `${from ? `[${from}] ` : ''}back`);
router.back();
} catch (error) {
Logger.error(TAG, `${from ? `[${from}] ` : ''}back failed`, error);
}
}
获取路由参数:
/**
* 获取路由参数
* @returns 路由参数对象
*/
static getParams(): RouterParams {
try {
const params = router.getParams() as RouterParams;
return params;
} catch (error) {
Logger.error(TAG, 'getParams failed', error);
// 获取失败时返回空对象,防止调用方空指针异常
const emptyParams: RouterParams = {};
return emptyParams;
}
}
清空路由栈,跳转到指定页面:
/**
* 清空路由栈,跳转到指定页面
* @param url 目标页面URL
* @param from 来源标记,用于日志追踪
*/
static async clearAndPush(url: string, from: string = ''): Promise<void> {
try {
Logger.info(TAG, `${from ? `[${from}] ` : ''}clearAndPush: ${url}`);
router.clear();
const pushOptions: RouterOptions = { url: url };
await router.pushUrl(pushOptions);
} catch (error) {
Logger.error(TAG, `${from ? `[${from}] ` : ''}clearAndPush failed: ${url}`, error);
}
}
辅助方法:路由栈长度与状态查询:
/**
* 获取路由栈长度
* @returns 路由栈长度
*/
static getStackLength(): number {
try {
const len = router.getLength();
return Number(len) || 0;
} catch (error) {
Logger.error(TAG, 'getStackLength failed', error);
return 0;
}
}
/**
* 获取当前路由状态
* @returns 路由状态对象
*/
static getState(): router.RouterState | null {
try {
return router.getState();
} catch (error) {
Logger.error(TAG, 'getState failed', error);
return null;
}
}
步骤三:参数封装与传递实战
场景一:携带文章ID跳转详情页(pushUrl)
这是应用中最常见的参数传递场景。用户在首页、科普列表、收藏页、历史记录页点击文章时,都需要携带topicId跳转到TopicDetail页面。
发起方代码(以 Index.ets 首页为例):
// 页面文件:entry/src/main/ets/pages/Index.ets
import { RouterUtil, RouterOptions, RouterParams } from '../utils/RouterUtil';
import { RouteUrls } from '../constants/RouteUrls';
// 跳转到科普详情页
goToTopicDetail(topic: Topic): void {
// 构建类型安全的路由参数
const params: RouterParams = { topicId: topic.id };
// 构建路由选项
const options: RouterOptions = {
url: RouteUrls.TOPIC_DETAIL,
params: params
};
// 使用 RouterUtil 执行压栈跳转,'Index' 标识来源页面
RouterUtil.pushUrl(options, 'Index');
}
接收方代码(TopicDetail.ets 详情页):
// 页面文件:entry/src/main/ets/pages/TopicDetail.ets
import { RouterUtil } from '../utils/RouterUtil';
import { scienceData } from '../viewmodel/ScienceData';
@Entry
@Component
struct TopicDetail {
@State topic: Topic | null = null;
@State isFavorite: boolean = false;
aboutToAppear() {
// 通过 RouterUtil 获取路由参数
const params = RouterUtil.getParams() as Record<string, Object>;
// 校验参数存在性,防止空参数导致崩溃
if (params && params.topicId) {
// 类型断言:从 Object 转为 number
const topicId = params.topicId as number;
// 根据ID查询文章数据
const foundTopic = scienceData.getTopicById(topicId);
if (foundTopic) {
this.topic = foundTopic;
this.isFavorite = userPrefs.isFavoriteSync(topicId);
this.recordRead(foundTopic);
}
}
}
// 文章不存在时的兜底处理
// build() 方法中会判断 this.topic 是否为 null
// 如果为 null,显示"文章不存在"提示和"返回首页"按钮
}
参数传递链路:
Index.ets
RouterParams { topicId: 1 }
-> router.pushUrl({ url: 'pages/TopicDetail', params: { topicId: 1 } })
-> 系统路由序列化参数
-> TopicDetail 页面加载
-> RouterUtil.getParams() -> { topicId: 1 }
-> as Record<string, Object> -> topicId as number -> 1
-> scienceData.getTopicById(1) -> Topic 对象
场景二:携带实验ID跳转实验详情(pushUrl)
实验室列表跳转到实验详情的参数传递模式与文章详情类似,但参数类型不同:
// 页面文件:entry/src/main/ets/pages/Lab.ets
import { RouterUtil, RouterOptions, RouterParams } from '../utils/RouterUtil';
import { RouteUrls } from '../constants/RouteUrls';
// 跳转到实验详情页
goToDetail(experimentId: string): void {
// 实验ID是 string 类型(如 'exp_001'),与文章的 number 类型不同
const params: RouterParams = { labId: experimentId };
const options: RouterOptions = { url: RouteUrls.LAB_DETAIL, params: params };
RouterUtil.pushUrl(options, 'Lab');
}
// 页面文件:entry/src/main/ets/pages/LabDetail.ets
aboutToAppear() {
const params = RouterUtil.getParams() as Record<string, Object>;
if (params && params.labId) {
const id = params.labId as string;
// 根据实验ID查询实验数据
this.experiment = scienceData.getExperimentById(id) || null;
}
this.isLoading = false;
}
场景三:携带分类ID跳转问答页(pushUrl + params)
// 页面文件:entry/src/main/ets/pages/Quiz.ets
aboutToAppear() {
const params = RouterUtil.getParams() as Record<string, Object>;
// 如果传入了分类ID,直接进入该分类的答题
if (params && params.categoryId) {
this.currentCategory = params.categoryId as string;
}
this.categories = scienceData.getAllCategories();
Logger.info(TAG, '问答页面加载');
}
场景四:答题结果页的多参数传递
答题结果页(QuizResult.ets)是参数最多的页面,接收多达7个参数。虽然这些参数不完全在RouterParams接口中定义(如correctCount、percentage等是结果页特有的),但通过RouterUtil.getParams()统一获取:
// 页面文件:entry/src/main/ets/pages/QuizResult.ets
aboutToAppear() {
const params = RouterUtil.getParams() as Record<string, Object>;
if (params) {
// 逐个参数做存在性校验和类型转换
if (params.correctCount !== undefined && params.correctCount !== null) {
this.correctCount = params.correctCount as number;
}
if (params.totalCount !== undefined && params.totalCount !== null) {
this.totalCount = params.totalCount as number;
}
if (params.percentage !== undefined && params.percentage !== null) {
this.percentage = params.percentage as number;
}
if (params.category !== undefined && params.category !== null) {
this.category = params.category as string;
}
if (params.isDaily !== undefined && params.isDaily !== null) {
this.isDaily = params.isDaily as boolean;
}
if (params.usedTime !== undefined && params.usedTime !== null) {
this.usedTime = params.usedTime as number;
this.hasUsedTime = true;
}
}
}
防御性编程:每个参数都做了undefined和null双重校验。这是因为在Previewer环境中路由参数可能为空,如果不做校验,Previewer会直接崩溃。
步骤四:pushUrl 与 replaceUrl 选择策略
4.1 两种跳转方式的核心区别
pushUrl(压栈式跳转):
路由栈变化:[A] -> pushUrl(B) -> [A, B]
用户行为:点击返回 -> 回到A
适用场景:用户从列表进入详情,需要返回列表
replaceUrl(替换式跳转):
路由栈变化:[A] -> replaceUrl(B) -> [B]
用户行为:点击返回 -> 回到A之前的页面
适用场景:Tab切换、重定向到首页、登录后跳转
4.2 本应用中的使用统计与策略
通过分析全应用的16处路由跳转调用,可以总结出明确的选择策略:
pushUrl 的使用场景(8处):
// 场景1:列表 -> 详情(最常见)
// 首页 -> 科普详情
RouterUtil.pushUrl({ url: RouteUrls.TOPIC_DETAIL, params: { topicId: topic.id } }, 'Index');
// 科普列表 -> 科普详情
RouterUtil.pushUrl({ url: RouteUrls.TOPIC_DETAIL, params: { topicId: topic.id } }, 'Topics');
// 收藏列表 -> 科普详情
RouterUtil.pushUrl({ url: RouteUrls.TOPIC_DETAIL, params: { topicId: topic.id } }, 'Favorites');
// 历史记录 -> 科普详情
RouterUtil.pushUrl({ url: RouteUrls.TOPIC_DETAIL, params: { topicId: topicId } }, 'History');
// 场景2:列表 -> 详情(实验)
// 实验室列表 -> 实验详情
RouterUtil.pushUrl({ url: RouteUrls.LAB_DETAIL, params: { labId: experimentId } }, 'Lab');
// 场景3:功能入口 -> 子页面
// 个人中心 -> 成就页面
RouterUtil.pushUrl({ url: RouteUrls.ACHIEVEMENT }, 'Profile');
// 科普列表 -> 设置页面
RouterUtil.pushUrl({ url: RouteUrls.SETTINGS }, 'Topics');
// 个人中心 -> 其他功能页
RouterUtil.pushUrl({ url: item.pageUrl }, 'Profile');
replaceUrl 的使用场景(8处):
// 场景1:返回首页(答题结束后)
// 问答结果 -> 首页
RouterUtil.replaceUrl({ url: RouteUrls.MAIN_TABS }, 'Quiz');
// 每日挑战结果 -> 首页
RouterUtil.replaceUrl({ url: RouteUrls.MAIN_TABS }, 'DailyChallenge');
// 每日挑战空状态 -> 首页
RouterUtil.replaceUrl({ url: RouteUrls.MAIN_TABS }, 'DailyChallenge');
// 答题结果页 -> 首页
RouterUtil.replaceUrl({ url: RouteUrls.MAIN_TABS }, 'QuizResult');
// 文章不存在时 -> 首页
RouterUtil.replaceUrl({ url: RouteUrls.MAIN_TABS }, 'TopicDetail');
// 场景2:空状态引导去指定Tab
// 收藏为空 -> 首页(切到科普Tab)
RouterUtil.replaceUrl({ url: RouteUrls.MAIN_TABS, params: { tabIndex: 1 } }, 'Favorites');
// 历史记录为空 -> 首页(切到科普Tab)
RouterUtil.replaceUrl({ url: RouteUrls.MAIN_TABS, params: { tabIndex: 1 } }, 'History');
// 场景3:底部Tab栏切换
RouterUtil.replaceUrl({ url: tab.pageUrl }, 'BottomTabBar');
4.3 选择策略总结
选择决策树:
需要用户能"返回"当前页面吗?
├── 是 -> pushUrl(压栈跳转)
│ 典型场景:
│ - 列表页 -> 详情页
│ - 功能入口 -> 子功能页
│ - 任何需要"返回"的导航
│
└── 否 -> replaceUrl(替换跳转)
典型场景:
- 底部Tab栏切换
- 答题结束返回首页
- 文章不存在时回首页
- 空状态引导去其他Tab
4.4 正反对比:错误选择导致的路由栈问题
// 场景:用户在首页点击"每日挑战",答完题后点击"返回首页"
// 正确做法:答题结束后用 replaceUrl
// 路由栈:[MainTabs] -> pushUrl(DailyChallenge) -> [MainTabs, DailyChallenge]
// -> replaceUrl(MainTabs) -> [MainTabs]
// 用户点击返回:退出应用(栈底)
// 结果:路由栈干净,没有冗余页面
// 错误做法:答题结束后用 pushUrl
// 路由栈:[MainTabs] -> pushUrl(DailyChallenge) -> [MainTabs, DailyChallenge]
// -> pushUrl(MainTabs) -> [MainTabs, DailyChallenge, MainTabs]
// 用户点击返回:回到 DailyChallenge 页面!然后又到 MainTabs...
// 结果:路由栈堆积,用户反复在页面间循环,体验极差
// 场景:底部Tab栏切换
// 正确做法:使用 replaceUrl
// 路由栈:[MainTabs(首页)] -> replaceUrl(MainTabs(科普)) -> [MainTabs(科普)]
// 用户切换Tab不会增加栈深度,永远只有一层
// 错误做法:使用 pushUrl
// 路由栈:[MainTabs] -> pushUrl(MainTabs) -> [MainTabs, MainTabs]
// -> pushUrl(MainTabs) -> [MainTabs, MainTabs, MainTabs]
// 用户切换5次Tab后,按返回需要连续按5次才能退出
// 结果:路由栈无限增长,最终可能导致栈溢出
步骤五:日志追踪与异常处理机制
5.1 日志格式设计
RouterUtil的每个方法都通过Logger工具输出统一格式的日志。以pushUrl为例:
Logger.info(TAG, `${from ? `[${from}] ` : ''}pushUrl: ${options.url}`);
日志输出示例:
// 从首页跳转到科普详情页
[ScienceApp] [RouterUtil] [Index] pushUrl: pages/TopicDetail
// 从收藏页跳转到科普详情页
[ScienceApp] [RouterUtil] [Favorites] pushUrl: pages/TopicDetail
// 从每日挑战替换到首页
[ScienceApp] [RouterUtil] [DailyChallenge] replaceUrl: pages/MainTabs
// 返回操作
[ScienceApp] [RouterUtil] [TopicDetail] back
// 跳转失败
[ScienceApp] [RouterUtil] [Quiz] pushUrl failed: pages/NonExist, error: page not found
日志的价值:
- **
[Index]**:一眼就能看出是哪个页面发起的跳转 - **
pushUrl: pages/TopicDetail**:清楚知道跳转目标 - **
failed**:快速定位跳转失败的记录
5.2 异常处理策略
// 本应用的异常处理策略:吞掉异常 + 记录日志
try {
await router.pushUrl(options);
} catch (error) {
// 不抛出异常,只记录日志
Logger.error(TAG, `pushUrl failed: ${options.url}`, error);
}
// 替代方案对比:
// 方案A:抛出异常,让调用方处理
// 优点:调用方可以自定义错误处理
// 缺点:每个调用点都要写 catch,容易遗漏
// 方案B(本应用采用):统一捕获,不向上抛出
// 优点:调用方代码简洁,不会因为路由异常崩溃
// 缺点:调用方无法感知跳转失败
// 为什么选择方案B?
// 儿童教育应用中,路由失败不应该弹错误弹窗
// 用户(6-12岁儿童)看到技术错误信息会造成困惑
// 静默失败 + 日志记录是最友好的方式
5.3 Logger 工具的配合
RouterUtil依赖Logger工具类输出日志。Logger封装了@kit.PerformanceAnalysisKit的hilog接口:
// 工具文件:entry/src/main/ets/utils/Logger.ets
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN = 0x0000;
const LOG_TAG = 'ScienceApp';
export class Logger {
static info(tag: string, message: string): void {
hilog.info(DOMAIN, LOG_TAG, '[%{public}s] %{public}s', tag, message);
}
static error(tag: string, message: string, error?: Error | string): void {
if (error) {
const errMsg = typeof error === 'string' ? error : error.message || 'unknown error';
hilog.error(DOMAIN, LOG_TAG, '[%{public}s] %{public}s, error: %{public}s', tag, message, errMsg);
} else {
hilog.error(DOMAIN, LOG_TAG, '[%{public}s] %{public}s', tag, message);
}
}
}
Logger使用%{public}s格式化标记确保日志在发布版本中也能被hilog工具读取(非public标记的日志在发布版会被过滤)。
五、常见问题
Q1:为什么路由参数接收必须做防御性编程?
在HarmonyOS应用中,路由参数的获取有两个特殊场景需要处理:
场景一:Previewer环境
DevEco Studio的Previewer在预览单个页面时,不会经过正常的路由跳转流程,因此router.getParams()返回的参数为空。如果页面代码直接使用params.topicId而不做校验,Previewer会崩溃。
场景二:异常跳转
如果用户通过某种异常方式(如通知栏点击、系统回调等)直接进入某个页面,可能没有携带预期参数。
Q2:本应用采用了哪些防御性参数接收模式?
本应用针对不同的参数情况采用了两种防御模式:
模式一:空值检查 + 降级UI
适用于有明确业务数据的页面,如文章详情、实验详情。参数缺失时显示兜底UI:
// TopicDetail.ets:文章详情页的参数接收
aboutToAppear() {
const params = RouterUtil.getParams() as Record<string, Object>;
// 第一层防御:检查 params 对象是否存在
if (params && params.topicId) {
// 第二层防御:类型断言
const topicId = params.topicId as number;
// 第三层防御:查询结果可能为空
const foundTopic = scienceData.getTopicById(topicId);
if (foundTopic) {
this.topic = foundTopic;
// 正常业务逻辑
}
}
// 如果任何一层防御失败,this.topic 保持 null
// build() 方法中有 null 判断,显示"文章不存在"兜底页面
}
模式二:默认值 + 逐字段校验
适用于参数多、每个参数有独立默认值的页面,如答题结果页:
// QuizResult.ets:答题结果页的参数接收
@State correctCount: number = 0; // 默认值为0,防止显示异常
@State totalCount: number = 0;
@State percentage: number = 0;
@State hasUsedTime: boolean = false; // 标记是否传入了用时参数
aboutToAppear() {
const params = RouterUtil.getParams() as Record<string, Object>;
if (params) {
// 每个参数独立校验,互不影响
if (params.correctCount !== undefined && params.correctCount !== null) {
this.correctCount = params.correctCount as number;
}
if (params.usedTime !== undefined && params.usedTime !== null) {
this.usedTime = params.usedTime as number;
this.hasUsedTime = true; // 标记该参数有效,控制UI显示
}
}
// 在 build() 中通过 hasUsedTime 控制是否显示"用时"卡片
}
Q3:有防御和无防御的代码差异是什么?
// 无防御的写法(危险)
aboutToAppear() {
const params = RouterUtil.getParams();
// 直接访问,Previewer 中 params 为空会崩溃
const topicId = params.topicId as number;
this.topic = scienceData.getTopicById(topicId);
}
// 风险:Previewer 崩溃、异常跳转白屏、无兜底UI
// 有防御的写法(本应用采用)
aboutToAppear() {
const params = RouterUtil.getParams() as Record<string, Object>;
if (params && params.topicId) {
const topicId = params.topicId as number;
const foundTopic = scienceData.getTopicById(topicId);
if (foundTopic) {
this.topic = foundTopic;
}
}
}
// 安全:Previewer 正常、异常跳转显示兜底UI、数据为空不崩溃
六、本章小结
8.1 RouterUtil 的三层设计总结
回顾本应用的路由跳转设计,可以归纳为"三层架构":
第一层:RouteUrls(路由地址层)
"去哪里" —— 集中定义所有页面地址
-> 下一篇(第65篇)已详细讲解
第二层:RouterParams + RouterOptions(参数定义层)
"带什么" —— 统一参数类型,编译期检查
-> 本篇重点讲解
第三层:RouterUtil(跳转执行层)
"怎么去" —— 封装跳转API,统一异常处理和日志
-> 本篇重点讲解
三层各司其职,相互配合:
// 一次完整的类型安全跳转,需要三层协同
// 第一层:提供地址
import { RouteUrls } from '../constants/RouteUrls';
// 第二层:提供类型
import { RouterUtil, RouterOptions, RouterParams } from '../utils/RouterUtil';
// 第三层:执行跳转
const params: RouterParams = { topicId: 1 };
const options: RouterOptions = { url: RouteUrls.TOPIC_DETAIL, params: params };
RouterUtil.pushUrl(options, 'Index');
8.2 实践规范汇总
路由跳转规范清单:
地址管理:
所有路由地址使用 RouteUrls 常量,禁止硬编码字符串
参数传递:
使用 RouterParams 接口定义参数,享受类型检查
参数接收方必须做空值校验
跳转方式选择:
需要返回 -> pushUrl(列表进详情)
不需要返回 -> replaceUrl(Tab切换、结果回首页)
日志追踪:
所有跳转必须传入 from 参数标识来源页面
异常处理:
统一由 RouterUtil 捕获,调用方不需要 try-catch
8.3 正反实践对比总结
// 完整的正反对比
// 1. 跳转调用
// 正确
RouterUtil.pushUrl({ url: RouteUrls.TOPIC_DETAIL, params: { topicId: 1 } }, 'Index');
// 错误
router.pushUrl({ url: 'pages/TopicDetail', params: { topicId: 1 } });
// 问题:绕过封装,无日志无异常处理,地址硬编码
// 2. 参数构建
// 正确
const params: RouterParams = { topicId: topic.id };
const options: RouterOptions = { url: RouteUrls.TOPIC_DETAIL, params: params };
// 错误
router.pushUrl({ url: 'pages/TopicDetail', params: { id: topic.id } as any });
// 问题:参数名与接收方不一致,使用 any 绕过类型检查
// 3. 参数接收
// 正确
const params = RouterUtil.getParams() as Record<string, Object>;
if (params && params.topicId) {
const topicId = params.topicId as number;
// 安全使用 topicId
}
// 错误
const params = RouterUtil.getParams();
const id = params.topicId;
// 问题:params 可能为 null,直接访问会崩溃
// 4. 跳转方式选择
// 正确:答题结束返回首页
RouterUtil.replaceUrl({ url: RouteUrls.MAIN_TABS }, 'Quiz');
// 错误
RouterUtil.pushUrl({ url: RouteUrls.MAIN_TABS }, 'Quiz');
// 问题:路由栈堆积,用户按返回会回到答题页
8.4 适用场景与展望
本应用的RouterUtil+RouterParams设计特别适合以下场景:
- 页面数量多(10个以上):统一的跳转封装减少重复代码
- 参数传递频繁:类型安全的参数接口避免运行时错误
- 面向儿童的应用:异常静默处理确保用户体验不受技术错误影响
- 需要问题排查:统一的日志格式加速开发调试
在后续版本中,可以考虑为RouterParams增加更细粒度的参数类型约束——为每个目标页面定义专属的参数接口,实现"发送方和接收方的参数类型双向校验"。当前的统一接口设计是务实的选择,在类型安全和开发效率之间取得了平衡。
本篇与第65篇共同构成了本应用路由管理的完整技术方案:第65篇解决"去哪里"的问题(RouteUrls中心化管理),本篇解决"怎么去"和"带什么"的问题(RouterUtil封装 + RouterParams类型安全)。
七、相关链接
- 源码仓库:WonderSciencePark
- 相关文章:第65篇《路由地址中心化管理》(RouteUrls 设计)
- 下一篇预告:后续将继续分享《奇妙科学乐园》HarmonyOS 实战开发技巧,敬请关注
🔗 相关链接
更多推荐


所有评论(0)