讲解

单页应用只有一张 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。