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需要做以下这些事情
- 实现VueRouter根据不同模式进行不同处理
- 根据传入的路由配置,生成对应的路由映射
- init函数监听hashchange或
popState
事件,浏览器记录改变时重新渲染router-view
组件 - 实现install静态方法
- 给Vue实例挂载router实例
- 注册全局组件和,
router-view
组件通过当前url找到对应组件进行渲染,并且url改变时,重新渲染组件,router-link则渲染为a标签 - 使用
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
对象,该对象包含两个属性:mode
和routes
。mode
指定路由模式(hash
或history
),routes
是一个路由配置数组,每个路由配置对象包含path
和component
属性。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
添加相应的事件监听器,以监听路由变化(hashchange
或popstate
事件),并更新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方法
- 设置Vue实例:
Vue = v;
:将传入的Vue实例赋值给全局变量Vue
,以便后续使用。- 全局混入:
- 使用
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.$router
和this.$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>
标签组件,根据路由模式(hash
或history
)自动添加#
或正常路径。点击时,如果是history
模式,会阻止默认跳转行为,改为手动更新URL和路由状态。router-view
:一个用于渲染当前路由对应组件的占位符组件。它根据history.current
和routesMap
找到对应的组件,并使用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的核心概念和工作原理。