五分钟手写简版VueRouter(秒理解)
前言
Vue Router 是 Vue.js 官⽅的路由管理器。它和 Vue.js 的核⼼深度集成,让构建单⻚⾯应⽤变得很简单。
很多同学在面试的时候只能说出Router内部是依赖hash以及history这两种模式实现,但是内部原理就知其然而不知所以然了。本文通过手写简版Hash模式的VueRouter,快速拆分理解Router内部机制。
1.使用回顾
开始前,我们先来快速回顾VueRouter的使用
1.安装: vue add router
2.router.js
引入vue-router并使用
import Router from 'vue-router'
Vue.use(Router)
创建router实例并导出
const routes=[{...}]
export default new Router({routes})
复制代码
3.main.js中
引入router
import router from './router'
并在根组件上添加实例
new Vue({
router,
}).$mount("#app");
复制代码
4.步骤四:App.vue中添加路由视图及对应的代码编写
<router-view></router-view>
<router-link to="/">Home</router-link> <router-link to="/other">other</router-link>
this.$router.push('/')
this.$router.push('/other')
复制代码
2.问题
回顾完Router的使用,我们开始正文。但是在这之前,我们要先思考如下几个问题。
2.1 我们要通过VueRouter解决什么问题?
答:实现在单⻚⾯应⽤程序中,url地址发⽣变化的时候,页面不能刷新,但是显示与url地址对应的内容
2.2 那么根据上面的需求,我们有什么实现的办法,任务如何做拆分?
答:
- url地址发生变化,页面不刷新:hash\history Api
- 根据url地址显示对应内容:监听路由,拿到当前地址,从用户的配置项中找到对应componet,并在router-view中渲染
2.3 我们要实现一个VueRouter,我们有具体哪些工作要做呢?
答:
首先肯定是实现一个VueRouter类,在类的内部要做4件事情。
- 保存用户在生成router实例时候的配置选项。
- 监听当前的hash地址,并把该地址处理成响应式数据
-
实现插件的install方法
vueRouter是一个插件,插件在Vue中使用的时候是通过Vue.use()方法来实现注册,而Vue.use方法其实就是调用插件的install方法,并把Vue实例以参数形式向install方法传递,所以我们第一步要实现一个插件的install方法,方法内部就是我们要在挂载的时候的一些具体实现。
-
router实例的挂载
-
注册两个router-link和router-view
routerlink本质上就是渲染成一个a标签而router-view的功能就是根据当前的hash,在用户配置的路由选项中找到对应的componet组件,并渲染出来
-
3.代码实现
好,回答完上述问题呢,我们开始手撸一个简版的Vue-Router,同时各位也可以从VueRouter源码中去看真实的具体实现。
3.1 实现一个VueRouter类
- 在constructor中保存配置项
- 对当前路径current(hash)的初始化和响应式处理,供后面的的router-view组件寻找路由组件使用
class vueRouter {
constructor(options) {
// 选项中包含路由配置信息,保存
this.$options = options;
// 需要将current属性声明为响应式的
//Vue.util.defineReactive是Vue提供的响应式方法
Vue.util.defineReactive(
this,
"current",
window.location.hash.slice(1) || "/"
);
// 2.监听hashchang事件,并且在变化的时候响应
window.addEventListener("hashchange", () => {
// hash: #/about
this.current = window.location.hash.slice(1);
});
}
}
复制代码
3.2 实现vueRouter类的install方法,该方法会在vue.use时执行,包含2个功能,
- 挂载实例router
let Vue
vueRouter.install=function(_vue){
//保存vue.use时候传入的Vue构造函数,供后续使用
Vue=_vue
//由于在Vue.use(VueRouter)时,router实例并未生成(详见最开始的使用回顾),因此通过在use的时候通过Vue.mixin声明,在Vue实例化的时候才会去挂载router,也就是mian.js中的new Vue({router,render: h => h(App)}).$mount('#app')
Vue.mixin({
beforeCreate(){
if(this.option.router) Vue.prototype.$router=this.option.router
}
})
}
复制代码
- 注册router-link及router-view全局组件
let Vue
vueRouter.install=function(_vue){
Vue=_vue
Vue.mixin({
beforeCreate(){
if(this.option.router) Vue.prototype.$router=this.option.router
}
})
//注册router-link及router-view全局组件
Vue.component("router-link", {
//接受to参数,也就是我们写的 <router-link to="/about"></router-link>
props: {
to: {
type: String,
required: true,
},
},
//component第二个参数可以通过render函数返回虚拟dom,
render(h) {
// h是createElement, 返回vnode
// <router-link to="/about"></router-link>
// <a href="#/about"></a>
// return <a href={'#' + this.to}>{this.$slots.default}</a>
//这里h函数表达的是渲染成一个 <a href="#/other"></a>的形式
return h(
"a",
{
attrs: {
href: "#" + this.to,
},
},
this.$slots.default
);
},
});
Vue.component("router-view", {
render(h) {
// 数据响应式:数据变化可侦听,使用这些数据组件就会和响应式数据产生依赖关系
// 将来如果响应式数据发生变化,这些组件会重新渲染
let component = null;
//这里从this.$router中获取用户的配置项(也就是我们常写的路由表)中找到和当前路由实例路径current匹配的组件,把该组件通过h函数渲染出来
//注意:这个current是响应式的,是window.location.hash截取出来的,只要这个current发生变化,那么这个组件就会重新渲染,达到路由切换的目的
const route = this.$router.$options.routes.find(
(route) => route.path === this.$router.current
);
if (route) {
component = route.component;
}
// 1.获取hash部分,获取path
// 2.根据path,从路由表中获取组件
return h(component);
},
});
};
}
复制代码
至此,就实现了一个依赖Vue的响应式处理,对不同hash值匹配不同组件并在routerView组件中渲染的简易路由。当然真实源码考虑的情况要比现在的情况复杂的多,后续会补上嵌套路由的分析及具体实现。
4.完整代码
vue-router.js
let Vue;
// vue插件形式:
// 实现一个install方法,该方法会在use的时候被调用
class vueRouter {
constructor(options) {
// 选项中包含路由配置信息,保存
this.$options = options;
// 需要将current属性声明为响应式的
Vue.util.defineReactive(
this,
"current",
window.location.hash.slice(1) || "/"
);
// 2.监听hashchang事件,并且在变化的时候响应
window.addEventListener("hashchange", () => {
this.current = window.location.hash.slice(1);
});
}
}
// 形参1是Vue构造函数
VueRouter.install = function(_Vue) {
// 传入构造函数,我们可以修改它的原型,起到扩展的作用
Vue = _Vue;
// install中this是KVueRouter
// 1.注册$router
// 延迟执行接下来代码,等到router实例创建之后
// 全局混入:Vue.mixn
Vue.mixin({
beforeCreate() {
// 此处this指的是组件实例
if (this.$options.router) {
Vue.prototype.$router = this.$options.router;
}
},
});
// 2.注册router-link和router-view全局组件
Vue.component("router-link", {
props: {
to: {
type: String,
required: true,
},
},
render(h) {
// h是createElement, 返回vnode
// 获取插槽内容
// <router-link to="/about"></router-link>
// <a href="#/ohter"></a>
// return <a href={'#' + this.to}>{this.$slots.default}</a>
return h(
"a",
{
attrs: {
href: "#" + this.to,
},
},
this.$slots.default
);
},
});
Vue.component("router-view", {
render(h) {
// 数据响应式:数据变化可侦听,使用这些数据组件就会和响应式数据产生依赖关系
// 将来如果响应式数据发生变化,这些组件会重新渲染
// 0.获取router实例
// console.log(this.$router.$options, this.$router.current);
let component = null;
const route = this.$router.$options.routes.find(
(route) => route.path === this.$router.current
);
if (route) {
component = route.component;
}
// 1.获取hash部分,获取path
// 2.根据path,从路由表中获取组件
return h(component);
},
});
};
export default VueRouter;
复制代码