1. UniApp路由系统概述
1.1 路由基础概念
UniApp采用页面栈的方式管理路由,每个页面都是栈中的一个元素。路由操作包括: - navigateTo: 保留当前页面,跳转到应用内的某个页面 - redirectTo: 关闭当前页面,跳转到应用内的某个页面 - reLaunch: 关闭所有页面,打开到应用内的某个页面 - switchTab: 跳转到tabBar页面,并关闭其他所有非tabBar页面 - navigateBack: 关闭当前页面,返回上一页面或多级页面
1.2 页面栈管理
// 获取当前页面栈
const pages = getCurrentPages()
console.log('当前页面栈长度:', pages.length)
console.log('当前页面:', pages[pages.length - 1])
// 页面栈限制
// 小程序端:最多10层
// App端:无限制
// H5端:无限制
2. 页面跳转方法详解
2.1 navigateTo - 保留式跳转
// 基础跳转
uni.navigateTo({
url: '/pages/detail/detail'
})
// 带参数跳转
uni.navigateTo({
url: '/pages/detail/detail?id=123&name=test',
success: (res) => {
console.log('跳转成功', res)
},
fail: (err) => {
console.log('跳转失败', err)
}
})
// 传递复杂参数
const params = {
id: 123,
user: {
name: 'John',
age: 25
},
list: [1, 2, 3]
}
uni.navigateTo({
url: `/pages/detail/detail?data=${encodeURIComponent(JSON.stringify(params))}`
})
2.2 redirectTo - 替换式跳转
// 关闭当前页面,跳转到新页面
uni.redirectTo({
url: '/pages/login/login'
})
// 适用场景:
// 1. 登录成功后跳转到首页
// 2. 支付完成后跳转到结果页
// 3. 表单提交后跳转到成功页
// 登录成功示例
login() {
// 登录逻辑
this.doLogin().then(() => {
uni.redirectTo({
url: '/pages/index/index'
})
})
}
2.3 reLaunch - 重启式跳转
// 关闭所有页面,打开到应用内的某个页面
uni.reLaunch({
url: '/pages/index/index'
})
// 适用场景:
// 1. 退出登录后返回首页
// 2. 应用重置
// 3. 切换用户身份
// 退出登录示例
logout() {
// 清除用户信息
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
// 重启到首页
uni.reLaunch({
url: '/pages/index/index'
})
}
2.4 switchTab - 标签页跳转
// 跳转到tabBar页面
uni.switchTab({
url: '/pages/index/index'
})
// 注意事项:
// 1. 只能跳转到pages.json中定义的tabBar页面
// 2. 不能传递参数
// 3. 会关闭所有非tabBar页面
// tabBar页面间跳转
methods: {
goToHome() {
uni.switchTab({
url: '/pages/index/index'
})
},
goToProfile() {
uni.switchTab({
url: '/pages/user/user'
})
}
}
2.5 navigateBack - 返回上级页面
// 返回上一页
uni.navigateBack()
// 返回指定层数
uni.navigateBack({
delta: 2 // 返回2层
})
// 带回调的返回
uni.navigateBack({
delta: 1,
success: () => {
console.log('返回成功')
}
})
// 条件返回
goBack() {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
// 如果是第一页,跳转到首页
uni.reLaunch({
url: '/pages/index/index'
})
}
}
3. 参数传递与接收
3.1 URL参数传递
// 发送页面
export default {
methods: {
goToDetail() {
const params = {
id: 123,
title: 'UniApp教程',
category: 'frontend'
}
// 方法1:直接拼接
const url = `/pages/detail/detail?id=${params.id}&title=${params.title}&category=${params.category}`
// 方法2:使用URLSearchParams
const searchParams = new URLSearchParams(params)
const url2 = `/pages/detail/detail?${searchParams.toString()}`
uni.navigateTo({ url })
}
}
}
// 接收页面
export default {
data() {
return {
id: '',
title: '',
category: ''
}
},
onLoad(options) {
// 接收参数
this.id = options.id
this.title = decodeURIComponent(options.title)
this.category = options.category
console.log('接收到的参数:', options)
}
}
3.2 复杂数据传递
// 发送复杂数据
export default {
methods: {
goToDetail() {
const data = {
user: {
id: 123,
name: 'John Doe',
avatar: 'https://example.com/avatar.jpg'
},
products: [
{ id: 1, name: 'Product 1' },
{ id: 2, name: 'Product 2' }
],
settings: {
theme: 'dark',
language: 'zh-CN'
}
}
// 方法1:JSON序列化
const jsonData = encodeURIComponent(JSON.stringify(data))
uni.navigateTo({
url: `/pages/detail/detail?data=${jsonData}`
})
// 方法2:使用全局数据
getApp().globalData.transferData = data
uni.navigateTo({
url: '/pages/detail/detail?hasData=true'
})
// 方法3:使用本地存储
uni.setStorageSync('transferData', data)
uni.navigateTo({
url: '/pages/detail/detail?fromStorage=true'
})
}
}
}
// 接收复杂数据
export default {
data() {
return {
pageData: null
}
},
onLoad(options) {
if (options.data) {
// 方法1:解析JSON数据
try {
this.pageData = JSON.parse(decodeURIComponent(options.data))
} catch (e) {
console.error('数据解析失败:', e)
}
} else if (options.hasData) {
// 方法2:从全局数据获取
this.pageData = getApp().globalData.transferData
// 清除全局数据
delete getApp().globalData.transferData
} else if (options.fromStorage) {
// 方法3:从本地存储获取
this.pageData = uni.getStorageSync('transferData')
// 清除存储数据
uni.removeStorageSync('transferData')
}
}
}
3.3 页面间数据回传
// 页面A - 发送页面
export default {
methods: {
goToSelect() {
uni.navigateTo({
url: '/pages/select/select'
})
}
},
onShow() {
// 监听从其他页面返回
const eventChannel = this.getOpenerEventChannel()
if (eventChannel) {
eventChannel.on('selectResult', (data) => {
console.log('接收到选择结果:', data)
this.selectedData = data
})
}
}
}
// 页面B - 选择页面
export default {
methods: {
selectItem(item) {
// 方法1:使用事件通道
const eventChannel = this.getOpenerEventChannel()
if (eventChannel) {
eventChannel.emit('selectResult', item)
}
// 方法2:使用全局事件
uni.$emit('selectResult', item)
// 返回上一页
uni.navigateBack()
}
}
}
4. 导航守卫与拦截
4.1 路由拦截器
// utils/router-guard.js
class RouterGuard {
constructor() {
this.beforeEachHooks = []
this.afterEachHooks = []
this.init()
}
init() {
// 拦截uni.navigateTo
const originalNavigateTo = uni.navigateTo
uni.navigateTo = (options) => {
return this.guard(originalNavigateTo, options)
}
// 拦截uni.redirectTo
const originalRedirectTo = uni.redirectTo
uni.redirectTo = (options) => {
return this.guard(originalRedirectTo, options)
}
// 拦截uni.reLaunch
const originalReLaunch = uni.reLaunch
uni.reLaunch = (options) => {
return this.guard(originalReLaunch, options)
}
}
guard(originalMethod, options) {
const to = this.parseUrl(options.url)
const from = this.getCurrentRoute()
// 执行前置守卫
for (const hook of this.beforeEachHooks) {
const result = hook(to, from)
if (result === false) {
return // 阻止导航
}
if (typeof result === 'string') {
options.url = result // 重定向
}
}
// 执行原始方法
const result = originalMethod(options)
// 执行后置守卫
for (const hook of this.afterEachHooks) {
hook(to, from)
}
return result
}
beforeEach(hook) {
this.beforeEachHooks.push(hook)
}
afterEach(hook) {
this.afterEachHooks.push(hook)
}
parseUrl(url) {
const [path, query] = url.split('?')
const params = {}
if (query) {
query.split('&').forEach(item => {
const [key, value] = item.split('=')
params[key] = decodeURIComponent(value)
})
}
return { path, params }
}
getCurrentRoute() {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
return {
path: '/' + currentPage.route,
params: currentPage.options
}
}
}
export default new RouterGuard()
4.2 权限控制
// main.js
import RouterGuard from '@/utils/router-guard.js'
// 登录验证
RouterGuard.beforeEach((to, from) => {
const token = uni.getStorageSync('token')
const authPages = ['/pages/user/user', '/pages/order/order']
// 需要登录的页面
if (authPages.includes(to.path) && !token) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
return '/pages/login/login'
}
// 已登录用户访问登录页
if (to.path === '/pages/login/login' && token) {
return '/pages/index/index'
}
})
// 页面访问统计
RouterGuard.afterEach((to, from) => {
console.log(`从 ${from.path} 跳转到 ${to.path}`)
// 统计页面访问
// analytics.track('page_view', { page: to.path })
})
4.3 页面缓存控制
// utils/page-cache.js
class PageCache {
constructor() {
this.cache = new Map()
this.maxSize = 10
}
set(key, data) {
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, {
data,
timestamp: Date.now()
})
}
get(key, maxAge = 5 * 60 * 1000) { // 默认5分钟过期
const item = this.cache.get(key)
if (!item) return null
if (Date.now() - item.timestamp > maxAge) {
this.cache.delete(key)
return null
}
return item.data
}
clear() {
this.cache.clear()
}
}
export default new PageCache()
5. TabBar导航
5.1 TabBar配置
// pages.json
{
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#3cc51f",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"position": "bottom",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/tab-home.png",
"selectedIconPath": "static/tab-home-current.png",
"text": "首页"
},
{
"pagePath": "pages/category/category",
"iconPath": "static/tab-category.png",
"selectedIconPath": "static/tab-category-current.png",
"text": "分类"
},
{
"pagePath": "pages/cart/cart",
"iconPath": "static/tab-cart.png",
"selectedIconPath": "static/tab-cart-current.png",
"text": "购物车"
},
{
"pagePath": "pages/user/user",
"iconPath": "static/tab-user.png",
"selectedIconPath": "static/tab-user-current.png",
"text": "我的"
}
]
}
}
5.2 动态TabBar
// 动态设置TabBar样式
export default {
onShow() {
// 设置TabBar徽标
uni.setTabBarBadge({
index: 2, // 购物车tab
text: '5'
})
// 显示红点
uni.showTabBarRedDot({
index: 3 // 我的tab
})
// 设置TabBar样式
uni.setTabBarStyle({
color: '#7A7E83',
selectedColor: '#3cc51f',
backgroundColor: '#ffffff',
borderStyle: 'black'
})
// 设置TabBar项
uni.setTabBarItem({
index: 0,
text: '首页',
iconPath: '/static/tab-home.png',
selectedIconPath: '/static/tab-home-current.png'
})
},
methods: {
updateCartBadge() {
const cartCount = this.getCartCount()
if (cartCount > 0) {
uni.setTabBarBadge({
index: 2,
text: cartCount.toString()
})
} else {
uni.removeTabBarBadge({
index: 2
})
}
}
}
}
5.3 自定义TabBar
<!-- components/custom-tabbar/custom-tabbar.vue -->
<template>
<view class="custom-tabbar" :style="{ paddingBottom: safeAreaBottom + 'px' }">
<view
class="tabbar-item"
v-for="(item, index) in tabList"
:key="index"
:class="{ active: currentIndex === index }"
@click="switchTab(item, index)"
>
<view class="item-icon">
<image
:src="currentIndex === index ? item.selectedIconPath : item.iconPath"
class="icon-image"
/>
<view class="badge" v-if="item.badge">{{ item.badge }}</view>
<view class="red-dot" v-if="item.redDot"></view>
</view>
<text class="item-text">{{ item.text }}</text>
</view>
</view>
</template>
<script>
export default {
name: 'CustomTabbar',
props: {
current: {
type: Number,
default: 0
}
},
data() {
return {
currentIndex: 0,
safeAreaBottom: 0,
tabList: [
{
pagePath: '/pages/index/index',
iconPath: '/static/tab-home.png',
selectedIconPath: '/static/tab-home-current.png',
text: '首页'
},
{
pagePath: '/pages/category/category',
iconPath: '/static/tab-category.png',
selectedIconPath: '/static/tab-category-current.png',
text: '分类'
},
{
pagePath: '/pages/cart/cart',
iconPath: '/static/tab-cart.png',
selectedIconPath: '/static/tab-cart-current.png',
text: '购物车',
badge: '5'
},
{
pagePath: '/pages/user/user',
iconPath: '/static/tab-user.png',
selectedIconPath: '/static/tab-user-current.png',
text: '我的',
redDot: true
}
]
}
},
watch: {
current: {
immediate: true,
handler(val) {
this.currentIndex = val
}
}
},
mounted() {
this.getSafeAreaBottom()
},
methods: {
switchTab(item, index) {
if (this.currentIndex === index) return
this.currentIndex = index
uni.switchTab({
url: item.pagePath
})
this.$emit('change', index)
},
getSafeAreaBottom() {
const systemInfo = uni.getSystemInfoSync()
this.safeAreaBottom = systemInfo.safeAreaInsets ? systemInfo.safeAreaInsets.bottom : 0
}
}
}
</script>
<style scoped>
.custom-tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background-color: #ffffff;
border-top: 1rpx solid #e5e5e5;
display: flex;
z-index: 1000;
}
.tabbar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
}
.item-icon {
position: relative;
margin-bottom: 4rpx;
}
.icon-image {
width: 44rpx;
height: 44rpx;
}
.badge {
position: absolute;
top: -10rpx;
right: -10rpx;
background-color: #ff4757;
color: white;
font-size: 20rpx;
padding: 2rpx 8rpx;
border-radius: 20rpx;
min-width: 32rpx;
text-align: center;
}
.red-dot {
position: absolute;
top: -6rpx;
right: -6rpx;
width: 16rpx;
height: 16rpx;
background-color: #ff4757;
border-radius: 50%;
}
.item-text {
font-size: 20rpx;
color: #7a7e83;
}
.tabbar-item.active .item-text {
color: #3cc51f;
}
</style>
6. 路由动画
6.1 页面切换动画
// 自定义页面切换动画
uni.navigateTo({
url: '/pages/detail/detail',
animationType: 'slide-in-right',
animationDuration: 300
})
// 可用动画类型:
// slide-in-right: 从右侧滑入
// slide-in-left: 从左侧滑入
// slide-in-top: 从顶部滑入
// slide-in-bottom: 从底部滑入
// fade-in: 淡入
// zoom-out: 缩放退出
// zoom-fade-out: 缩放淡出
// pop-in: 弹入
6.2 自定义转场动画
<!-- 页面转场动画组件 -->
<template>
<view class="page-transition" :class="transitionClass">
<slot></slot>
</view>
</template>
<script>
export default {
name: 'PageTransition',
props: {
type: {
type: String,
default: 'slide'
}
},
data() {
return {
transitionClass: ''
}
},
mounted() {
this.$nextTick(() => {
this.transitionClass = `transition-${this.type}-enter`
})
},
beforeDestroy() {
this.transitionClass = `transition-${this.type}-leave`
}
}
</script>
<style>
.page-transition {
transition: all 0.3s ease;
}
/* 滑动动画 */
.transition-slide-enter {
transform: translateX(100%);
animation: slideIn 0.3s ease forwards;
}
.transition-slide-leave {
animation: slideOut 0.3s ease forwards;
}
@keyframes slideIn {
to {
transform: translateX(0);
}
}
@keyframes slideOut {
to {
transform: translateX(-100%);
}
}
/* 淡入淡出动画 */
.transition-fade-enter {
opacity: 0;
animation: fadeIn 0.3s ease forwards;
}
.transition-fade-leave {
animation: fadeOut 0.3s ease forwards;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
@keyframes fadeOut {
to {
opacity: 0;
}
}
</style>
7. 路由最佳实践
7.1 路由管理器
// utils/router.js
class Router {
constructor() {
this.routes = new Map()
this.history = []
this.maxHistoryLength = 50
}
// 注册路由
register(name, path, meta = {}) {
this.routes.set(name, { path, meta })
}
// 根据名称跳转
push(name, params = {}, options = {}) {
const route = this.routes.get(name)
if (!route) {
console.error(`路由 ${name} 不存在`)
return
}
let url = route.path
if (Object.keys(params).length > 0) {
const query = new URLSearchParams(params).toString()
url += `?${query}`
}
// 记录历史
this.addHistory({ name, path: route.path, params })
return uni.navigateTo({ url, ...options })
}
// 替换当前页面
replace(name, params = {}, options = {}) {
const route = this.routes.get(name)
if (!route) {
console.error(`路由 ${name} 不存在`)
return
}
let url = route.path
if (Object.keys(params).length > 0) {
const query = new URLSearchParams(params).toString()
url += `?${query}`
}
return uni.redirectTo({ url, ...options })
}
// 返回
back(delta = 1) {
return uni.navigateBack({ delta })
}
// 重启到指定页面
reLaunch(name, params = {}) {
const route = this.routes.get(name)
if (!route) {
console.error(`路由 ${name} 不存在`)
return
}
let url = route.path
if (Object.keys(params).length > 0) {
const query = new URLSearchParams(params).toString()
url += `?${query}`
}
this.history = [] // 清空历史
return uni.reLaunch({ url })
}
// 添加历史记录
addHistory(record) {
this.history.push({
...record,
timestamp: Date.now()
})
if (this.history.length > this.maxHistoryLength) {
this.history.shift()
}
}
// 获取历史记录
getHistory() {
return this.history
}
// 清空历史记录
clearHistory() {
this.history = []
}
}
// 创建路由实例
const router = new Router()
// 注册路由
router.register('home', '/pages/index/index', { title: '首页' })
router.register('detail', '/pages/detail/detail', { title: '详情页' })
router.register('user', '/pages/user/user', { title: '个人中心', requireAuth: true })
router.register('login', '/pages/login/login', { title: '登录' })
export default router
7.2 路由使用示例
// 在页面中使用路由管理器
import router from '@/utils/router.js'
export default {
methods: {
goToDetail() {
router.push('detail', {
id: 123,
title: 'UniApp教程'
})
},
goToUser() {
router.push('user')
},
logout() {
// 清除用户信息
uni.removeStorageSync('token')
// 重启到首页
router.reLaunch('home')
}
}
}
7.3 路由性能优化
// 路由预加载
class RoutePreloader {
constructor() {
this.preloadedRoutes = new Set()
}
// 预加载页面
preload(url) {
if (this.preloadedRoutes.has(url)) {
return
}
// 预加载逻辑
this.preloadedRoutes.add(url)
// 可以在这里预加载页面数据
this.preloadPageData(url)
}
// 预加载页面数据
async preloadPageData(url) {
try {
// 根据URL判断需要预加载的数据
if (url.includes('/pages/detail/detail')) {
// 预加载详情页数据
await this.preloadDetailData()
}
} catch (error) {
console.error('预加载失败:', error)
}
}
async preloadDetailData() {
// 预加载详情页相关数据
// const data = await api.getDetailData()
// 缓存数据
}
}
export default new RoutePreloader()
8. 总结
本章详细介绍了UniApp中的路由与导航系统:
- 路由基础:掌握了UniApp路由系统的基本概念和页面栈管理
- 页面跳转:学习了各种页面跳转方法的使用场景和注意事项
- 参数传递:了解了简单参数和复杂数据的传递方法
- 导航守卫:实现了路由拦截和权限控制
- TabBar导航:掌握了TabBar的配置和自定义实现
- 路由动画:学习了页面切换动画的实现
- 最佳实践:了解了路由管理器的设计和性能优化方法
良好的路由设计是用户体验的重要组成部分,下一章我们将学习数据绑定与事件处理。