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(
pushState、replaceState) - URL 更加美观,没有
# - 需要服务器配置支持,否则刷新页面会 404
- 需要在服务器端配置 fallback 到 index.html
1.2 路由匹配原理
路由匹配是 Vue Router 的核心功能,其流程如下:
1. 用户访问 URL
2. 路由系统解析 URL
3. 匹配路由配置表
4. 渲染对应的组件
5. 更新视图匹配优先级
- 静态路径:完全匹配的路径优先级最高
- 动态路径参数:如
/user/:id - 通配符:
*匹配所有路径,优先级最低
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 的核心原理可以概括为以下几点:
- 路由模式:hash 模式基于
hashchange事件,history 模式基于 HTML5 History API - 路由匹配:通过路由配置表和匹配算法,将 URL 映射到对应的组件
- 导航守卫:提供全局、路由级、组件级三个层面的钩子函数,用于控制路由跳转
- 路由懒加载:通过动态 import 实现代码分割,优化首屏加载性能
- 嵌套路由:通过
<router-view>嵌套实现复杂的页面结构
掌握这些原理,可以帮助你更好地理解和使用 Vue Router,解决实际开发中遇到的各种路由问题。
