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中的路由与导航系统:

  1. 路由基础:掌握了UniApp路由系统的基本概念和页面栈管理
  2. 页面跳转:学习了各种页面跳转方法的使用场景和注意事项
  3. 参数传递:了解了简单参数和复杂数据的传递方法
  4. 导航守卫:实现了路由拦截和权限控制
  5. TabBar导航:掌握了TabBar的配置和自定义实现
  6. 路由动画:学习了页面切换动画的实现
  7. 最佳实践:了解了路由管理器的设计和性能优化方法

良好的路由设计是用户体验的重要组成部分,下一章我们将学习数据绑定与事件处理。