当前位置: 首页 > news >正文

手写路由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模式下,首页刷新到显示的整体流程:

1.将这个完整的url发到服务器nginx2.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,我们自己实现

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

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) //注意! 这是我们自己实现的文件,只是名字叫vuerouterconst 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 historyroutes //路由表给实例传过去
})export default router  //最后到处router

接下来就是正式实现router

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

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

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

用途:

  • 状态管理this.current用于存储当前路由的状态或信息,比如当前激活的路由路径、参数等。
  • 初始化:通过将其初始化为null,你可以在类的其他方法中根据需要更新this.current的值,而不会受到未初始化属性的影响。
  • 灵活性:将this.current初始化为null提供了灵活性,允许你在类的生命周期中的任何时刻为其分配一个具体的值。
//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类用于管理当前路由状态。
class VueRouter {// 可以看到,暂时传入了两个,一个是mode,还有一个是routes数组。因此,我们可以这样实现构造器constructor(options) {this.mode = options.mode || "hash"; //默认是hashthis.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语言来构建DOMreturn 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
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语言来构建DOMreturn 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
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-linkthis._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方法里面写就行
 // 定义$routerObject.defineProperty(Vue.prototype, "$router", {get() {console.log(this);return this._root._router;},});// 定义$routeObject.defineProperty(Vue.prototype, "$route", {get() {return this._root._router.history.current;},});

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

  • router-link:一个用于导航的<a>标签组件,根据路由模式(hashhistory)自动添加#或正常路径。点击时,如果是history模式,会阻止默认跳转行为,改为手动更新URL和路由状态。
  • router-view:一个用于渲染当前路由对应组件的占位符组件。它根据history.currentroutesMap找到对应的组件,并使用Vue的render函数渲染。
  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]);},});

完整代码

/** @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语言来构建DOMreturn 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-linkthis._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);}},});// 定义$routerObject.defineProperty(Vue.prototype, "$router", {get() {console.log(this);return this._root._router;},});// 定义$routeObject.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中注册一下就行

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中

<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应用中通过和组件实现页面导航和组件渲染。尽管这个实现相对简单,但它展示了Vue Router的核心概念和工作原理。


http://www.mrgr.cn/news/57737.html

相关文章:

  • 054_python基于爬虫与文本挖掘的网络舆情监控系统
  • 中阳国际金融市场的多样化资产配置策略及风险应对
  • GitHub上传文件
  • [COCI2015-2016#7] Prosti
  • ProteinMPNN中负对数似然损失函数解读
  • Web保存状态的手段(Session的使用)
  • 昇思MindSpore进阶教程--安装常见问题(下)
  • Spring Boot植物健康系统:智慧农业的新趋势
  • com.baomidou.mybatisplus.extension.service.IService用法详解及使用例子
  • 用phython处理当前路径的文件
  • 口含烟贴纸设计公司哪家好?
  • 算法复习核心题目策略总结,以便回顾
  • STM32 C语言基础知识
  • JavaWeb合集22-Apache POI
  • 某游戏的某促销活动,会向玩家推荐一个道具
  • 桂花网蓝牙网关X1000覆盖范围有多少?
  • Java笔试07
  • 1024程序员java纪念
  • 使用Selenium时,如何模拟正常用户行为?
  • 参加了十多个面试,一个offer也没拿到...为什么?
  • Base64编码
  • 真AI遇到招聘管理系统,帮助企业打造新质生产力
  • Vue3:横向滑动导航组件路由跳转保留滚动(条)量
  • HKC双模显示器评测报告 - HKC G27H7Pro
  • 1688API商品详情接口如何获取
  • 解锁PDF权限密码