uni-app 路由管理

章节介绍

路由管理是单页应用开发中的重要组成部分,它负责管理页面之间的导航和跳转。uni-app 提供了一套完整的路由管理机制,基于 pages.json 配置文件和内置的导航 API,实现了跨平台的路由功能。本章节将详细介绍 uni-app 中的路由管理方法,包括路由配置、导航守卫、路由参数传递等核心知识点,以及如何实现权限控制路由,帮助你掌握 uni-app 应用中的路由管理技术。

核心知识点讲解

1. 路由配置

uni-app 的路由配置主要通过 pages.json 文件实现,该文件定义了应用的页面结构、导航栏样式、底部标签栏等。

基本路由配置

{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "首页"
      }
    },
    {
      "path": "pages/login/login",
      "style": {
        "navigationBarTitleText": "登录"
      }
    },
    {
      "path": "pages/detail/detail",
      "style": {
        "navigationBarTitleText": "详情页"
      }
    }
  ],
  "tabBar": {
    "color": "#999",
    "selectedColor": "#007AFF",
    "backgroundColor": "#fff",
    "borderStyle": "black",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "static/tabbar/home.png",
        "selectedIconPath": "static/tabbar/home-active.png"
      },
      {
        "pagePath": "pages/mine/mine",
        "text": "我的",
        "iconPath": "static/tabbar/mine.png",
        "selectedIconPath": "static/tabbar/mine-active.png"
      }
    ]
  }
}

路由样式配置

{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "首页",
        "navigationBarBackgroundColor": "#007AFF",
        "navigationBarTextStyle": "white",
        "enablePullDownRefresh": true,
        "backgroundColor": "#f5f5f5",
        "backgroundTextStyle": "dark"
      }
    }
  ]
}

全局样式配置

{
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "uni-app",
    "navigationBarBackgroundColor": "#F8F8F8",
    "backgroundColor": "#F8F8F8"
  },
  "pages": [
    // 页面配置
  ]
}

2. 导航 API

uni-app 提供了多种导航 API,用于实现页面之间的跳转和返回。

1. uni.navigateTo

保留当前页面,跳转到应用内的某个页面,使用 uni.navigateBack 可以返回到原页面。

uni.navigateTo({
  url: '/pages/detail/detail?id=1&name=test',
  success: function(res) {
    console.log('跳转成功');
  },
  fail: function(err) {
    console.error('跳转失败:', err);
  }
});

2. uni.redirectTo

关闭当前页面,跳转到应用内的某个页面。

uni.redirectTo({
  url: '/pages/login/login',
  success: function(res) {
    console.log('跳转成功');
  }
});

3. uni.reLaunch

关闭所有页面,打开到应用内的某个页面。

uni.reLaunch({
  url: '/pages/index/index',
  success: function(res) {
    console.log('跳转成功');
  }
});

4. uni.switchTab

跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。

uni.switchTab({
  url: '/pages/index/index',
  success: function(res) {
    console.log('跳转成功');
  }
});

5. uni.navigateBack

关闭当前页面,返回上一页面或多级页面。

// 返回上一页
uni.navigateBack({
  delta: 1
});

// 返回上两页
uni.navigateBack({
  delta: 2
});

3. 路由参数传递

1. 通过 URL 参数传递

// 跳转时传递参数
uni.navigateTo({
  url: '/pages/detail/detail?id=1&name=test'
});

// 接收参数(在 detail 页面)
export default {
  onLoad(options) {
    console.log(options.id); // 1
    console.log(options.name); // test
  }
};

2. 通过事件通道传递

对于复杂数据的传递,可以使用事件通道(EventChannel)。

// 跳转时传递事件通道
uni.navigateTo({
  url: '/pages/detail/detail',
  events: {
    // 监听来自 detail 页面的事件
    receiveDataFromDetail: function(data) {
      console.log('接收到来自详情页的数据:', data);
    }
  },
  success: function(res) {
    // 向 detail 页面发送数据
    res.eventChannel.emit('sendDataToDetail', {
      data: '来自首页的数据'
    });
  }
});

// 接收事件通道(在 detail 页面)
export default {
  onLoad() {
    const eventChannel = this.getOpenerEventChannel();
    
    // 监听来自首页的事件
    eventChannel.on('sendDataToDetail', function(data) {
      console.log('接收到来自首页的数据:', data);
    });
    
    // 向首页发送数据
    eventChannel.emit('receiveDataFromDetail', {
      data: '来自详情页的数据'
    });
  }
};

3. 通过全局状态管理传递

对于需要在多个页面共享的数据,可以使用 Vuex 等状态管理工具。

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    userInfo: null
  },
  mutations: {
    setUserInfo(state, userInfo) {
      state.userInfo = userInfo
    }
  },
  actions: {
    login({ commit }, userInfo) {
      commit('setUserInfo', userInfo)
    }
  }
})

// 登录页面
this.$store.dispatch('login', userInfo)

// 其他页面
const userInfo = this.$store.state.userInfo

4. 导航守卫

uni-app 提供了全局导航守卫,用于控制页面的访问权限和跳转逻辑。

全局前置守卫

main.js 中配置全局前置守卫:

// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

App.mpType = 'app'

const app = new Vue({
  ...App,
  store
})

// 全局前置守卫
uni.addInterceptor('navigateTo', {
  invoke(e) {
    console.log('即将跳转到:', e.url);
    // 可以在这里进行权限判断
  },
  success(e) {
    console.log('跳转成功');
  },
  fail(e) {
    console.log('跳转失败:', e);
  }
});

app.$mount()

页面级守卫

在页面的生命周期函数中实现页面级守卫:

export default {
  onLoad() {
    // 页面加载时的逻辑
  },
  onShow() {
    // 页面显示时的逻辑
  },
  onHide() {
    // 页面隐藏时的逻辑
  },
  onUnload() {
    // 页面卸载时的逻辑
  }
};

5. 路由动画

uni-app 支持配置页面跳转的动画效果,提升用户体验。

全局路由动画

{
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "uni-app",
    "navigationBarBackgroundColor": "#F8F8F8",
    "backgroundColor": "#F8F8F8",
    "app-plus": {
      "animationType": "fade-in",
      "animationDuration": 300
    }
  }
}

页面级路由动画

{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "首页",
        "app-plus": {
          "animationType": "slide-in-right",
          "animationDuration": 300
        }
      }
    }
  ]
}

实用案例分析

案例:实现权限控制路由

1. 路由配置

{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "首页"
      }
    },
    {
      "path": "pages/login/login",
      "style": {
        "navigationBarTitleText": "登录"
      }
    },
    {
      "path": "pages/profile/profile",
      "style": {
        "navigationBarTitleText": "个人中心"
      }
    },
    {
      "path": "pages/settings/settings",
      "style": {
        "navigationBarTitleText": "设置"
      }
    }
  ],
  "tabBar": {
    "color": "#999",
    "selectedColor": "#007AFF",
    "backgroundColor": "#fff",
    "borderStyle": "black",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "static/tabbar/home.png",
        "selectedIconPath": "static/tabbar/home-active.png"
      },
      {
        "pagePath": "pages/profile/profile",
        "text": "我的",
        "iconPath": "static/tabbar/profile.png",
        "selectedIconPath": "static/tabbar/profile-active.png"
      }
    ]
  }
}

2. 全局导航守卫实现

// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

App.mpType = 'app'

const app = new Vue({
  ...App,
  store
})

// 需要登录权限的页面
const needAuthPages = [
  '/pages/profile/profile',
  '/pages/settings/settings'
];

// 全局前置守卫
uni.addInterceptor('navigateTo', {
  invoke(e) {
    const url = e.url;
    const path = url.split('?')[0];
    
    // 检查是否需要登录权限
    if (needAuthPages.includes(path)) {
      const isLoggedIn = store.state.user.isLoggedIn;
      
      if (!isLoggedIn) {
        // 未登录,跳转到登录页面
        uni.navigateTo({
          url: '/pages/login/login?redirect=' + encodeURIComponent(url)
        });
        
        // 阻止原跳转
        return false;
      }
    }
  }
});

// 对 switchTab 也添加拦截
uni.addInterceptor('switchTab', {
  invoke(e) {
    const url = e.url;
    
    // 检查是否需要登录权限
    if (needAuthPages.includes(url)) {
      const isLoggedIn = store.state.user.isLoggedIn;
      
      if (!isLoggedIn) {
        // 未登录,跳转到登录页面
        uni.navigateTo({
          url: '/pages/login/login?redirect=' + encodeURIComponent(url)
        });
        
        // 阻止原跳转
        return false;
      }
    }
  }
});

app.$mount()

3. 登录页面实现

<!-- pages/login/login.vue -->
<template>
  <view class="container">
    <view class="login-form">
      <text class="title">登录</text>
      
      <view class="form-item">
        <input 
          type="text" 
          v-model="userInfo.username" 
          placeholder="请输入用户名"
          class="input"
        />
      </view>
      
      <view class="form-item">
        <input 
          type="password" 
          v-model="userInfo.password" 
          placeholder="请输入密码"
          class="input"
        />
      </view>
      
      <text v-if="error" class="error-message">{{ error }}</text>
      
      <button 
        @click="handleLogin" 
        :disabled="loading"
        class="login-button"
      >
        {{ loading ? '登录中...' : '登录' }}
      </button>
    </view>
  </view>
</template>

<script>
import { mapState, mapActions } from 'vuex'

export default {
  data() {
    return {
      userInfo: {
        username: '',
        password: ''
      },
      redirectUrl: ''
    }
  },
  computed: {
    ...mapState('user', ['loading', 'error', 'isLoggedIn'])
  },
  onLoad(options) {
    // 获取重定向 URL
    if (options.redirect) {
      this.redirectUrl = decodeURIComponent(options.redirect);
    }
  },
  watch: {
    // 监听认证状态变化,登录成功后跳转到原页面或首页
    isAuthenticated(newVal) {
      if (newVal) {
        if (this.redirectUrl) {
          // 跳转到原页面
          if (this.redirectUrl.includes('/pages/tabbar/')) {
            uni.switchTab({
              url: this.redirectUrl
            });
          } else {
            uni.navigateTo({
              url: this.redirectUrl
            });
          }
        } else {
          // 跳转到首页
          uni.switchTab({
            url: '/pages/index/index'
          });
        }
      }
    }
  },
  methods: {
    ...mapActions('user', ['login']),
    
    handleLogin() {
      if (!this.userInfo.username || !this.userInfo.password) {
        uni.showToast({
          title: '请输入用户名和密码',
          icon: 'none'
        });
        return;
      }
      
      this.login(this.userInfo)
        .catch(error => {
          console.error('Login failed:', error);
        });
    }
  }
}
</script>

<style scoped>
.container {
  flex: 1;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 40rpx;
  background-color: #f5f5f5;
}

.login-form {
  width: 100%;
  max-width: 500rpx;
  background-color: #fff;
  padding: 60rpx;
  border-radius: 20rpx;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}

.title {
  font-size: 36rpx;
  font-weight: bold;
  text-align: center;
  margin-bottom: 60rpx;
  color: #333;
}

.form-item {
  margin-bottom: 40rpx;
}

.input {
  width: 100%;
  height: 80rpx;
  border: 2rpx solid #e0e0e0;
  border-radius: 10rpx;
  padding: 0 20rpx;
  font-size: 28rpx;
}

.error-message {
  color: #ff4d4f;
  font-size: 24rpx;
  margin-bottom: 30rpx;
  display: block;
}

.login-button {
  width: 100%;
  height: 80rpx;
  background-color: #409eff;
  color: #fff;
  font-size: 28rpx;
  font-weight: bold;
  border-radius: 10rpx;
  margin-top: 20rpx;
}

.login-button:disabled {
  background-color: #c0c4cc;
}
</style>

4. 个人中心页面实现

<!-- pages/profile/profile.vue -->
<template>
  <view class="container">
    <view class="user-info">
      <image :src="userInfo.avatar || '/static/avatar/default.png'" class="avatar"></image>
      <text class="username">{{ userInfo.username || '用户' }}</text>
    </view>
    
    <view class="menu-list">
      <view class="menu-item" @click="navigateToSettings">
        <text class="menu-text">设置</text>
        <text class="menu-arrow">></text>
      </view>
      <view class="menu-item" @click="handleLogout">
        <text class="menu-text">退出登录</text>
        <text class="menu-arrow">></text>
      </view>
    </view>
  </view>
</template>

<script>
import { mapState, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState('user', ['userInfo', 'isLoggedIn'])
  },
  onLoad() {
    // 检查登录状态
    if (!this.isLoggedIn) {
      uni.navigateTo({
        url: '/pages/login/login?redirect=/pages/profile/profile'
      });
    }
  },
  methods: {
    ...mapActions('user', ['logout']),
    
    navigateToSettings() {
      uni.navigateTo({
        url: '/pages/settings/settings'
      });
    },
    
    handleLogout() {
      uni.showModal({
        title: '退出登录',
        content: '确定要退出登录吗?',
        success: (res) => {
          if (res.confirm) {
            this.logout()
              .then(() => {
                uni.switchTab({
                  url: '/pages/index/index'
                });
              })
              .catch(error => {
                console.error('Logout failed:', error);
              });
          }
        }
      });
    }
  }
}
</script>

<style scoped>
.container {
  flex: 1;
  padding: 40rpx;
  background-color: #f5f5f5;
}

.user-info {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 40rpx;
  background-color: #fff;
  border-radius: 20rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}

.avatar {
  width: 160rpx;
  height: 160rpx;
  border-radius: 50%;
  margin-bottom: 20rpx;
}

.username {
  font-size: 32rpx;
  font-weight: bold;
  color: #333;
}

.menu-list {
  background-color: #fff;
  border-radius: 20rpx;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}

.menu-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 30rpx 40rpx;
  border-bottom: 1rpx solid #f0f0f0;
}

.menu-item:last-child {
  border-bottom: none;
}

.menu-text {
  font-size: 28rpx;
  color: #333;
}

.menu-arrow {
  font-size: 24rpx;
  color: #999;
}
</style>

常见问题与解决方案

1. 路由跳转失败

问题:调用导航 API 时跳转失败,可能的原因包括路径错误、页面不存在、权限不足等。

解决方案

  • 检查 pages.json 中是否正确配置了页面路径
  • 确保页面文件存在且路径正确
  • 检查是否有导航守卫阻止了跳转
  • 对于需要权限的页面,确保用户已登录

2. 路由参数传递失败

问题:页面跳转时参数传递失败,可能的原因包括参数格式错误、参数过大、参数包含特殊字符等。

解决方案

  • 对于简单参数,使用 URL 查询参数传递
  • 对于复杂参数,使用事件通道或全局状态管理传递
  • 对于包含特殊字符的参数,使用 encodeURIComponent 编码
  • 对于大型数据,避免使用 URL 参数传递,改用其他方式

3. 导航守卫不生效

问题:配置的导航守卫不生效,可能的原因包括拦截器配置错误、拦截器返回值处理不当等。

解决方案

  • 确保正确配置了导航拦截器
  • 对于需要阻止跳转的情况,返回 false
  • 对不同的导航 API(navigateTo、switchTab 等)分别添加拦截器
  • 检查拦截器的执行顺序和逻辑

4. 页面栈溢出

问题:频繁使用 navigateTo 跳转页面,导致页面栈溢出。

解决方案

  • 合理使用不同的导航 API,对于不需要返回的页面使用 redirectTo
  • 对于需要重置页面栈的场景使用 reLaunch
  • 定期使用 navigateBack 关闭不需要的页面
  • 监控页面栈的深度,避免过度跳转

代码优化建议

1. 路由配置模块化

// 优化前:单一 pages.json 文件
{
  "pages": [
    // 大量页面配置
  ]
}

// 优化后:使用配置文件拆分
// config/routes.js
export const pages = [
  {
    path: "pages/index/index",
    style: {
      navigationBarTitleText: "首页"
    }
  },
  // 其他页面配置
];

export const tabBar = {
  color: "#999",
  selectedColor: "#007AFF",
  backgroundColor: "#fff",
  borderStyle: "black",
  list: [
    // 标签栏配置
  ]
};

// 然后通过构建工具生成 pages.json

2. 导航工具函数封装

// 优化前:直接调用导航 API
uni.navigateTo({ url: '/pages/detail/detail?id=1' });

// 优化后:封装导航工具函数
// utils/navigation.js
export const navigateTo = (url, params = {}) => {
  // 构建带参数的 URL
  const queryString = Object.keys(params)
    .map(key => `${key}=${encodeURIComponent(params[key])}`)
    .join('&');
  
  const fullUrl = queryString ? `${url}?${queryString}` : url;
  
  uni.navigateTo({ url: fullUrl });
};

export const navigateToWithAuth = (url, params = {}) => {
  // 检查登录状态
  const isLoggedIn = store.state.user.isLoggedIn;
  
  if (!isLoggedIn) {
    uni.navigateTo({
      url: `/pages/login/login?redirect=${encodeURIComponent(url)}`
    });
    return;
  }
  
  navigateTo(url, params);
};

// 使用
import { navigateTo, navigateToWithAuth } from '@/utils/navigation';

navigateTo('/pages/detail/detail', { id: 1, name: 'test' });
navigateToWithAuth('/pages/profile/profile');

3. 路由参数解析优化

// 优化前:直接使用 onLoad 中的 options
onLoad(options) {
  const id = options.id;
  const name = options.name;
  // 处理参数
}

// 优化后:封装参数解析工具
// utils/params.js
export const parseParams = (options) => {
  const params = {};
  
  Object.keys(options).forEach(key => {
    let value = options[key];
    
    // 尝试解析 JSON 字符串
    try {
      value = JSON.parse(value);
    } catch (e) {
      // 不是 JSON 字符串,保持原值
    }
    
    params[key] = value;
  });
  
  return params;
};

// 使用
import { parseParams } from '@/utils/params';

onLoad(options) {
  const params = parseParams(options);
  const id = params.id;
  const name = params.name;
  // 处理参数
}

4. 导航守卫集中管理

// 优化前:分散的导航守卫
uni.addInterceptor('navigateTo', {
  invoke(e) {
    // 处理逻辑
  }
});

uni.addInterceptor('switchTab', {
  invoke(e) {
    // 处理逻辑
  }
});

// 优化后:集中管理导航守卫
// utils/routeGuard.js
import store from '@/store';

// 需要登录权限的页面
const needAuthPages = [
  '/pages/profile/profile',
  '/pages/settings/settings'
];

// 检查权限
const checkAuth = (url) => {
  const path = url.split('?')[0];
  
  if (needAuthPages.includes(path)) {
    const isLoggedIn = store.state.user.isLoggedIn;
    
    if (!isLoggedIn) {
      uni.navigateTo({
        url: '/pages/login/login?redirect=' + encodeURIComponent(url)
      });
      return false;
    }
  }
  
  return true;
};

// 注册导航守卫
export const registerRouteGuards = () => {
  // 对 navigateTo 添加拦截
  uni.addInterceptor('navigateTo', {
    invoke(e) {
      return checkAuth(e.url);
    }
  });
  
  // 对 switchTab 添加拦截
  uni.addInterceptor('switchTab', {
    invoke(e) {
      return checkAuth(e.url);
    }
  });
  
  // 对 redirectTo 添加拦截
  uni.addInterceptor('redirectTo', {
    invoke(e) {
      return checkAuth(e.url);
    }
  });
};

// 使用
// main.js
import { registerRouteGuards } from '@/utils/routeGuard';

// 注册导航守卫
registerRouteGuards();

章节总结

本章节详细介绍了 uni-app 中的路由管理方法,包括:

  1. 路由配置:通过 pages.json 文件配置应用的页面结构、导航栏样式和底部标签栏
  2. 导航 API:使用 uni.navigateTouni.redirectTouni.reLaunchuni.switchTabuni.navigateBack 等 API 实现页面跳转
  3. 路由参数传递:通过 URL 参数、事件通道和全局状态管理传递参数
  4. 导航守卫:使用全局拦截器和页面生命周期函数实现路由守卫
  5. 路由动画:配置页面跳转的动画效果,提升用户体验
  6. 实用案例:通过实现权限控制路由,展示了路由管理在实际应用中的使用
  7. 常见问题与解决方案:针对路由跳转失败、参数传递失败、导航守卫不生效和页面栈溢出等问题提供了解决方案
  8. 代码优化建议:提供了路由配置模块化、导航工具函数封装、路由参数解析优化和导航守卫集中管理的优化建议

通过本章节的学习,你应该能够掌握 uni-app 中的路由管理方法,有效地配置和使用路由功能,实现页面之间的流畅跳转和权限控制。在实际开发中,应根据应用的具体需求,合理选择和使用不同的路由管理策略,确保应用的导航体验流畅、可靠。

« 上一篇 uni-app 状态管理 下一篇 » uni-app 网络请求封装