Vue Router 深度详解

中等 🟡Vue 生态
7 个标签
预计阅读时间:107 分钟
Vue Router路由导航守卫路由传参懒加载权限控制TypeScript

Vue Router 深度详解

Vue Router 是 Vue.js 官方的路由管理器,与 Vue.js 深度集成,让构建单页面应用变得简单。本文详细讲解 Vue Router 的核心概念、底层原理、高级用法和最佳实践,覆盖从入门到工程化落地的完整链路。

零、先理解"前端路由"到底是什么

在传统多页应用中,每个 URL 对应服务器上的一个真实文件,点击链接就是让浏览器重新向服务器要一份新 HTML。而在单页应用(SPA)里,整个站点只有一份 HTML,"切换页面"其实是 JavaScript 在前端动态替换 DOM。前端路由就是负责"监听 URL 变化 → 找到对应组件 → 渲染到页面"这套机制的模块。

可以用一个类比理解:Vue Router 就像一部电梯的楼层控制系统。URL 是你按下的楼层按钮,路由表是电梯知道每层是什么的映射表,router-view 是电梯门打开后你走进的那个楼层空间。整栋楼(页面)不动,只是把你送到不同楼层(组件)。

为什么这件事重要?

| 能力 | 没有前端路由 | 有 Vue Router |

|------|--------------|---------------|

| URL 可分享 | 否,永远是同一个地址 | 是,每个视图有独立 URL |

| 浏览器前进后退 | 失效 | 正常工作 |

| 首屏加载 | 每次切换都白屏 | 只加载一次,切换无刷新 |

| SEO / 埋点 | 难以按页面统计 | 可按路由统计 PV |

| 代码组织 | v-if 地狱 | 声明式路由表 |

一、Vue Router 基础

1. 安装与配置

Vue Router 4(Vue3)安装:

bashCode
npm install vue-router@4

Vue Router 3(Vue2)安装:

bashCode
npm install vue-router@3

基本配置:

javascriptCode
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

export default router;

在 main.js 中注册:

javascriptCode
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

const app = createApp(App);
app.use(router);
app.mount('#app');

在根组件放置出口与导航:

vueCode
<!-- App.vue -->
<template>
  <header>
    <router-link to="/">首页</router-link>
    <router-link to="/about">关于</router-link>
  </header>
  <main>
    <!-- 当前路由匹配的组件渲染在这里 -->
    <router-view />
  </main>
</template>

Vue Router 3 与 4 的注册差异对比:

javascriptCode
// Vue Router 3(Vue2)
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
const router = new VueRouter({ mode: 'history', routes });
new Vue({ router, render: h => h(App) }).$mount('#app');

// Vue Router 4(Vue3)
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({ history: createWebHistory(), routes });
createApp(App).use(router).mount('#app');

2. 路由模式

路由模式决定了 URL 的形态以及底层监听 URL 变化的技术方案。

History 模式(推荐):

javascriptCode
import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory(),
  routes
});

URL 示例:https://example.com/user/123

原理:基于 HTML5 History API 的 history.pushState() 和 history.replaceState(),它们能在不刷新页面的前提下修改地址栏 URL,同时通过监听 popstate 事件感知用户点击前进/后退。

Hash 模式:

javascriptCode
import { createRouter, createWebHashHistory } from 'vue-router';

const router = createRouter({
  history: createWebHashHistory(),
  routes
});

URL 示例:https://example.com/#/user/123

原理:URL 中 # 后面的部分称为 hash,它的变化不会触发浏览器向服务器发起请求,同时会触发 hashchange 事件,Vue Router 借此实现前端路由。因此 hash 模式无需任何服务器配置。

Memory 模式(适用于 Node.js 环境):

javascriptCode
import { createRouter, createMemoryHistory } from 'vue-router';

const router = createRouter({
  history: createMemoryHistory(),
  routes
});

这种模式不依赖浏览器地址栏,路由状态保存在内存中,常用于 SSR(服务端渲染)、单元测试、以及非浏览器环境。

三种模式对比:

| 模式 | 底层 API | URL 形态 | 需服务器配置 | 典型场景 |

|------|----------|----------|--------------|----------|

| History | pushState / popstate | 干净无 # | 是 | 生产环境主流 |

| Hash | location.hash / hashchange | 带 # | 否 | 静态托管、老浏览器 |

| Memory | 内存栈 | 无地址栏 | 否 | SSR、测试 |

History 模式的服务器配置(否则刷新会 404):

nginxCode
# Nginx
location / {
  try_files $uri $uri/ /index.html;
}
javascriptCode
// Node.js / Express
const history = require('connect-history-api-fallback');
app.use(history());
app.use(express.static('dist'));

二、路由配置详解

1. 路由参数

动态路由参数:

javascriptCode
const routes = [
  {
    path: '/user/:id',
    name: 'User',
    component: User,
    props: true // 将路由参数作为 props 传递
  }
];

组件中获取参数:

vueCode
<script setup>
import { useRoute } from 'vue-router';

const route = useRoute();
// 方式 1:通过 route.params
console.log(route.params.id);

// 方式 2:通过 props(需要设置 props: true)
defineProps(['id']);
</script>

为什么推荐 props: true? 它让组件不再直接依赖 $route,组件变得更纯粹、更易测试与复用。你甚至可以在单元测试里直接给组件传 id,而不需要 mock 整个路由。

javascriptCode
// props 还可以是函数,做转换或注入静态数据
{
  path: '/user/:id',
  component: User,
  props: (route) => ({
    id: Number(route.params.id), // 把字符串转成数字
    query: route.query.q
  })
}

多个动态参数:

javascriptCode
{
  path: '/user/:userId/post/:postId',
  name: 'UserPost',
  component: UserPost
}
// 访问 /user/1/post/99 时:
// route.params.userId === '1'
// route.params.postId === '99'

可选参数:

javascriptCode
{
  path: '/user/:id?',
  name: 'User',
  component: User
}
// /user 和 /user/123 都能匹配

带正则约束的参数:

javascriptCode
{
  // 仅匹配纯数字 id,/user/abc 不会命中此路由
  path: '/user/:id(\\d+)',
  component: User
}

*通配符参数(Vue Router 4 已移除 ,使用 catch-all 路由):**

javascriptCode
{
  path: '/:pathMatch(.*)*',
  name: 'NotFound',
  component: NotFound
}

2. 查询参数

传递查询参数:

vueCode
<template>
  <router-link :to="{ path: '/search', query: { q: 'vue', page: 1 } }">
    搜索 Vue
  </router-link>
</template>

<script setup>
import { useRouter } from 'vue-router';

const router = useRouter();

// 编程式导航
router.push({
  path: '/search',
  query: { q: 'vue', page: 1 }
});
</script>

获取查询参数:

vueCode
<script setup>
import { useRoute } from 'vue-router';

const route = useRoute();
console.log(route.query.q); // 'vue'
console.log(route.query.page); // '1'(注意始终是字符串)
</script>

响应式监听查询参数变化:

vueCode
<script setup>
import { useRoute } from 'vue-router';
import { watch } from 'vue';

const route = useRoute();

watch(
  () => route.query,
  (newQuery) => {
    // 分页、筛选变化时重新拉取数据
    fetchList({ q: newQuery.q, page: Number(newQuery.page) || 1 });
  },
  { immediate: true }
);
</script>

3. 路由传参方式对比

| 方式 | 位置 | 刷新后 | 是否需在 path 声明 | 适用场景 |

|------|------|--------|--------------------|----------|

| params | URL 路径 | 保留 | 是(:id) | 资源标识 |

| query | URL 查询 | 保留 | 否 | 搜索、过滤、分页 |

| state | history.state | 丢失 | 否 | 临时数据、不宜暴露在 URL |

使用 state 传参:

javascriptCode
router.push('/user/123', {
  state: { from: 'home' }
});

// 获取 state
const location = router.currentRoute.value.state.from;

为什么有 state? 有些数据不适合放在 URL 里(太长、敏感、或纯粹的临时上下文),state 把数据挂在 history 记录上,好处是不污染 URL,代价是刷新页面就会丢失。适合"从哪个页面跳过来"这类一次性信息。

三、导航守卫

导航守卫本质上是"在路由跳转的各个阶段插入钩子函数",用于鉴权、埋点、数据预取、离开确认等横切逻辑。

1. 全局守卫

全局前置守卫:

javascriptCode
router.beforeEach((to, from, next) => {
  console.log('全局前置守卫');
  console.log('to:', to.path);
  console.log('from:', from.path);

  // 权限检查示例
  const token = localStorage.getItem('token');
  if (to.meta.requiresAuth && !token) {
    next('/login');
  } else {
    next();
  }
});

Vue Router 4 更推荐的返回值写法(不再必须调用 next):

javascriptCode
router.beforeEach((to, from) => {
  const token = localStorage.getItem('token');
  if (to.meta.requiresAuth && !token) {
    // 返回一个路由地址进行重定向
    return { name: 'Login', query: { redirect: to.fullPath } };
  }
  // 返回 true 或 undefined 表示放行;返回 false 表示取消导航
});

全局解析守卫:

javascriptCode
router.beforeResolve(async (to, from) => {
  // 在所有组件内守卫和异步路由组件被解析后调用
  // 适合做"确保数据加载完成再进入"的最终检查
  if (to.meta.requiresData) {
    await preloadData(to);
  }
});

全局后置守卫:

javascriptCode
router.afterEach((to, from) => {
  console.log('全局后置守卫');
  // 页面标题设置
  document.title = to.meta.title || '默认标题';
  // 埋点上报
  sendPageView(to.fullPath);
});

后置守卫不接收 next,也不能改变导航,因为此时导航已经完成,它只适合做"副作用",如改标题、埋点、关闭 loading。

2. 路由独享守卫

javascriptCode
const routes = [
  {
    path: '/dashboard',
    component: Dashboard,
    beforeEnter: (to, from, next) => {
      console.log('路由独享守卫');
      const token = localStorage.getItem('token');
      if (!token) {
        next('/login');
      } else {
        next();
      }
    }
  }
];

beforeEnter 也支持传入数组,组合多个守卫:

javascriptCode
function requireAuth(to, from, next) {
  localStorage.getItem('token') ? next() : next('/login');
}
function requireAdmin(to, from, next) {
  isAdmin() ? next() : next('/403');
}

const routes = [
  { path: '/admin', component: Admin, beforeEnter: [requireAuth, requireAdmin] }
];

3. 组件内守卫

vueCode
<script>
export default {
  // 进入组件前
  beforeRouteEnter(to, from, next) {
    console.log('beforeRouteEnter');
    // 不能访问 this,因为组件实例还没创建
    next(vm => {
      // 可以通过 vm 访问组件实例
      console.log(vm);
    });
  },

  // 组件更新前(同一组件被复用,仅参数变化时触发)
  beforeRouteUpdate(to, from, next) {
    console.log('beforeRouteUpdate');
    // 可以访问 this
    next();
  },

  // 离开组件前
  beforeRouteLeave(to, from, next) {
    console.log('beforeRouteLeave');
    const answer = window.confirm('确定要离开吗?未保存的更改将丢失。');
    next(answer);
  }
};
</script>