4、路由架构:安装Vue Router,设计模块化路由配置,实现动态路由加载
路由,说白了就是单页应用的「导航地图」。没有它,你的页面就是一潭死水,点哪都跳不动。我个人习惯在项目搭建初期就把路由架构想清楚,不然后面改起来,那叫一个痛苦。
我记得刚带团队那会儿,有个同事把所有路由都塞在一个文件里,足足两千行。每次加个页面都得翻半天,还经常合并冲突。嗯,这就是典型的「能用就行」心态,但维护成本太高了。
4.1 安装与基础配置
先装包,这个没啥好说的:
npm install vue-router@4
装完之后,在 src 下建个 router 文件夹。我习惯把路由相关的逻辑都放这里,别散落在各处。
创建 src/router/index.js:
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
createWebHistory 而不是 hash 模式。虽然 hash 模式兼容性更好,但 URL 里带个 # 实在太丑了。生产环境配好 Nginx 就行。
然后在 main.js 里挂上:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
4.2 模块化路由设计
项目一大,路由肯定不能全写在一个文件里。我见过最夸张的项目,路由文件里还混着权限判断、页面标题、缓存配置……简直是个大杂烩。
我的做法是按业务模块拆分。比如一个后台管理系统,可以拆成这样:
src/router/
├── index.js # 主入口,合并所有模块
├── modules/
│ ├── user.js # 用户管理模块
│ ├── order.js # 订单模块
│ ├── product.js # 商品模块
│ └── dashboard.js # 仪表盘模块
└── dynamic.js # 动态路由逻辑
每个模块文件长这样:
// src/router/modules/user.js
export default [
{
path: '/user',
name: 'User',
component: () => import('@/views/user/UserList.vue'),
meta: { title: '用户管理', icon: 'user' }
},
{
path: '/user/:id',
name: 'UserDetail',
component: () => import('@/views/user/UserDetail.vue'),
meta: { title: '用户详情', hidden: true }
}
]
然后在 index.js 里合并:
import userRoutes from './modules/user'
import orderRoutes from './modules/order'
import productRoutes from './modules/product'
import dashboardRoutes from './modules/dashboard'
const routes = [
...dashboardRoutes,
...userRoutes,
...orderRoutes,
...productRoutes
]
4.3 动态路由加载
动态路由,说白了就是根据用户权限来「组装」路由。比如管理员能看到「系统设置」,普通用户就看不到。
我曾经踩过一个坑:把动态路由和静态路由混在一起初始化,结果用户还没登录,路由表里就已经有了「系统设置」的路径。虽然前端做了权限拦截,但 URL 直接访问还是能跳过去……嗯,这就是典型的「前端防君子不防小人」。
正确的做法是:
- 初始化时只加载公共路由(登录页、404等)
- 用户登录后,根据角色获取可访问的路由配置
- 用
router.addRoute()动态添加
来看代码:
// src/router/dynamic.js
import store from '@/store'
// 根据角色获取路由
export function getAsyncRoutes(role) {
// 这里可以从后端获取,也可以本地配置
const allRoutes = {
admin: [
{ path: '/settings', component: () => import('@/views/Settings.vue') },
{ path: '/logs', component: () => import('@/views/Logs.vue') }
],
editor: [
{ path: '/editor', component: () => import('@/views/Editor.vue') }
]
}
return allRoutes[role] || []
}
// 动态添加路由
export function addDynamicRoutes(role) {
const routes = getAsyncRoutes(role)
routes.forEach(route => {
router.addRoute(route)
})
}
在登录成功后调用:
// 登录成功后的逻辑
async function handleLogin(role) {
await store.dispatch('user/login')
addDynamicRoutes(role)
// 跳转到首页
router.push('/')
}
App.vue 的 onMounted 里重新加载。我一般会在路由守卫里做这件事,每次跳转前检查一下路由表是否完整。
4.4 路由守卫与权限控制
路由守卫,就是「门卫」。每次跳转前,它都会问一句:「你谁啊?要去哪?有票吗?」
我习惯在 router/index.js 里统一配置:
router.beforeEach(async (to, from, next) => {
// 设置页面标题
document.title = to.meta.title || '默认标题'
// 判断是否需要登录
if (to.meta.requiresAuth) {
const token = localStorage.getItem('token')
if (!token) {
next({ name: 'Login', query: { redirect: to.fullPath } })
return
}
}
// 判断角色权限
if (to.meta.roles) {
const userRole = store.state.user.role
if (!to.meta.roles.includes(userRole)) {
next({ name: '403' })
return
}
}
next()
})
beforeResolve 里,用户体验好多了。说白了,beforeEach 只做同步判断,异步操作往后放。
4.5 路由元信息的最佳实践
meta 字段是个好东西,但别乱用。我见过有人把接口地址、组件配置、甚至 CSS 样式都塞进去……那还不如直接写个配置文件。
我常用的 meta 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
title |
String | 页面标题,用于浏览器标签和面包屑 |
icon |
String | 菜单图标,侧边栏用 |
hidden |
Boolean | 是否在菜单中隐藏(比如详情页) |
requiresAuth |
Boolean | 是否需要登录 |
roles |
Array | 允许访问的角色列表 |
keepAlive |
Boolean | 是否缓存页面(配合 <keep-alive>) |
举个例子:
{
path: '/order/detail/:id',
name: 'OrderDetail',
component: () => import('@/views/order/OrderDetail.vue'),
meta: {
title: '订单详情',
hidden: true, // 不在菜单显示
requiresAuth: true, // 需要登录
roles: ['admin', 'editor'], // 只有这两种角色能看
keepAlive: false // 每次进入都重新加载
}
}
嗯,这一章就到这。下一章我们聊聊状态管理,Vuex 和 Pinia 到底怎么选,以及如何设计一个可扩展的 Store 架构。