讲解
单页应用只有一张 HTML 页面,「多页面」体验靠路由实现:URL 变化时切换显示的组件,同时保持应用状态不丢。Vue Router 是官方路由库,核心概念三个:路由表(path 到组件的映射)、router-view(组件的渲染出口)、router-link(声明式导航,代替 a 标签避免整页刷新)。
路由表的常见形态:静态路径 { path: '/about', component: About }、动态参数 { path: '/user/:id', component: User }(组件内用 useRoute().params.id 取参)、嵌套路由(children 配父子层级,父组件里再放一个 router-view 做子出口)、404 兜底({ path: '/:pathMatch(.*)*', component: NotFound })。
导航守卫是路由的拦截器:beforeEach 全局前置守卫最常用来做登录校验——目标路由需要登录而用户未登录时,return '/login' 重定向。组件内可以用 useRouter 编程式导航:router.push('/user/1') 跳转、router.replace() 不留历史、router.back() 后退。
性能细节:路由组件是天然的代码分割点,用动态 import 做懒加载——component: () => import('./About.vue'),用户没访问的页面不下载。history 模式(createWebHistory)URL 干净但要求服务器把所有路径指回 index.html;hash 模式(createWebHashHistory)带 # 但零服务器配置,内网系统常用。
示例
实现一个迷你路由匹配器:静态路径、动态参数、兜底 404(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// 把 '/user/:id' 编译成匹配函数
function compilePath(pattern) {
const keys = [];
const regex = new RegExp(
'^' +
pattern
.split('/')
.map((seg) => {
if (seg.startsWith(':')) {
keys.push(seg.slice(1));
return '([^/]+)';
}
return seg;
})
.join('/') +
'$',
);
return (path) => {
const m = path.match(regex);
if (!m) return null;
return Object.fromEntries(keys.map((k, i) => [k, decodeURIComponent(m[i + 1])]));
};
}
const routes = [
{ path: '/', name: 'home' },
{ path: '/user/:id', name: 'user' },
{ path: '/post/:id/comment/:cid', name: 'comment' },
];
function resolve(path) {
for (const r of routes) {
const params = compilePath(r.path)(path);
if (params) return { name: r.name, params };
}
return { name: 'not-found', params: {} }; // 兜底
}
assert.deepStrictEqual(resolve('/'), { name: 'home', params: {} });
assert.deepStrictEqual(resolve('/user/42'), { name: 'user', params: { id: '42' } });
assert.deepStrictEqual(resolve('/post/7/comment/99'), {
name: 'comment',
params: { id: '7', cid: '99' },
});
assert.strictEqual(resolve('/no/such/page').name, 'not-found');
console.log('/user/42 →', JSON.stringify(resolve('/user/42')));
console.log('/no/such/page →', resolve('/no/such/page').name);
真实的 Vue Router 配置与使用:
// router.js —— 仅示意:真实项目代码(依赖 vue-router 包),构建时不执行
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('./pages/Home.vue') },
{ path: '/user/:id', component: () => import('./pages/User.vue') },
{ path: '/admin', component: () => import('./pages/Admin.vue'), meta: { requiresAuth: true } },
{ path: '/:pathMatch(.*)*', component: () => import('./pages/NotFound.vue') },
],
});
router.beforeEach((to) => {
const loggedIn = Boolean(localStorage.getItem('token'));
if (to.meta.requiresAuth && !loggedIn) return '/login'; // 重定向
});
export default router;
<script setup>
// User.vue —— 取动态参数
import { useRoute } from 'vue-router';
const route = useRoute();
</script>
<template>
<h1>用户 {{ route.params.id }} 的主页</h1>
</template>
常见坑
- 用 a 标签做站内跳转:整页刷新、应用状态全丢;站内导航一律 router-link 或 router.push。
- watch 漏了参数变化:/user/1 跳到 /user/2 组件实例复用,created/mounted 不再跑;取参用 useRoute 并 watch route.params。
- history 模式忘配服务器:生产环境刷新 404,Nginx 需要 try_files 指回 index.html,或改用 hash 模式。
- 在守卫里写死循环重定向:未登录 → /login → /login 也触发守卫,注意排除登录页自身。
- 路由组件不做懒加载:首页打包体积随页面数线性膨胀,路由级动态 import 是性价比最高的拆包手段。
小结
路由表映射 path→组件,router-view 渲染、router-link 导航;动态参数、嵌套路由、守卫、懒加载是四大必备件。下一章解决跨组件共享状态:Pinia。