返回首页

Vue Router如何重塑导航

布莱克2026-06-10 23:44已编辑
Tip:文章封面与内容无关,作者旅游时拍摄,因为没什么值得把四季都错过!

传统跳转(全量刷新)

当你访问 a.com/page1 跳到 a.com/page2 时:

  1. 浏览器销毁当前页面的所有变量(Vue 实例、未提交的 Vuex/Pinia 数据、定时器)。
  2. 向服务器请求新的 HTML。
  3. 重新加载 CSS、重新下载并执行 JS(即便两个页面的 JS 是一模一样的)。
  4. 结果: 性能浪费,且无法维持跨页面的动画或复杂的应用状态。


SPA路由的核心 = URL改变 + 页面不刷新 + 内容局部更新


Vue Router(组件映射)

Vue Router 的出现,就是为了在 “不刷新页面” 的前提下,模拟出 “换页” 的效果。

它建立了一张映射表
| URL 路径 | 对应的 Vue 组件 |
| :--- | :--- |
| /home | Home.vue |
| /user/:id | User.vue |

它的工作流程是:

  1. 感知变化:通过监听 popstate 或 hashchange 知道 URL 变了。
  2. 查找映射:在内存里的路由表查到:“哦,现在该看 User 组件了”。
  3. 局部替换:Vue Router 通知 Vue 引擎:“把页面里那个 <router-view> 位置的组件切成 User 吧”

实现了三个维度的“桥接”

  1. URL 与 组件的映射:让每一个 URL 都有一个对应的 UI 界面。
  2. History 与 内存状态的同步:在不刷新页面的前提下,通过劫持浏览器历史记录,让“后退/前进”按钮在单页应用中依然有效。
  3. 导航生命周期的管理:提供了一套拦截机制(钩子函数),让开发者能在页面切换的“缝隙”里处理权限、埋点或异步数据加载


第一层:History 管理器(抹平差异)

它封装了浏览器的原生 API。

  • Hash 模式:监听 hashchange。
  • History 模式:监听 popstate,并封装 pushState/replaceState。
  • 作用:对外提供统一的 push、replace 接口,无论底层用哪种模式,上层调用逻辑一致。

第二层:Matcher 匹配器(建立映射)

这是路由器的“大脑”。

  • 路由表转换:将你写的嵌套对象转换成扁平化的索引表。

   如果保持原始的嵌套树结构,每次 URL 变化都要进行一次递归搜索,在大型应用中(比如包含数百个路由),性能开销将不可接受

   当你执行 createRouter 时,内部会调用一个名为 createRouterMatcher 的函数。它会遍历你定义的嵌套 routes 数组,递归地生成一个扁平化的结构

   为了实现 $O(1)$ 或极高效率的查询,它会维护三张核心表:(Vue Router4)

       matcherMap:以 name 为 Key 的映射表。

       pathMatchers:一个包含所有路由记录的数组(用于路径正则匹配)。

       matchers:所有路由记录的集合

扁平化前:

const routes = [
  {
    path: '/algorithm',
    component: Parent,
    children: [
      { name: 'Detail', path: 'detail/:id', component: Detail }
    ]
  }
];

扁平化后:

// 扁平化后的 Matchers 数组
[
  {
    path: '/algorithm',
    record: { component: Parent, children: [...] },
    re: /^\/algorithm\/?$/i, // 自动生成的正则表达式
    score: [10, 0], // 用于排序的权重分
  },
  {
    path: '/algorithm/detail/:id',
    name: 'Detail',
    record: { component: Detail },
    re: /^\/algorithm\/detail\/([^/]+?)\/?$/i,
    score: [10, 10, 5], // 动态参数权重略低于静态路径
    parent: { ... } // 依然保留对父级的引用,以便渲染嵌套视图
  }
]
  • 正则匹配:当你给出一个路径时,它通过 pathToRegexp 快速定位到对应的 RouteRecord。

第三层:响应式桥接(驱动渲染)

这是 Vue Router 区别于其他框架路由的关键。

  • 它在内部维护一个响应式变量(currentRoute)。
  • 当 History 模块检测到 URL 变化 -> Matcher 匹配到新组件 -> 修改 currentRoute。
  • 此时,依赖该变量的 <router-view> 会自动触发 Vue 的 render 函数,完成组件的物理替换。


路由模式及监听原理

Hash 模式(默认,兼容性最好)

原理: 利用 URL 的 # 符号,# 后面的变化不会触发浏览器请求服务器

// 监听 hashchange 事件
window.addEventListener('hashchange', () => {
  // 获取当前 hash
  const hash = window.location.hash.slice(1)
  // 匹配路由,渲染组件
  router.match(hash)
})
优点缺点
兼容性极好(支持IE9+)URL中有#,不够美观
无需服务端配置SEO较差(爬虫不解析#后内容)
不会导致404锚点功能失效


History 模式(HTML5,更美观)

原理: 利用 HTML5 的 History API(pushState(往历史栈添加一条新记录)replaceState(替换当前历史记录)popstate(浏览器前进后退))来管理路由。

// 1. 监听 popstate 事件(浏览器前进/后退)
window.addEventListener('popstate', () => {
  const path = window.location.pathname
  router.match(path)
})

// 2. pushState 和 replaceState 不触发事件,需要手动处理
function push(path) {

  history.pushState(null, '', path) //状态对象 标题 新url
  // 手动触发路由更新
  router.match(path)
}


function replace(path) {

  history.replaceState(null, '', path)
  router.match(path)
}
优点缺点
URL美观,无#需要服务端配置(否则刷新404)
SEO友好兼容性稍差(IE10+)
更接近原生体验需要处理锚点冲突

需要在nginx配置:

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


router.push() 主动修改URL,然后事件监听用来响应“浏览器前进/后退”

简化版实现:

class MiniRouter {
  constructor(routes) {
    this.routes = routes          // 路由配置
    this.currentPath = '/'        // 当前路径
    this._route = { current: null } // 响应式数据
    
    // 1. 监听 popstate(处理浏览器前进/后退)
    window.addEventListener('popstate', () => {
      console.log('监听到popstate,URL变了')
      // 获取新的URL
      const newPath = window.location.pathname
      // 手动更新页面
      this.transitionTo(newPath)
    })
    
    // 2. 监听 hashchange(hash模式用)
    window.addEventListener('hashchange', () => {
      const newPath = window.location.hash.slice(1)
      this.transitionTo(newPath)
    })
  }
  
  // ⭐ 核心方法:路由跳转
  push(path) {
    console.log('主动跳转到:', path)
    
    // 第一步:修改浏览器URL
    // 注意:这里用的是 pushState,它不会触发 popstate!(history模式)
    history.pushState(null, null, path)
    // hash模式为 window.location.hash = path,会主动触发hashchange
    
    // 第二步:手动更新页面内容
    // 因为 pushState 不会自动触发任何事件,必须手动调用
    this.transitionTo(path)
  }
  
  // 实际执行页面更新的方法
  transitionTo(path) {
    console.log('执行页面更新:', path)
    
    // 匹配路由
    const route = this.matchRoute(path)
    
    // 更新响应式数据(触发 router-view 重新渲染)
    this._route.current = route
    
    // 更新当前路径
    this.currentPath = path
  }
  
  matchRoute(path) {
    return this.routes.find(route => route.path === path)
  }
}

// 使用示例
const router = new MiniRouter([
  { path: '/', component: 'Home' },
  { path: '/about', component: 'About' }
])

// 主动跳转 - 调用 push
router.push('/about')  
// 执行流程:
// 1. history.pushState() → 地址栏变成 /about
// 2. transitionTo() → 页面切换到 About 组件

// 用户点击后退 - 浏览器触发 popstate
// 执行流程:
// 1. 浏览器改变URL → 地址栏变成 /
// 2. 触发 popstate 事件
// 3. 事件回调里调用 transitionTo('/') → 页面切换到 Home 组件


执行流程

比如router.push(`/code/detail/${row.id}`)执行后,内部经历了哪些步骤

第一阶段:标准化与初筛(History 模块)

当你调用 router.push,首先接待你的是 History 记录管理器

  1. 参数规格化:Router 首先判断你传的是字符串还是对象。如果是字符串(如上述示例),它会立刻将其解析为一个 Location 对象。
  2. 预判拦截:它会检查新路径是否与当前路径完全一致。如果完全一样(且没有强制刷新的逻辑),它可能会直接拦截,避免重复跳转产生的冗余性能开销。


第二阶段:寻图定位(Matcher 匹配器)

这是最硬核的一步。Router 需要弄清楚:这个字符串路径到底对应谁?

  1. 正则提取:Matcher 拿着 /algorithm/detail/123 去路由表里匹配。
  2. 得分计算 (Ranking):它发现你的路由配置里有一个 { path: '/algorithm/detail/:id', name: 'AlgDetail', component: Detail }。Matcher 内部的 pathToRegexp 跑了一遍,确认匹配成功,并从 URL 中提取出 params: { id: '123' }。
  3. 生成匹配记录 (Matched Records):如果是嵌套路由,Matcher 会生成一个数组(如 [ParentRecord, ChildRecord])。输出:一个 Route 对象(即我们平时用的 $route 的雏形),里面包含了完整的 path, params, query 和 meta。


第三阶段:过五关斩六将(导航守卫流水线)

在 URL 真正改变之前,必须通过一系列的“安检”。Vue Router 4 将这些钩子函数串联成了一个 Promise 链

  1. 失活钩子:调用当前页面组件的 beforeRouteLeave。
  2. 全局前置:执行 router.beforeEach。
  3. 重用钩子:如果是在同一个组件内切换(仅 ID 变了),执行 beforeRouteUpdate。
  4. 路由配置钩子:执行配置表里的 beforeEnter。
  5. 解析钩子:执行 beforeResolve。

底层秘密:只要其中任何一个钩子返回 false 或者 reject,整个 router.push 产生的 Promise 就会直接中断,地址栏和视图都不会有任何变化。


第四阶段:改变表象(同步地址栏)

一旦所有守卫通过,Router 才会真正去动浏览器的东西。

  1. 物理修改:如果你是 History 模式,它执行 window.history.pushState(state, '', url)。如果你是 Hash 模式,它修改 location.hash。
  2. 注意:此时浏览器地址栏变了,但页面依然是旧的!

       原生限制:popstate 只有在用户点击后退、前进按钮,或者调用 history.back() 时才会触发。

       Vue Router 的解决方案:它重写了跳转方法。当你调用 router.push,它在内部同时执行了“改地址栏”和“触发 Vue 组件渲染”两个任务


第五阶段:核心跃迁(驱动视图重绘)

这是最后一步,也是最关键的一步:如何通知 Vue 更新界面?

  1. 修改响应式状态: Router 内部有一个核心状态(在 v4 中通常是 currentRoute)。此时,Router 会执行: currentRoute.value = toLocation;(这是一个 shallowRef)。
  2. 依赖触发: 你的组件树中一定包含 <router-view>。由于 <router-view> 在渲染时访问了 currentRoute,它就被作为“依赖”收集到了。
  3. 组件替换: currentRoute 的改变触发了 <router-view> 的 Re-render。它根据 toLocation.matched 里的组件定义,通过 Vue 的内建组件(如 <component :is="..."> 的底层逻辑)将旧组件卸载,新组件挂载。


路由懒加载:

正常加载:是在页面初始化时,通过 import Content from '...' 直接引入。这意味着无论用户是否访问该页面,组件代码都会被打包进 app.js(主包)中。

懒加载:使用 () => import('...') 这种动态导入语法。只有当用户 真正访问 那个路由时,浏览器才会去加载该组件对应的脚本

“路由懒加载本质上是一种**‘空间换时间’**的策略。

我们通过在磁盘上生成更多的分包文件(空间),换取了首屏加载时更少的网络传输量(时间)


assistant