【HarmonyOS学习笔记】2026-07-06 | 页面与数据5:实现页面跳转
【HarmonyOS学习笔记】2026-07-06 | 页面与数据5:实现页面跳转
date: 2026-07-06
tags: [HarmonyOS, Navigation, 路由, 生命周期, @Builder, @Extend, 安全区域, ArkTS严格模式]
1️⃣ 学习内容概览
本节学习的是"开发者学堂"中"构建更加丰富页面"章节的"页面与数据5 - 实现页面跳转",围绕登录页面的构建,涉及组件装饰器、Navigation 路由容器、页面跳转机制、生命周期管理、ArkTS 严格模式等核心知识点。学习过程中遇到了多个实操问题,逐一排查解决。
2️⃣ 核心知识点与实操问题
一、装饰器体系:@Extend / @Builder / @Component / @Entry
@Extend(组件名)— 给指定组件扩展自定义属性方法
@Extend(TextInput) function inputStyle() { .placeholderColor('#99182431') .height('45vp') .fontSize('18fp') .backgroundColor('#F1F3F5') .width('328vp') }关键特点:
| 特点 | 说明 |
|---|---|
| 必须指定组件类型 | @Extend(TextInput)只能用于 TextInput |
| 支持传参 | 可以传入参数实现差异化(但参数不能是函数类型) |
| 全局可用 | 定义在全局作用域 |
| 可封装属性和事件 | 既能封装样式,也能封装事件 |
与@Styles对比:
| 对比项 | @Extend | @Styles |
|---|---|---|
| 指定组件 | ✅ 必须 | ❌ 通用 |
| 支持事件 | ✅ | ❌ 仅通用属性 |
| 支持传参 | ✅ | ❌ |
@Builder— 声明 UI 构建函数
@Builder imageButton(src: Resource) { Button({ type: ButtonType.Circle, stateEffect: true }) { Image(src) } .height('48vp') .width('48vp') .backgroundColor('#F1F3F5') }⚠️ 关键规则:@Builder必须逐个添加,不会自动延伸到下一个函数!
@Builder imageButton(src: Resource) { ... } // ✅ 有 @Builder @Builder // ← 必须再加一个! login(isLogin: boolean) { ... } // 如果不加,login 就是普通函数⚠️@Builder函数不能为空,必须有至少一个 UI 组件作为返回值。
调用方式:组件内用this.xxx()调用。
与@Extend、@Component对比:
| 对比项 | @Builder | @Extend | @Component |
|---|---|---|---|
| 粒度 | 函数级,复用 UI 片段 | 属性级,扩展组件样式 | 组件级,独立组件 |
| 能否有状态 | ❌ | ❌ | ✅@State |
| 能否传参 | ✅ | ✅ | @Prop接收 |
| 使用方式 | this.xxx() | .xxx()链式 | <Xxx /> |
@Component+struct+@Entry组合
@Entry @Component struct LoginPage { build() { ... } }| 装饰器/关键字 | 作用 |
|---|---|
struct | 值类型结构体,ArkTS 组件的基本单元,承载状态和 UI |
@Component | 将 struct 声明为 UI 组件,必须实现build() |
@Entry | 标记为页面入口,可被路由导航到 |
通俗类比:
struct= 空白画布@Component= 装上画笔和颜料(能画 UI 了)@Entry= 挂到展厅墙上(变成可跳转的页面)
二、Navigation 路由容器
官方定义
Navigation 是路由容器组件,一般作为首页的根容器,支持单页面(Stack)、分栏(Split)、导航栏(Declare)三种模式,支持 NavDestination 子页面的路由跳转。
三种导航模式
| 模式 | 常量 | 适用场景 |
|---|---|---|
| Stack(栈模式) | NavigationMode.Stack | 手机端最常见,推入推出 |
| Split(分栏模式) | NavigationMode.Split | 平板端,左右分栏 |
| Declare(声明模式) | NavigationMode.Declare | 自定义导航栏 |
路由操作
| 操作 | 方法 |
|---|---|
| 跳转 | navStack.pushPath({ name: 'PageName' })或pushPathByName('PageName', null) |
| 返回 | navStack.pop() |
| 替换 | navStack.replacePath({ name: 'PageName' }) |
| 清空 | navStack.clear() |
Navigation vs router
| 对比项 | Navigation | router |
|---|---|---|
| 架构 | 单页面内导航(组件级) | 跨页面导航(页面级) |
| 状态共享 | 同一 Navigation 下可共享 | 页面间需传参 |
| 转场动画 | 更丰富的内置支持 | 基础转场 |
| 推荐程度 | ✅ 官方推荐新项目 | 旧项目兼容 |
三、Navigation 布局问题排查(实操)
问题1:Column 没有占满 Navigation
原因:Navigation 默认有标题栏(即使没设.title()),占据空间。
解决:.hideTitleBar(true)+.layoutWeight(1)组合使用。
| 操作 | 效果 |
|---|---|
hideTitleBar(true) | 消除标题栏,内容区域变大 |
layoutWeight(1) | Column 撑满内容区域 |
| 两者结合 | ✅ Column 撑满全屏 |
⚠️ 单独用layoutWeight(1)不行:内容区域本身不够大(被标题栏压缩了),Column 占满的只是被压缩后的区域。
⚠️height('100%')vslayoutWeight(1):在 Navigation 内部,height('100%')计算可能不精确,推荐用layoutWeight(1)替代。
问题2:上下仍有留白(安全区域)
原因:Navigation 默认避让系统安全区域(状态栏 + 底部导航条)。
解决:
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])⚠️ API 纠正:是SafeAreaEdge,不是SafeAreaSide!实操验证比文档更可靠,遇到 API 差异以实际 SDK 为准。
问题3:Blank() 不生效
原因:Blank()需要父容器有明确高度才能工作,配合layoutWeight(1)或height('100%')使用。
四、白屏问题排查(实机测试)
根因:EntryAbility.ets加载了错误的页面。
// ❌ 错误:MainPage 没有 @Entry,且是 NavDestination windowStage.loadContent('pages/MainPage', (err) => { ... }); // ✅ 正确:加载 @Entry 入口页面 windowStage.loadContent('pages/LoginPage', (err) => { ... });NavDestination 子页面不能作为入口页面直接加载,必须通过 Navigation 容器的路由跳转到达。
五、路由联动机制(重点)
三个配置文件的职责
1.module.json5— 桥梁配置
{ "module": { "pages": "$profile:main_pages", // 系统规定字段,指向入口页面清单 "routerMap": "$profile:route_map" // 系统规定字段,指向导航路由表 } }pages和routerMap是系统规定字段名,不可改名$profile:xxx指向resources/base/profile/xxx.json
2.main_pages.json— 入口页面清单
{"src":["pages/LoginPage"]}- 只放
@Entry标记的页面 - 路径格式:
pages/xxx,不带.ets后缀
3.route_map.json— 导航路由表
{"routerMap":[{"name":"MainPage","pageSourceFile":"src/main/ets/pages/MainPage.ets","buildFunction":"MainPageBuilder","data":{"description":"this is mainPage"}}]}| 字段 | 含义 | 是否自定义 |
|---|---|---|
name | 路由名称,pushPathByName()的参数 | ✅ 可自定义,需与代码一致 |
pageSourceFile | 页面源文件路径 | ✅ 但有格式要求 |
buildFunction | 构建函数名 | ✅ 需与代码中导出的@Builder函数名一致 |
data | 自定义附加数据 | ✅ 可选 |
完整联动流程
1. 应用启动 → 系统读取 module.json5 ├── pages → main_pages.json → 入口 "pages/LoginPage" └── routerMap → route_map.json → 注册 "MainPage" 路由 2. EntryAbility.loadContent('pages/LoginPage') → LoginPage 渲染(@Entry 页面,含 Navigation 容器) 3. 用户点击登录 → pathStack.pushPathByName('MainPage', null) 4. 系统查找 route_map.json → name: "MainPage" → buildFunction: "MainPageBuilder" 5. 调用 MainPageBuilder() → 渲染 MainPage(NavDestination) 6. MainPage 显示在 Navigation 容器内 ✅对应代码结构
// MainPage.ets 中必须有: @Builder export function MainPageBuilder() { // ← 与 route_map 的 buildFunction 一致 MainPage() } // @Entry ← 注意:没有 @Entry! @Component export struct MainPage { build() { NavDestination() { ... } // ← 必须是 NavDestination 包裹 } }两种路由体系对比
| router(传统) | Navigation + NavPathStack(推荐) | |
|---|---|---|
| 页面标记 | 全部@Entry | 入口@Entry,子页面NavDestination |
| 跳转方式 | router.pushUrl() | pathStack.pushPathByName() |
| 状态共享 | 页面间独立 | 同一 Navigation 下可共享 |
| 路由表 | main_pages.json | main_pages.json+route_map.json |
六、组件生命周期
| 生命周期 | 触发时机 | 用途 |
|---|---|---|
aboutToAppear() | 组件即将显示 | 初始化数据、订阅事件 |
aboutToDisappear() | 组件即将销毁 | 清理资源、取消定时器 |
onPageShow() | 页面显示到前台 | 刷新数据 |
onPageHide() | 页面退到后台 | 暂停操作 |
实操应用:清理 setTimeout 定时器
private timeOutId: number = -1; login(result: boolean): void { this.isShowProgress = true; if (this.timeOutId === -1) { this.timeOutId = setTimeout(() => { this.isShowProgress = false; this.timeOutId = -1; this.pathStack.pushPathByName('MainPage', null); }, 2000); } } aboutToDisappear() { clearTimeout(this.timeOutId); // 防止页面销毁后定时器仍在执行 this.timeOutId = -1; }原则:在aboutToAppear中创建的资源,必须在aboutToDisappear中清理。
七、setTimeout返回值与防重复点击
setTimeout()返回定时器 ID(number 类型),是定时器的唯一标识- 保存 ID 后可用于
clearTimeout(id)取消定时器 timeOutId === -1判断防止重复点击登录
八、ArkTS 严格模式:异常处理
Function may throw exceptions. Special handling is required.含义:所有可能抛出异常的函数,调用时必须用try-catch包裹。
// ❌ 裸调,编译报错 promptAction.openToast({ message: '账号密码不能为空!' }) // ✅ try-catch 包裹 try { promptAction.openToast({ message: '账号密码不能为空!' }) } catch (err) { // 处理异常 }常见触发函数:promptAction.openToast()、pathStack.pushPathByName()、router.pushUrl()等。
与 JS 的区别:JS 编译不检查异常处理,ArkTS 编译时强制要求。
九、错误 import 导致类型冲突
This expression is not callable. Type 'typeof Button' has no call signatures.根因:错误导入了import { Button } from '@ohos.multimodalInput.mouseEvent',这个 Button 是鼠标事件类型,不是 ArkUI 的 Button 组件。
解决:ArkUI 组件(Button/Text/Image 等)是全局内置的,不需要也不应该 import。
3️⃣ 为什么这些问题会出现?(刨根问底)
🧠 我的理解
Navigation 的"隐形占位":Navigation 默认有标题栏和安全区域避让,这些是"隐形的"——不设背景色你看不到,但它们确实在占空间。这是框架的默认保护行为,避免内容被状态栏遮挡。
声明式 UI 的上下文限制:
Button()、Text()等不是普通函数,而是声明式 UI 语法。它们只能在build()和@Builder内使用。在普通函数或事件回调中写 UI 组件会报"not callable"错误。两种路由体系的混用陷阱:
@Entry页面是独立路由页面,NavDestination是 Navigation 内的子页面。两者不能混用——NavDestination 不能被loadContent直接加载,@Entry 页面不需要在 route_map 中注册。ArkTS 严格模式的设计哲学:强制异常处理、禁止
any、禁止as断言——所有可能出错的地方,开发者必须显式处理。这是为了提升应用稳定性,避免运行时崩溃。
4️⃣ 实操验证记录
| 问题 | 现象 | 排查过程 | 解决 |
|---|---|---|---|
| Column 不占满 | 棕色背景只占 60% | 先怀疑 height 问题,后确认是标题栏占空间 | hideTitleBar(true)+layoutWeight(1) |
| 上下留白 | 安全区域避让 | 对比色观察边界,确认是系统安全区域 | expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) |
| 白屏 | 实机全白 | 检查 EntryAbility → 发现 loadContent 指向非 @Entry 页面 | 改为pages/LoginPage |
| Button not callable | 编译报错 | 发现错误 import 覆盖了全局 Button | 删除错误 import |
| @Builder 空函数 | 编译报错 | @Builder 必须有 UI 返回值 | 按需决定是否加 @Builder |
| 异常未处理 | ArkTSCheck 报错 | ArkTS 严格模式要求 | try-catch 包裹 |
5️⃣ 最终结论(最佳实践)
| 维度 | 建议 |
|---|---|
| Navigation 布局 | hideTitleBar(true)+layoutWeight(1)+expandSafeArea()三件套 |
| 安全区域 API | SafeAreaEdge(非 SafeAreaSide),以实际 SDK 为准 |
| 路由页面标记 | @Entry= 入口页面,NavDestination= 子页面,不可混用 |
| 路由配置 | main_pages.json放 @Entry 页面,route_map.json放 NavDestination 页面 |
| @Builder 使用 | 逐个添加、不能为空、只能在构建上下文中调用 |
| 组件 import | ArkUI 组件全局内置,禁止 import,避免类型冲突 |
| 生命周期清理 | aboutToDisappear中清理定时器、订阅等资源 |
| 异常处理 | ArkTS 强制 try-catch,编译时检查 |
| 防重复操作 | 用标识变量(如timeOutId === -1)防止重复触发 |
// 登录方法最佳实践 login(result: boolean): void { if (this.account === '' || this.password === '') { try { promptAction.openToast({ message: '账号密码不能为空!' }) } catch (err) { /* 处理异常 */ } } else { this.isShowProgress = true; if (this.timeOutId === -1) { this.timeOutId = setTimeout(() => { this.isShowProgress = false; this.timeOutId = -1; try { this.pathStack.pushPathByName('MainPage', null); } catch (err) { /* 处理异常 */ } }, 2000); } } } aboutToDisappear() { clearTimeout(this.timeOutId); this.timeOutId = -1; }核心原则:Navigation 是页面的"总管",它的标题栏、安全区域、路由栈是三个独立的"隐形层",必须显式处理才能让内容真正占满屏幕;ArkTS 严格模式要求开发者对所有潜在风险(异常、类型安全)显式负责,不存在"假装没看见"。
随笔小结:今天围绕登录页面构建,系统学习了装饰器体系(@Extend/@Builder/@Component/@Entry)、Navigation 路由容器的布局和路由机制、页面跳转的配置联动(main_pages.json + route_map.json + module.json5)、组件生命周期管理、ArkTS 严格模式的异常处理要求。实操中遇到的最大坑是 Navigation 的"隐形占位"(标题栏 + 安全区域)导致布局不预期,以及 EntryAbility 加载非 @Entry 页面导致白屏。核心收获:框架的默认保护行为(标题栏、安全避让、异常检查)都需要开发者显式覆盖,理解这些默认行为是排错的关键。
懿路向前 · AI辅助整理
2026-07-06
