Chapter 05

路由与页面跳转

掌握 uni-app 路由 API 的差异与适用场景,实现鉴权拦截与页面栈精确控制

5.1 uni-app 路由系统概述

uni-app 不使用 Vue Router,而是有自己的路由体系。路由由 pages.json 中的 pages 数组集中管理,所有页面必须先在此注册,才能被访问。路由跳转通过 uni 全局对象的一系列 API 完成。

页面栈的概念

uni-app 维护一个页面栈,记录历史访问过的页面。栈的最顶层是当前显示的页面。不同的跳转方式对页面栈的影响不同,这直接决定了用户能否"返回"到上一页。小程序的页面栈深度最大为 10 层,超出后将无法继续 navigateTo。

5.2 路由 API 详解

navigateTo — 普通跳转(保留当前页)

// 基本用法
uni.navigateTo({
  url: '/pages/detail/detail?id=123&type=goods'
})

// 使用 Promise 封装(推荐)
try {
  await uni.navigateTo({ url: '/pages/detail/detail?id=123' })
} catch (err) {
  console.error('跳转失败:', err)
}

// 在目标页面接收参数(onLoad 的 query 对象)
// pages/detail/detail.vue
onLoad((query) => {
  const id = query?.id    // '123'(注意是字符串类型)
  const type = query?.type // 'goods'
})
URL 参数的类型注意

通过 URL 传递的参数全部是字符串类型,包括数字和布尔值。如果需要传递 id=123(数字),在目标页面接收到的是字符串 "123",需要手动转换 Number(query.id)。复杂对象参数需要 JSON.stringify + JSON.parse 处理。

redirectTo — 重定向(关闭当前页)

// 关闭当前页面并跳转,用户无法通过返回键回到当前页
// 典型场景:登录成功后跳转首页、引导页跳转主页
uni.redirectTo({
  url: '/pages/index/index'
})

reLaunch — 重启(清空所有页面栈)

// 关闭所有页面,打开目标页面(相当于重新启动到该页面)
// 典型场景:退出登录后回到登录页(确保用户无法返回需要登录的页面)
uni.reLaunch({
  url: '/pages/login/login'
})

switchTab — 切换 tabBar 页面

// 跳转到 tabBar 页面,并关闭所有非 tabBar 页面
// 注意:tabBar 页面只能用此 API 跳转,不能用 navigateTo
uni.switchTab({
  url: '/pages/index/index'  // 不能带参数!
})

navigateBack — 返回上一页

// 返回上一页(默认返回 1 层)
uni.navigateBack()

// 返回多层(delta 为返回的层数)
uni.navigateBack({ delta: 2 })

// 获取当前页面栈(调试用)
const pages = getCurrentPages()
console.log('当前页面栈深度:', pages.length)
const currentPage = pages[pages.length - 1]
console.log('当前页面路径:', currentPage.route)

路由 API 对比总结

API当前页面页面栈适用场景
navigateTo保留入栈(+1)普通跳转详情页
redirectTo关闭替换栈顶登录成功跳转
reLaunch关闭清空重建退出登录
switchTab关闭非tab页切换根页面切换底部导航
navigateBack关闭出栈(-delta)返回上一页

5.3 页面间通信

页面之间需要传递数据时,有多种方案可选:

方案一:URL 参数(简单数据)

// A 页面跳转,传简单参数
uni.navigateTo({ url: '/pages/b/b?id=42&name=苹果' })

// B 页面接收
onLoad((query) => {
  const id = Number(query?.id)  // 42
  const name = decodeURIComponent(query?.name ?? '')  // '苹果'
})

方案二:EventChannel(大数据 / 对象,推荐)

EventChannel 是 uni-app 提供的页面间通信通道,可以传递任意数据类型:

// A 页面跳转时创建事件通道
uni.navigateTo({
  url: '/pages/b/b',
  events: {
    // 监听 B 页面向 A 页面发送的事件
    selectItem(data: { item: Product }) {
      console.log('B 页面选中了:', data.item)
      selectedProduct.value = data.item
    }
  },
  success(res) {
    // A 向 B 发送初始数据
    res.eventChannel.emit('initData', { userId: currentUser.value.id })
  }
})

// B 页面接收数据
onLoad(() => {
  const eventChannel = getCurrentPages()
    .at(-1)?.getOpenerEventChannel()

  // 监听 A 发来的初始数据
  eventChannel?.on('initData', (data: { userId: number }) => {
    console.log('收到用户 ID:', data.userId)
  })
})

// B 页面向 A 发送选中结果(用户选完商品)
function confirmSelect(item: Product) {
  const eventChannel = getCurrentPages().at(-1)?.getOpenerEventChannel()
  eventChannel?.emit('selectItem', { item })
  uni.navigateBack()
}

方案三:全局状态(Pinia)

对于需要在多个页面共享的状态(用户信息、购物车等),使用 Pinia store 是最清晰的方案。详见第 6 章。

5.4 路由拦截器实现

uni-app 没有内置的路由守卫(不同于 Vue Router 的 beforeEach),但可以通过重写路由 API 实现拦截逻辑:

// utils/router.ts — 带鉴权的路由工具
import { useUserStore } from '@/stores/user'

// 不需要登录即可访问的页面白名单
const WHITE_LIST = [
  '/pages/login/login',
  '/pages/index/index',
  '/pages/register/register'
]

function checkAuth(url: string): boolean {
  const path = url.split('?')[0]
  if (WHITE_LIST.includes(path)) return true

  const userStore = useUserStore()
  return !!userStore.token
}

export function navigateTo(url: string, options?: UniApp.NavigateToOptions) {
  if (!checkAuth(url)) {
    // 未登录,跳转登录页,并记录原始目标
    const redirectUrl = encodeURIComponent(url)
    uni.navigateTo({
      url: `/pages/login/login?redirect=${redirectUrl}`
    })
    return
  }
  uni.navigateTo({ url, ...options })
}

// 登录成功后跳回原始页面
export function afterLogin(query: Record<string, string>) {
  const redirect = query?.redirect
  if (redirect) {
    uni.redirectTo({ url: decodeURIComponent(redirect) })
  } else {
    uni.switchTab({ url: '/pages/index/index' })
  }
}
第三方路由拦截方案

如果需要更完整的路由守卫能力,可以考虑 uni-routeruniapp-router 等社区库,它们参考了 Vue Router 的 API 设计,提供 beforeEach / afterEach 等钩子。

5.5 tabBar 高级配置

自定义 tabBar 组件

默认的 tabBar 样式有限,可以用自定义 tabBar 实现更丰富的视觉效果:

// pages.json
{
  "tabBar": {
    "custom": true,           // 开启自定义 tabBar
    "color": "#78716c",
    "selectedColor": "#0ea5e9",
    "list": [
      { "pagePath": "pages/index/index", "text": "首页" },
      { "pagePath": "pages/user/user", "text": "我的" }
    ]
  }
}
<!-- custom-tab-bar/index.vue(必须在这个固定路径)-->
<template>
  <view class="tab-bar">
    <view
      v-for="(item, index) in tabs"
      :key="index"
      class="tab-item"
      :class="{ active: currentIndex === index }"
      @tap="switchTo(index, item.pagePath)"
    >
      <image :src="currentIndex === index ? item.activeIcon : item.icon" />
      <text>{{ item.text }}</text>
    </view>
  </view>
</template>

5.6 页面栈管理实战

某些场景需要对页面栈进行精确操作。例如:用户在 A → B → C 流程后,希望直接跳回 A 而非逐页返回:

// 方案一:reLaunch(最彻底,但会重新加载 A 页面)
uni.reLaunch({ url: '/pages/a/a' })

// 方案二:精确计算 delta 值
const pages = getCurrentPages()
const targetIndex = pages.findIndex(p => p.route === 'pages/a/a')
if (targetIndex !== -1) {
  const delta = pages.length - 1 - targetIndex
  uni.navigateBack({ delta })
}

// 向指定历史页面传数据(通过全局事件或 store)
const prevPage = pages[pages.length - 2]  // 上一页
// 直接调用上一页的方法(仅 App/H5,小程序限制严格)
const prevVm = prevPage?.$vm as any
prevVm?.refreshData?.()
uni.navigateBack()

5.7 小结