Vue-Router源码实现详解

Vue-Router源码实现详解

1.Hash模式

  • hash就是url中#后面的部分
  • hash改变时,页面不会从新加载,会触发hashchange事件,去监听hash改变,而且也会被记录到浏览器历史记录中
  • vue-router的hash模式,主要是通过hashchange事件,根据hash值找到对应的组件去进行渲染(源码里会先判断浏览器支不支持popstate事件,如果支持,则是通过监听popstate事件,如果不支持,则监听hashchange事件)

hash模式页面跳转不刷新

根据http协议所示,url中hash改变请求是不会发送到服务端的,不管怎么location跳转,或者url上直接加上hash值回车,他都一样,请求是不会发送到服务端。 但是我们的系统里又引入了vue-router,其中hashchange这个事件监听到了hash的变化,从而触发了组件的更新,也就绘制出了相应的页面

2.History模式

  • 通过history.pushstate去修改页面的地址
  • 当history改变时,会触发popstate事件,所以可以通过监听popstate事件获取路由地址
  • 根据当前路由地址找到对应组件渲染

history模式,切换路由时页面刷新

看一下正确的history模式下,首页刷新到显示的整体流程:

bash 复制代码
1.将这个完整的url发到服务器nginx

2.ngix需要配置用这个uri在指给前端index.html(因为根本没有任何一个服务器提供了这个url路由,如果直接访问的话就是404,所以就要指回给前端,让前端自己去根据path来显示)
location / {
   root   /usr/share/nginx/html/store;//项目存放的地址
   index  index.html index.htm;
   try_files $uri $uri/ /index.html;//history模式下,需要配置它
}
 所以try_files $uri $uri/的意思就是,比如http://test.com/example先去查找单个文件example,如果example不存在,则去查找同名的文件目录/example/,如果再不存在,将进行重定向index.html(只有最后一个参数可以引起一个内部重定向)
 凡是404的路由,都会被重定向到index.html,这样就显示正确了
 
3.此时nginx将这个请求指回了前端的index.html,index.html中开始加载js,js中已有vue-router的代码,vue-router自动触发了popstate这个事件,在这个事件回调中,绘制了这个path下对应的页面组件

3.实现vue-router

VueRouter需要做以下这些事情

  1. 实现VueRouter根据不同模式进行不同处理
  2. 根据传入的路由配置,生成对应的路由映射
  3. init函数监听hashchange或popState事件,浏览器记录改变时重新渲染router-view组件
  4. 实现install静态方法
  5. 给Vue实例挂载router实例
  6. 注册全局组件和, router-view组件通过当前url找到对应组件进行渲染,并且url改变时,重新渲染组件,router-link则渲染为a标签
  7. 使用Object.defineProperty在Vue的原型上定义$router$route属性

代码实现

首先在创建文件 router/index.js,router/my-router.js

在index中我就不做多讲解了,和平常vue-router一样的配置,只是不需要vue-router,我们自己实现

一下都会有详细的注释,每一项的作用

javascript 复制代码
import Vue from 'vue'
import VueRouter from './my-router';  //实现router文件
import HomeView from '../views/HomeView.vue'  //home文件
import about from '../views/AboutView.vue' //about文件


Vue.use(VueRouter) //注意! 这是我们自己实现的文件,只是名字叫vuerouter

const routes = [   //这是我们的路由表
  {
    path: '/home',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: about
  }
]

const router = new VueRouter({  //创建实例
  mode: 'history', //模式 hash history
  routes //路由表给实例传过去
})

export default router  //最后到处router

接下来就是正式实现router

首先在my-router最顶部,声明一个变量Vue并将其初始化为null

javascript 复制代码
//my-router.js
let Vue = null; //保存Vue的构造函数,在插件中要使用,保留在将来为Vue分配一个值,减少全局命名空间的污染

定义一个HistoryRoute的类,并在其构造函数中将this.current也初始化为null

用途:

  • 状态管理this.current用于存储当前路由的状态或信息,比如当前激活的路由路径、参数等。
  • 初始化 :通过将其初始化为null,你可以在类的其他方法中根据需要更新this.current的值,而不会受到未初始化属性的影响。
  • 灵活性 :将this.current初始化为null提供了灵活性,允许你在类的生命周期中的任何时刻为其分配一个具体的值。
javascript 复制代码
//my-router.js
let Vue = null;  //保存Vue的构造函数,在插件中要使用,保留在将来为Vue分配一个值,减少全局命名空间的污染
class HistoryRoute {
  constructor() {
    this.current = null;
  }
}

定义一个HistoryRoute的类

构造函数

  • constructor(options):接收一个options对象,该对象包含两个属性:moderoutesmode指定路由模式(hashhistory),routes是一个路由配置数组,每个路由配置对象包含pathcomponent属性。
  • this.changeMap:将routes数组转换为一个对象(Map),以path为键,component为值,方便后续根据路径快速查找对应的组件。
  • Vue.util.defineReactive(this, "history", new HistoryRoute());this.history = new HistoryRoute();:这里设置了history属性,后者覆盖了前者。HistoryRoute类用于管理当前路由状态。
kotlin 复制代码
class VueRouter {
  // 可以看到,暂时传入了两个,一个是mode,还有一个是routes数组。因此,我们可以这样实现构造器
  constructor(options) {
    this.mode = options.mode || "hash"; //默认是hash
    this.routes = options.routes || [];  //默认为空
    // 由于直接处理数组比较不方便,所以我们做一次转换,采用path为key,component为value的方式
    this.routesMap = this.changeMap(this.routes);
    // 我们还需要在vue-router的实例中保存当前路径(在包含一些例如params信息,其实就是$route),所以我们为了方便管理,使用一个对象来表示:
    Vue.util.defineReactive(this, "history", new HistoryRoute());
    this.history = new HistoryRoute();
  }
  changeMap(routes) {
    // 使用render函数我们可以用js语言来构建DOM
    return routes.reduce((pre, next) => {
      console.log(pre);
      pre[next.path] = next.component;
      console.log(pre);
      return pre;
    }, {});
  }
}

添加init 方法:

  • 根据mode的不同,为window添加相应的事件监听器,以监听路由变化(hashchangepopstate事件),并更新history.current属性。
  • 在页面加载时(load事件),也根据当前URL设置history.current
kotlin 复制代码
class VueRouter {
  // 可以看到,暂时传入了两个,一个是mode,还有一个是routes数组。因此,我们可以这样实现构造器
  constructor(options) {
    this.mode = options.mode || "hash";
    this.routes = options.routes || [];
    // 由于直接处理数组比较不方便,所以我们做一次转换,采用path为key,component为value的方式
    this.routesMap = this.changeMap(this.routes);
    // 我们还需要在vue-router的实例中保存当前路径(在包含一些例如params信息,其实就是$route),所以我们为了方便管理,使用一个对象来表示:
    Vue.util.defineReactive(this, "history", new HistoryRoute());
    this.history = new HistoryRoute();
    this.init();
  }
  init() {
    // 如果是hash模式
    if (this.mode === "hash") {
      location.hash ? void 0 : (location.hash = "/");
      window.addEventListener("load", () => {
        this.history.current = location.hash.slice(1);
      });
      window.addEventListener("hashchange", () => {
        console.log(location.hash.slice(1))
        this.history.current = location.hash.slice(1);
      });
    }
    // 如果是history模式
    if (this.mode === "history") {
      location.pathname ? void 0 : (location.pathname = "/");
      window.addEventListener("load", () => {
        console.log(location.pathname)
        this.history.current = location.pathname;
      });
      window.addEventListener("popstate", () => {
        console.log(location.pathname)
        this.history.current = location.pathname;
      });
    }
  }
  changeMap(routes) {
    // 使用render函数我们可以用js语言来构建DOM
    return routes.reduce((pre, next) => {
      console.log(pre);
      pre[next.path] = next.component;
      console.log(pre);
      return pre;
    }, {});
  }

Vue Router作为一个Vue插件,需要创建instll方法

  1. 设置Vue实例:
    • Vue = v;:将传入的Vue实例赋值给全局变量Vue,以便后续使用。
  2. 全局混入:
    • 使用Vue.mixin在Vue的生命周期钩子beforeCreate中注入代码,用于处理路由相关的初始化。
    • 在根组件中,保存_router_root属性,分别指向VueRouter实例和根组件自身。
    • 对于非根组件,通过$parent找到根组件,从而访问到_router_root
kotlin 复制代码
VueRouter.install = (v) => {
  Vue = v;
  // vue-router还自带了两个组件,分别是router-link和router-view  在Vue.use(VueRouter)的时候加载的 所以我们要写在install里面
  // 新增代码
  Vue.mixin({
    beforeCreate() {
      // 如果是根组件
      if (this.$options && this.$options.router) {
        console.log(this.$options)
        // 将根组件挂载到_root上
        this._root = this;
        this._router = this.$options.router;
        // 拦截router-link
        this._router.mode === "history" && document.addEventListener("click", (e) => {
          if (e.target.className === "router-link-to") {
            // 阻止默认跳转事件
            e.preventDefault();
            // 手动改变url路径
            console.log(e.target.getAttribute("href"))
            history.pushState(null, "", e.target.getAttribute("href"));
            // 为current赋值url路径
            this._router.history.current = location.pathname;
          }
        });
      } else {
        // 如果是子组件
        // 将根组件挂载到子组件的_root上
        this._root = this.$parent && this.$parent._root;
        console.log(this._root);
      }
    },
  });
};
  • 使用Object.defineProperty在Vue的原型上定义$router$route属性,以便在任何Vue组件中通过this.$routerthis.$route访问到VueRouter实例和当前路由信息。
  • 在install方法里面写就行
javascript 复制代码
 // 定义$router
  Object.defineProperty(Vue.prototype, "$router", {
    get() {
      console.log(this);
      return this._root._router;
    },
  });
  // 定义$route
  Object.defineProperty(Vue.prototype, "$route", {
    get() {
      return this._root._router.history.current;
    },
  });

定义全局组件(install方法里面添加)

  • router-link:一个用于导航的<a>标签组件,根据路由模式(hashhistory)自动添加#或正常路径。点击时,如果是history模式,会阻止默认跳转行为,改为手动更新URL和路由状态。
  • router-view:一个用于渲染当前路由对应组件的占位符组件。它根据history.currentroutesMap找到对应的组件,并使用Vue的render函数渲染。
kotlin 复制代码
  Vue.component("router-link", {
    props: {
      to: String,
    },
    render(h) {
      const mode = this._root._router.mode;
      let to = mode === "hash" ? "#" + this.to : this.to;
      return h(
        "a",
        {
          attrs: {
            href: to,
          },
          // 新增代码
          class: "router-link-to",
        },
        this.$slots.default
      );
    },
  });
  Vue.component("router-view", {
    render(h) {
      const current = this._root._router.history.current;
      const routesMap = this._root._router.routesMap;
      return h(routesMap[current]);
    },
  });

完整代码

kotlin 复制代码
/*
 * @Author: hukai huzhengen@gmail.com
 * @Date: 2023-06-08 17:49:08
 * @LastEditors: hukai huzhengen@gmail.com
 * @LastEditTime: 2024-10-24 11:08:00
 * @FilePath: \vue源码\vue-router-yuanma\src\router\my-router.js
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
let Vue = null;
class HistoryRoute {
  constructor() {
    this.current = null;
  }
}
// 因为router时new出来的 并且穿了一个对象 配置的路由由此可知router是一个class类
class VueRouter {
  // 可以看到,暂时传入了两个,一个是mode,还有一个是routes数组。因此,我们可以这样实现构造器
  constructor(options) {
    this.mode = options.mode || "hash";
    this.routes = options.routes || [];
    // 由于直接处理数组比较不方便,所以我们做一次转换,采用path为key,component为value的方式
    this.routesMap = this.changeMap(this.routes);
    // 我们还需要在vue-router的实例中保存当前路径(在包含一些例如params信息,其实就是$route),所以我们为了方便管理,使用一个对象来表示:
    Vue.util.defineReactive(this, "history", new HistoryRoute());
    this.history = new HistoryRoute();
    this.init();
  }
  init() {
    // 如果是hash模式
    if (this.mode === "hash") {
      location.hash ? void 0 : (location.hash = "/");
      window.addEventListener("load", () => {
        this.history.current = location.hash.slice(1);
      });
      window.addEventListener("hashchange", () => {
        console.log(location.hash.slice(1))
        this.history.current = location.hash.slice(1);
      });
    }
    // 如果是history模式
    if (this.mode === "history") {
      location.pathname ? void 0 : (location.pathname = "/");
      window.addEventListener("load", () => {
        console.log(location.pathname)
        this.history.current = location.pathname;
      });
      window.addEventListener("popstate", () => {
        console.log(location.pathname)
        this.history.current = location.pathname;
      });
    }
  }
  changeMap(routes) {
    // 使用render函数我们可以用js语言来构建DOM
    return routes.reduce((pre, next) => {
      console.log(pre);
      pre[next.path] = next.component;
      console.log(pre);
      return pre;
    }, {});
  }
}

// 通过Vue.use 知道里面有一个install方法 并且第一个参数是Vue实例
VueRouter.install = (v) => {
  Vue = v;
  // vue-router还自带了两个组件,分别是router-link和router-view  在Vue.use(VueRouter)的时候加载的 所以我们要写在install里面
  // 新增代码
  Vue.mixin({
    beforeCreate() {
      // 如果是根组件
      if (this.$options && this.$options.router) {
        console.log(this.$options)
        // 将根组件挂载到_root上
        this._root = this;
        this._router = this.$options.router;
        // 拦截router-link
        this._router.mode === "history" && document.addEventListener("click", (e) => {
          if (e.target.className === "router-link-to") {
            // 阻止默认跳转事件
            e.preventDefault();
            // 手动改变url路径
            console.log(e.target.getAttribute("href"))
            history.pushState(null, "", e.target.getAttribute("href"));
            // 为current赋值url路径
            this._router.history.current = location.pathname;
          }
        });
      } else {
        // 如果是子组件
        // 将根组件挂载到子组件的_root上
        this._root = this.$parent && this.$parent._root;
        console.log(this._root);
      }
    },
  });
  // 定义$router
  Object.defineProperty(Vue.prototype, "$router", {
    get() {
      console.log(this);
      return this._root._router;
    },
  });
  // 定义$route
  Object.defineProperty(Vue.prototype, "$route", {
    get() {
      return this._root._router.history.current;
    },
  });
  Vue.component("router-link", {
    props: {
      to: String,
    },
    render(h) {
      const mode = this._root._router.mode;
      let to = mode === "hash" ? "#" + this.to : this.to;
      return h(
        "a",
        {
          attrs: {
            href: to,
          },
          // 新增代码
          class: "router-link-to",
        },
        this.$slots.default
      );
    },
  });
  Vue.component("router-view", {
    render(h) {
      const current = this._root._router.history.current;
      const routesMap = this._root._router.routesMap;
      return h(routesMap[current]);
    },
  });
};
export default VueRouter;

最后在main.js中注册一下就行

javascript 复制代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

APP.vue中

xml 复制代码
<template>
  <div id="app">
    <nav>
      <router-link to="/home">Home</router-link> |
      <router-link to="/about">About</router-link>
    </nav>
    <router-view/>
  </div>
</template>
<script>
export default {
  name:"Router",
  mounted(){
    console.log(this.$router)
  }
}
</script>
<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

nav {
  padding: 30px;
}

nav a {
  font-weight: bold;
  color: #2c3e50;
}

nav a.router-link-exact-active {
  color: #42b983;
}
</style>

总结

这段代码通过定义VueRouter类和install方法,实现了一个简化版的Vue Router。它允许开发者定义路由规则,并在Vue应用中通过<router-link><router-view>组件实现页面导航和组件渲染。尽管这个实现相对简单,但它展示了Vue Router的核心概念和工作原理。

相关推荐
qq_3643717215 天前
VueRouter 导航故障问题
javascript·vue.js·前端框架·vue-router
奶昔不会射手2 个月前
vue3项目,本地页面正常显示,打包后页面空白
vue3·vue-router
kidding7233 个月前
前端的面试题
前端·前端框架·vue-router·history·hash·abstract·v-bind
又言又语3 个月前
【Vue3】编程式路由导航
vue·vue3·vue-router·编程式路由导航
又言又语3 个月前
【Vue3】配置路由规则props
vue·vue3·vue-router·props·路由传参
又言又语3 个月前
【Vue3】路由基础
vue·vue3·路由·vue-router
Java_慈祥4 个月前
懂个锤子Vue VueRouter路由深入浅出
前端·vue.js·vue-router
三六4 个月前
Vue 3 的5种路由守卫
前端·vue.js·vue-router
eiko莉4 个月前
vue权限路由-权利分配游戏
vue.js·vue-router