导航


HTML

CSS

JavaScript

浏览器 & 网络

版本管理

框架

构建工具

TypeScript

性能优化

软实力

算法

UI、组件库

Node

业务技能

针对性攻坚

AI


原理

总览

模式 URL 形态 底层依赖 是否需要后端配合
hash /#/user/1 location.hash + hashchange ❌ 不需要
history /user/1 history.pushState + popstate ✅ 需要

vue-router 本质:监听 URL 的变化 → 匹配路由表 → 渲染对应组件

hash 原理

<!DOCTYPE html>
<html lang="en">
<body>
  <button id="myBtn">按钮</button>
  <script>
    const btn = document.getElementById('myBtn');
    window.addEventListener('DOMContentLoaded', () => {
      console.log(location.hash);
    });
    btn.addEventListener('click', () => {
      location.hash = '#/lance';
    });
    window.addEventListener('**hashchange**', () => {
      console.log(location.hash);
    })
  </script>
</body>
</html>

vue-router 内部原理

function setupHashListener() {
  window.addEventListener('hashchange', () => {
    const path = location.hash.slice(1) // "/user/1"
    router.match(path)
    router.updateView()
  })
}

流程图:

修改 hash
   ↓
触发 hashchange
   ↓
解析 hash 路径
   ↓
路由匹配
   ↓
组件重新渲染

为什么 hash 改变不会刷新页面?

因为:

history 原理

  1. URL 变化:当调用 router.push() 或 router.replace() 时,Vue Router 实际上调用了浏览器的 history.pushState() 或 history.replaceState()
  2. 不刷新页面pushState 和 replaceState 仅修改浏览器 URL,并将新的路由记录存入历史栈,但不会触发页面重载。
  3. 监听 URL 变化
  4. 拦截和处理:由于 pushState/replaceState 不触发 popstate,Vue Router 会拦截这些方法的调用,并在内部手动触发路由切换逻辑,以确保路由始终与 URL 同步。
  5. 服务器配置(关键)
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button id="myBtn">按钮</button>
  <script>
    const btn = document.getElementById('myBtn');
    window.addEventListener('DOMContentLoaded', () => {
      console.log(location.pathname);
    });
    btn.addEventListener('click', () => {
      const state = { name: 'user' };
      history.pushState(state, '', 'user'); // 此处不会触发下方 popstate 监听
      // state, title, url
    });
    window.addEventListener('**popstate**', () => {
      console.log(location.pathname);
    });
  </script>
</body>
</html>

vue-router 怎么知道 pushState 发生了

“劫持”了 history 方法

vue-router 内部原理

const originalPush = history.pushState

history.pushState = function (...args) {
  originalPush.apply(history, args)
  router.notify() // 手动通知路由变化
}

流程图:

浏览器前进:

router.push('/user/1')
   ↓
history.pushState()
   ↓
vue-router 手动触发路由更新
   ↓
组件重新渲染

浏览器后退:

点击后退
   ↓
popstate
   ↓
vue-router 监听
   ↓
路由更新

为什么 history 模式需要后端配合?

问题场景

用户访问:

<http://example.com/user/1>

浏览器行为:

  1. 向服务器请求 /user/1
  2. 如果服务器没有这个路径
  3. 👉 返回 404

正确做法(后端兜底)

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

所有路径都返回 SPA 的 index.html 再由 vue-router 接管

总结

实现

步骤

  1. class VueRouter
  2. 添加静态方法 install()Vue.use() 时需要调用
    1. Vue在调用 install 时,会把 Vue 构造函数传给我们
    2. Vue.mixin -> beforeCreate 中:
      1. 寻找根组件(存在 this.$options.router,也就是 new VueRouter 实例);找到后调用 init 方法开始执行
  3. init 方法中要调用
    1. 监听 hash 值变化(window.addEventListener => DOMContentLoadedhashchange
    2. 创建 路由映射表 (routes数组 => hashMap{ path: route })
    3. 注册全局组件 router-linkrouter-view

my-router/index.js

let Vue;

class VueRouter {
  constructor(options) {
    this.$options = options;
    this.routeMap = {};
    this.vm = new Vue({
      data() {
        return {
          currentPath: '/'
        }
      }
    });
  }
  init() {
    // 1. 监听hash值变化
    this.bindEvent();
    // 2. 创建路由映射表
    this.createHashMap();
    // console.log(this.routeMap);
    // {/: {path: '/', name: 'Home', component: {…}}, /about: {path: '/about', name: 'About', component: {...}}

    // 3. 注册全局路由组件 router-link、router-view
    this.initRouteComponent();
  }
  bindEvent() {
    // 页面加载完监听
    window.addEventListener('DOMContentLoaded', this.handleHashChange.bind(this), false);
    // hash值变化后监听
    window.addEventListener('hashchange', this.handleHashChange.bind(this), false);
  }
  handleHashChange() {
    console.log('handleHashChange-hash:', window.location.hash);
    const hash = this.getHashValue();
    // vm实例下的属性发生改变,会导致重新渲染
    this.vm.currentPath = hash;
  }
  // 获取hash值
  getHashValue() {
    // console.log("getHashValue-hash:", window.location.hash.slice(1));
    return window.location.hash.slice(1) || '/';
  }
  // 创建路由映射表
  createHashMap() {
    // 把 routes数组 转 hash 方式存储
    this.$options.routes.forEach(item => {
      this.routeMap[item.path] = item;
    });
  }
  // 初始化全局路由组件
  initRouteComponent() {
    Vue.component('router-view', {
      render: h => {
        const component = this.routeMap[this.vm.currentPath].component;
        // return h('h1', 'Hello JS++');
        return h(component);
      }
    });
    Vue.component('router-link', {
      props: {
        to: String,
      },
      render(h) {
        // h函数: 标签名, 属性集合
        // this.$slots.default: 拿到插槽内容
        return h('a', {
          attrs: {
            href: '#' + this.to
          }
        }, this.$slots.default);
      }
    });
  }

  // install 得是个静态方法
  // 因为Vue需要调用install (Vue.install)
  // 但调用时不需要通过new来实例化
  // Vue在调用install时,会传入参数:Vue构造函数
  static install(_Vue) {
    Vue = _Vue;
    // console.log(Vue);
    // console.log('vue router install');
    Vue.mixin({
      beforeCreate() {
        // 这里会打印两次,因为有俩vue实例(main.js中new Vue,App.vue中的vue),vue会给它们分别添加 beforeCreate 钩子
        // console.log('mixin里的一些数据');

        // 打印后会发现,只有根组件 root 才有 router 实例,也就是 new VueRouter 这个实例
        // 所以后边我们需要判断,当有router实例时,才执行上边的 init 方法
        console.log('当前vue实例:', this.$options.name, this.$options.router);

        // 找到根Vue实例,然后取出里边的router实例,并执行init方法
        if (this.$options.router) {
          this.$options.router.init();
        }
      }
    });
  }
}

export default VueRouter;