4、页面路由设计:Router模块使用、页面栈管理、参数传递与返回

页面路由,说白了就是决定「用户点了个按钮,接下来该跳转到哪个页面」。在鸿蒙开发里,Router模块就是干这个活的。我刚开始接触时觉得这不就是个跳转嘛,但真正做多媒体播放器这种多页面应用时,才发现路由设计的好坏,直接影响用户体验和代码维护成本。

4.1 Router模块基础用法

鸿蒙的Router模块,提供了页面跳转的核心能力。它支持两种跳转方式:push(压栈)和replace(替换)。

先看一个最简单的例子:

// 从首页跳转到播放页面
import router from '@ohos.router';

@Entry
@Component
struct HomePage {
  build() {
    Column() {
      Button('播放音乐')
        .onClick(() => {
          router.pushUrl({
            url: 'pages/PlayerPage'
          });
        })
    }
  }
}

嗯,这里要注意:url 路径是相对于 pages 目录的。我在项目中遇到过有人把路径写成了绝对路径,结果编译报错,找了半天才发现是路径格式问题。

小技巧: 建议在项目初期就统一路由路径的命名规范。比如所有页面都用「Page」结尾,这样一眼就能看出是页面文件。

4.2 页面栈管理:push与replace的区别

页面栈,你可以想象成一个「历史记录堆叠」。每次push,就像往栈顶放一张卡片;每次返回,就像从栈顶取走一张卡片。

方法 行为 页面栈变化 适用场景
pushUrl 压入新页面 栈长度+1 正常跳转,保留返回路径
replaceUrl 替换当前页面 栈长度不变 登录后跳转主页,不希望返回登录页
back 返回上一页 栈长度-1 用户点击返回按钮
clear 清空页面栈 栈长度=1 退出登录,回到首页

为什么要有replace?我举个例子。在播放器里,用户从「歌曲列表」点进「播放页面」,这时候如果用push,用户按返回键会回到列表页,这没问题。但如果是「登录页」跳「主页」,你肯定不希望用户按返回还能回到登录页吧?这时候replace就派上用场了。

核心原则: 能用push就用push,只有确定「用户不应该返回」的场景才用replace。我见过有人滥用replace,导致用户想返回上一页却回不去,体验很差。

4.3 参数传递:让页面之间「对话」

页面跳转时,经常需要带一些数据过去。比如从歌曲列表点进播放页,你得告诉播放页「要播哪首歌」。Router模块支持两种参数传递方式:paramsdata

4.3.1 使用params传递参数

// 发送方:歌曲列表页
router.pushUrl({
  url: 'pages/PlayerPage',
  params: {
    songId: '12345',
    songName: '晴天',
    artist: '周杰伦'
  }
});

// 接收方:播放页面
@Entry
@Component
struct PlayerPage {
  @State songId: string = '';
  @State songName: string = '';
  @State artist: string = '';

  aboutToAppear() {
    // 从路由参数中获取数据
    const params = router.getParams();
    if (params) {
      this.songId = params['songId'];
      this.songName = params['songName'];
      this.artist = params['artist'];
    }
  }
}

我个人习惯在 aboutToAppear 生命周期里获取参数,因为这时候页面还没渲染,数据准备好了再展示,不会出现「先空白再显示」的闪烁问题。

4.3.2 参数传递的注意事项

  • 参数类型: params支持基本类型和JSON对象,但不支持函数或复杂对象。我踩过坑,传了一个Date对象过去,结果变成了字符串。
  • 参数大小: 别传太大的数据,比如整个歌曲列表。建议只传ID,接收方再通过ID去获取详情。
  • 参数安全: 敏感信息(如token)不要通过路由参数传递,容易被拦截或泄露。
避坑指南: 我曾经在params里传了一个很大的JSON对象,结果页面跳转卡顿了好几秒。后来改成只传ID,跳转瞬间完成。记住:路由参数只传「索引」,不传「数据体」。

4.4 页面返回与数据回传

有时候,从B页面返回A页面时,需要把B页面的结果带回去。比如在播放器里,用户从「设置页」调整了播放模式,返回时需要通知「播放页」更新状态。

// B页面:设置页,返回时带数据
router.back({
  url: 'pages/PlayerPage',
  params: {
    playMode: 'shuffle'  // 改为随机播放
  }
});

// A页面:播放页,监听返回
aboutToAppear() {
  const params = router.getParams();
  if (params && params['playMode']) {
    this.playMode = params['playMode'];
    // 更新播放模式
    this.updatePlayMode();
  }
}

这里有个细节:router.back()url 参数是可选的。如果不传,默认返回上一页。但如果你传了,它会检查当前页面栈中是否有这个页面,有的话就返回到那个页面,没有的话就报错。

4.5 页面栈深度管理

在实际项目中,页面栈不能无限增长。用户点来点去,栈里堆了几十个页面,内存迟早撑不住。我建议做以下管理:

  1. 限制栈深度: 比如最多保留10个页面,超过时自动清理栈底的页面。
  2. 使用replace代替push: 对于「详情页→详情页」这种连续跳转,用replace替换当前页,避免栈无限增长。
  3. 监听页面栈变化: 通过 router.getState() 获取当前栈信息,做动态调整。
// 获取当前页面栈信息
const state = router.getState();
console.info('当前页面栈深度:', state.index + 1);
console.info('当前页面路径:', state.path);

// 如果栈深度超过10,清理栈底
if (state.index >= 10) {
  router.clear();
  router.pushUrl({
    url: 'pages/HomePage'
  });
}
个人经验: 我在做播放器时,发现用户经常在「歌单→歌曲→歌词→歌手」之间来回跳转,页面栈很容易堆到20多层。后来我加了一个「返回首页」的按钮,调用 router.clear() 一键清空,用户反馈体验好了很多。

4.6 路由拦截与权限控制

有些页面需要登录才能访问,比如「个人中心」。这时候可以在路由跳转前做拦截:

function navigateWithAuth(url: string, params?: Object) {
  const isLoggedIn = AppStorage.get<boolean>('isLoggedIn');
  
  if (!isLoggedIn) {
    // 未登录,跳转到登录页
    router.pushUrl({
      url: 'pages/LoginPage',
      params: {
        redirectUrl: url,  // 登录成功后跳回原页面
        redirectParams: params
      }
    });
    return;
  }
  
  // 已登录,正常跳转
  router.pushUrl({
    url: url,
    params: params
  });
}

嗯,这里有个小坑:AppStorage 是全局存储,但页面销毁时数据可能会丢失。我建议把登录状态同时存在本地存储里,双重保险。

4.7 总结一下

页面路由设计,说白了就是三件事:怎么跳怎么传数据怎么回来。记住几个要点:

  • push用于正常跳转,replace用于「不回头」的场景
  • 参数只传ID,不传大数据
  • 页面栈要控制深度,别让用户「回不去」也别让栈「撑爆」
  • 敏感页面加路由拦截,别让未登录用户乱闯

你想想看,如果路由设计得好,用户操作起来行云流水,代码维护也轻松。如果设计得乱,用户点几下就卡死,你还得满世界找bug。所以,花点时间把路由规划好,绝对是值得的。