Skip to content

Vue 路由原理深度解析

前言

Vue Router 是 Vue.js 官方的路由管理器,它和 Vue.js 核心深度集成,让构建单页应用变得简单直观。本文将深入剖析 Vue Router 的核心原理,从底层机制到实际应用,帮助你全面理解路由系统的工作方式。


一、核心概念与原理

1.1 路由模式

Vue Router 支持两种路由模式:hash 模式history 模式

Hash 模式

URL: http://example.com/#/home
  • 使用 URL 的 hash(#)部分作为路由标识
  • hash 变化会触发 hashchange 事件,但不会向服务器发送请求
  • 兼容性好,所有浏览器都支持
  • 缺点是 URL 中带有 #,不够美观

History 模式

URL: http://example.com/home
  • 使用 HTML5 History API(pushStatereplaceState
  • URL 更加美观,没有 #
  • 需要服务器配置支持,否则刷新页面会 404
  • 需要在服务器端配置 fallback 到 index.html

1.2 路由匹配原理

路由匹配是 Vue Router 的核心功能,其流程如下:

1. 用户访问 URL
2. 路由系统解析 URL
3. 匹配路由配置表
4. 渲染对应的组件
5. 更新视图

匹配优先级

  1. 静态路径:完全匹配的路径优先级最高
  2. 动态路径参数:如 /user/:id
  3. 通配符* 匹配所有路径,优先级最低

1.3 路由实例创建流程

javascript
// 创建路由实例
const router = new VueRouter({
  mode: "history",
  routes: [
    { path: "/", component: Home },
    { path: "/about", component: About },
  ],
});

// 挂载到 Vue 实例
const app = new Vue({
  router,
}).$mount("#app");

创建过程中会初始化:

  • 路由配置表
  • 路由模式(hash/history)
  • 路由守卫
  • 路由匹配器

二、核心实现机制

2.1 路由模式实现

Hash 模式实现

javascript
class HashHistory {
  constructor(router) {
    this.router = router;
    this.current = createRoute(null, { path: "/" });

    // 监听 hash 变化
    window.addEventListener("hashchange", () => {
      this.transitionTo(this.getHash());
    });
  }

  getHash() {
    return window.location.hash.slice(1) || "/";
  }

  push(path) {
    window.location.hash = path;
  }

  transitionTo(path) {
    const route = this.router.match(path);
    this.updateRoute(route);
  }

  updateRoute(route) {
    this.current = route;
    this.router.cb && this.router.cb(route);
  }
}

History 模式实现

javascript
class HTML5History {
  constructor(router) {
    this.router = router;
    this.current = createRoute(null, { path: "/" });

    // 监听 popstate 事件
    window.addEventListener("popstate", (e) => {
      this.transitionTo(e.state ? e.state.path : window.location.pathname);
    });
  }

  push(path) {
    history.pushState({ path }, "", path);
    this.transitionTo(path);
  }

  replace(path) {
    history.replaceState({ path }, "", path);
    this.transitionTo(path);
  }

  transitionTo(path) {
    const route = this.router.match(path);
    this.updateRoute(route);
  }

  updateRoute(route) {
    this.current = route;
    this.router.cb && this.router.cb(route);
  }
}

2.2 路由匹配器

javascript
class RouteMatcher {
  constructor(routes) {
    this.routes = routes;
    this.pathList = [];
    this.pathMap = {};
    this.nameMap = {};

    this.addRoutes(routes);
  }

  addRoutes(routes) {
    routes.forEach((route) => {
      this.addRoute(route);
    });
  }

  addRoute(route, parentRoute = null) {
    const path = parentRoute ? `${parentRoute.path}/${route.path}` : route.path;

    const record = {
      path,
      name: route.name,
      component: route.component,
      parent: parentRoute,
      children: route.children || [],
    };

    // 添加到路径列表和映射
    this.pathList.push(path);
    this.pathMap[path] = record;

    if (route.name) {
      this.nameMap[route.name] = record;
    }

    // 递归添加子路由
    if (route.children) {
      route.children.forEach((child) => {
        this.addRoute(child, record);
      });
    }
  }

  match(path) {
    // 精确匹配
    if (this.pathMap[path]) {
      return this.createRoute(this.pathMap[path]);
    }

    // 动态路由匹配
    for (const routePath of this.pathList) {
      const match = this.matchDynamicPath(routePath, path);
      if (match) {
        return this.createRoute(this.pathMap[routePath], match.params);
      }
    }

    // 通配符匹配
    if (this.pathMap["*"]) {
      return this.createRoute(this.pathMap["*"]);
    }

    return null;
  }

  matchDynamicPath(routePath, path) {
    const routeSegments = routePath.split("/");
    const pathSegments = path.split("/");

    if (routeSegments.length !== pathSegments.length) {
      return null;
    }

    const params = {};

    for (let i = 0; i < routeSegments.length; i++) {
      const routeSeg = routeSegments[i];
      const pathSeg = pathSegments[i];

      if (routeSeg.startsWith(":")) {
        // 动态参数
        params[routeSeg.slice(1)] = pathSeg;
      } else if (routeSeg !== pathSeg) {
        return null;
      }
    }

    return { params };
  }

  createRoute(record, params = {}) {
    const matched = [];
    let current = record;

    while (current) {
      matched.unshift(current);
      current = current.parent;
    }

    return {
      path: record.path,
      name: record.name,
      component: record.component,
      params,
      matched,
    };
  }
}

2.3 导航守卫实现

导航守卫是路由跳转过程中的钩子函数,用于控制路由访问权限。

javascript
class NavigationGuard {
  constructor() {
    this.beforeEachHooks = [];
    this.beforeResolveHooks = [];
    this.afterEachHooks = [];
  }

  beforeEach(hook) {
    this.beforeEachHooks.push(hook);
  }

  beforeResolve(hook) {
    this.beforeResolveHooks.push(hook);
  }

  afterEach(hook) {
    this.afterEachHooks.push(hook);
  }

  async runBeforeEach(to, from) {
    for (const hook of this.beforeEachHooks) {
      const result = await hook(to, from);
      if (result === false || result instanceof Error) {
        return false;
      }
    }
    return true;
  }

  async runBeforeResolve(to, from) {
    for (const hook of this.beforeResolveHooks) {
      const result = await hook(to, from);
      if (result === false || result instanceof Error) {
        return false;
      }
    }
    return true;
  }

  runAfterEach(to, from) {
    this.afterEachHooks.forEach((hook) => hook(to, from));
  }
}

三、实际应用案例

3.1 完整路由配置

javascript
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
import About from "../views/About.vue";
import User from "../views/User.vue";
import UserProfile from "../views/UserProfile.vue";
import UserPosts from "../views/UserPosts.vue";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "Home",
    component: Home,
  },
  {
    path: "/about",
    name: "About",
    component: About,
  },
  {
    path: "/user/:id",
    name: "User",
    component: User,
    children: [
      {
        path: "profile",
        name: "UserProfile",
        component: UserProfile,
      },
      {
        path: "posts",
        name: "UserPosts",
        component: UserPosts,
      },
    ],
  },
  {
    path: "*",
    name: "NotFound",
    component: () => import("../views/NotFound.vue"),
  },
];

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes,
});

export default router;

3.2 导航守卫实战

javascript
// 全局前置守卫
router.beforeEach((to, from, next) => {
  console.log("Global beforeEach:", to.path);

  // 检查是否需要登录
  if (to.meta.requiresAuth && !isLoggedIn()) {
    next({ name: "Login" });
    return;
  }

  next();
});

// 全局解析守卫
router.beforeResolve((to, from, next) => {
  console.log("Global beforeResolve:", to.path);
  next();
});

// 全局后置守卫
router.afterEach((to, from) => {
  console.log("Global afterEach:", to.path);
});

// 路由独享守卫
const routes = [
  {
    path: "/admin",
    component: Admin,
    beforeEnter: (to, from, next) => {
      if (!isAdmin()) {
        next(false);
        return;
      }
      next();
    },
  },
];

// 组件内守卫
export default {
  beforeRouteEnter(to, from, next) {
    next((vm) => {
      vm.initData();
    });
  },

  beforeRouteUpdate(to, from, next) {
    this.updateData(to.params.id);
    next();
  },

  beforeRouteLeave(to, from, next) {
    if (this.hasUnsavedChanges) {
      if (confirm("Are you sure?")) {
        next();
      } else {
        next(false);
      }
    } else {
      next();
    }
  },
};

3.3 路由懒加载

javascript
// 方式一:动态 import
const Home = () => import("../views/Home.vue");

// 方式二:按需加载分组
const User = () => import(/* webpackChunkName: "user" */ "../views/User.vue");
const UserProfile = () =>
  import(/* webpackChunkName: "user" */ "../views/UserProfile.vue");

// 方式三:路由级别懒加载
const routes = [
  {
    path: "/about",
    name: "About",
    component: () => import("../views/About.vue"),
  },
];

3.4 动态路由添加

javascript
// 在运行时添加路由
router.addRoute({
  path: "/new-route",
  name: "NewRoute",
  component: NewComponent,
});

// 添加嵌套路由
router.addRoute("User", {
  path: "settings",
  name: "UserSettings",
  component: UserSettings,
});

// 获取当前路由配置
const routes = router.getRoutes();

// 移除路由
router.removeRoute("NewRoute");

四、常见踩坑与问题排查

4.1 History 模式刷新 404

问题:使用 history 模式时,刷新页面出现 404 错误。

原因:服务器没有配置 fallback 到 index.html。

解决方案

Nginx 配置

nginx
location / {
  try_files $uri $uri/ /index.html;
}

Apache 配置

apache
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

Express 配置

javascript
const express = require("express");
const path = require("path");
const app = express();

app.use(express.static(path.join(__dirname, "dist")));

app.get("*", (req, res) => {
  res.sendFile(path.join(__dirname, "dist", "index.html"));
});

app.listen(3000);

4.2 动态路由参数更新

问题:同一个路由组件,参数变化时组件不重新渲染。

原因:Vue Router 默认复用组件实例,不会销毁重建。

解决方案

javascript
// 方案一:监听 $route 变化
export default {
  watch: {
    '$route'(to, from) {
      this.fetchData(to.params.id)
    }
  },
  mounted() {
    this.fetchData(this.$route.params.id)
  }
}

// 方案二:使用 beforeRouteUpdate 守卫
export default {
  beforeRouteUpdate(to, from, next) {
    this.fetchData(to.params.id)
    next()
  }
}

// 方案三:给 router-view 添加 key
<router-view :key="$route.fullPath" />

4.3 路由跳转不生效

问题:调用 router.push 后页面没有变化。

常见原因及解决方案

javascript
// 原因一:导航守卫返回 false
router.beforeEach((to, from, next) => {
  // 忘记调用 next()
});

// 原因二:重复跳转到当前路由
router.push("/current-path"); // 不会触发导航

// 解决方案:强制跳转
router.push({ path: "/current-path", force: true });

// 原因三:路由配置错误
const routes = [{ path: "/user/:id", component: User }];
// 跳转时没有提供参数
router.push("/user"); // 不会匹配到路由

// 正确方式
router.push("/user/123");
router.push({ name: "User", params: { id: 123 } });

4.4 嵌套路由渲染问题

问题:嵌套路由的子组件无法渲染。

原因:父组件中没有放置 <router-view>

解决方案

vue
<!-- 父组件 User.vue -->
<template>
  <div>
    <h1>User {{ $route.params.id }}</h1>
    <!-- 必须添加 router-view 才能渲染子路由 -->
    <router-view></router-view>
  </div>
</template>

五、优化方案与进阶拓展

5.1 路由性能优化

javascript
// 方案一:路由懒加载
const routes = [
  {
    path: '/dashboard',
    component: () => import(/* webpackChunkName: "dashboard" */ '../views/Dashboard.vue')
  }
]

// 方案二:预加载关键路由
router.beforeResolve((to, from, next) => {
  if (to.meta.preload) {
    // 预加载相关资源
    preloadAssets(to.meta.preload)
  }
  next()
})

// 方案三:使用 keep-alive 缓存组件
<router-view v-slot="{ Component }">
  <keep-alive>
    <component :is="Component" />
  </keep-alive>
</router-view>

5.2 路由状态管理

javascript
// 方式一:使用 Vuex 管理路由状态
import store from "./store";

router.beforeEach((to, from, next) => {
  store.dispatch("route/setCurrentRoute", to);
  next();
});

// store/modules/route.js
export default {
  state: {
    currentRoute: null,
  },
  mutations: {
    SET_CURRENT_ROUTE(state, route) {
      state.currentRoute = route;
    },
  },
  actions: {
    setCurrentRoute({ commit }, route) {
      commit("SET_CURRENT_ROUTE", route);
    },
  },
  getters: {
    currentRoute: (state) => state.currentRoute,
  },
};

5.3 路由权限控制

javascript
// 细粒度权限控制
router.beforeEach(async (to, from, next) => {
  // 获取用户权限
  const permissions = await store.dispatch("auth/getPermissions");

  // 检查路由权限
  if (to.meta.permissions) {
    const hasPermission = to.meta.permissions.every((perm) =>
      permissions.includes(perm),
    );

    if (!hasPermission) {
      next({ name: "Forbidden" });
      return;
    }
  }

  next();
});

// 路由配置
const routes = [
  {
    path: "/admin/users",
    component: UserManagement,
    meta: {
      requiresAuth: true,
      permissions: ["user:read", "user:write"],
    },
  },
];

5.4 路由动画

vue
<template>
  <transition name="fade" mode="out-in">
    <router-view :key="$route.fullPath"></router-view>
  </transition>
</template>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

六、全文总结

Vue Router 的核心原理可以概括为以下几点:

  1. 路由模式:hash 模式基于 hashchange 事件,history 模式基于 HTML5 History API
  2. 路由匹配:通过路由配置表和匹配算法,将 URL 映射到对应的组件
  3. 导航守卫:提供全局、路由级、组件级三个层面的钩子函数,用于控制路由跳转
  4. 路由懒加载:通过动态 import 实现代码分割,优化首屏加载性能
  5. 嵌套路由:通过 <router-view> 嵌套实现复杂的页面结构

掌握这些原理,可以帮助你更好地理解和使用 Vue Router,解决实际开发中遇到的各种路由问题。