「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」
前言
学习源码能让我们更加深入的了解工具的原理,拓宽我们的知识面,也便于我们在使用中排查问题。
需求分析
今天的目标是实现一个简易版的vue-router。那我们先做一下需求分析,今天希望这个插件能有如下的功能。
- 实现router-view组件
- 实现router-link组件
- 根据当前hash动态的修改router-view展示的内容
那么至于路由的一些钩子,以及嵌套路由这些,我们暂不考虑,先实现一个MVP(最小化可行产品(Minimum Viable Product))。
环境搭建
这步没什么要求,大家随意找一个vue2的项目即可,我直接用vue-cli初始化了一个最简单的项目,结构
安装vue-cli
npm install -g @vue/cli
初始化项目
vue create vue-router-demo
项目结构
初始化vue-router
vue add router
我们选hash模式,这里输入N
这样目录结构如下,我们看下router下面的index.js
启动项目
npm run serve
修改文件
这个时候router都是配好的,我们新建一个文件夹miniRouter,在下面新建一个index.js,将router下面的index.js拷贝过来,修改里面的vueRouter为MiniRouter,并新建一个同级的MiniRouter.js文件
修改main.js里面对router的引用为miniRouter
好了,这个时候,准备工作就已经都做好了。
MiniRouter实现
主要框架
-
我们使用MiniRouter首先是
import MiniRouter from './MiniRouter'。这说明,我们MiniRouter文件一定要export default MiniRouter。 -
MiniRouter使用方式如下,那么显然,MiniRouter一定是一个类
new MiniRouter({ routes }), -
我们有使用Vue.use去注册MiniRouter,那么我们一定要实现一个MiniRouter类的静态方法install。(这里不明白的小伙伴们自行去百度一下,Vue.use做了些什么)
那么基本的MiniRouter.js结构如下
// MiniRouter.js
class MiniRouter {
}
MiniRouter.install = function() {
}
export default MiniRouter;
router-link实现
首先,我们知道router-link最终是渲染成了一个a标签,那么我们肯定要去注册一个router-link组件,然后渲染成一个a标签。
全局注册组件需要使用Vue.component,那么这个Vue从哪里来呢,我们在MiniRouter里面引入vue吗?
当然不用,这样会加大MiniRouter.js的体积。Vue.use调用插件install方法的时候,会传入一个Vue构造函数。我们这里只需要稍加操作将这个Vue构造函数保存下来即可。
然后去初始化router-link组件。
// 提前申明一个Vue变量用于后序保存Vue构造函数
// 这里class里也能正常使用Vue
// 因为我们在路由index.js里先执行的Vue.use(MiniRouter),后执行的new MiniRouter
// 所以install方法是在class的constructor之前执行的
let Vue;
class MiniRouter {
}
MiniRouter.install = function(_Vue) {
Vue = _Vue;
Vue.component('router-link',{
// 这里可以去获取到用户使用router-link标签时传入的属性to
props: {
to: {
type: String,
require: true
}
},
render(h) {
return h('a',{
attrs: {
// 将组件的to写入到a标签的href属性里
href: '#' + this.to
}
// 通过this.$slots.default获取当前router-link组件默认插槽内的文本内容
}, this.$slots.default)
}
})
}
export default MiniRouter;
页面展示如下
可以看到Home和About文案都渲染出来了,href也有了,也可以点击 了。暂不用管router-view的报错,这个还没注册呢。
router-view实现
router-view组件里面,我们需要去根据当前url的hash,去路由表里匹配到对应的组件,然后渲染出来。那么这里几个关键因素如下
- url的hash如何动态获取?保存到哪里?
- 路由表去哪拿?
- 如何将组件渲染出来?
- 点击标签跳转后hash变了,如何让router-view组件响应式的渲染?
我们来一一解答
获取hash并保存
首选,获取hash很简单,直接window.location.hash.slice(1),即可获取到hash的'#'之后的内容。
由于我们是在install方法里面的Vue.component里要使用这个hash,在Vue.component里面,我们可以通过this访问到组件实例,那么组件实例也是vue实例,vue实例则可以访问到Vue构造函数原型上挂载的内容。
那我们就可以曲线救国,先把这个当前hash存放到MiniRouter实例上,这一步在MiniRouter构造函数里即可实现。然后再将MiniRouter实例挂载到Vue构造函数的原型上,这一步后面细说。
class MiniRouter {
constructor(){
this.current = window.location.hash.slice(1);
}
}
挂载MiniRouter实例到Vue构造函数上
接上一步,我们需要将MiniRouter实例挂载到Vue构造函数的原型上,这样router-view组件实例就能获取到这个hash了。MiniRouter实例是在new Vue的时候传进去的,所以我们可以通过this.$options.router获取到MiniRouter实例,即如下:
Vue.prototype.$router = this.$options.router;
那么这里又有个问题,我们在什么时候执行这段代码呢?在install里直接执行吗?
答案是当然不行。因为执行install方法的时候,vue根实例都还没创建,上哪拿$options?所以我们要等vue根实例创建完成,或者至少在创建过程中也行对吧,因为我们知道new一个构造函数的过程中,构造函数内部的this是指向这个新对象的。
这里有个办法,使用Vue.mixin将这段代码混入到beforeCreate钩子中,就是vue实例即将创建完成的时候,这时候我们可以通过this.$options.router拿到路由实例。这里我们还要判断一下,只有根实例创建的时候我们才这样做,因为只有根实例才有router属性,就是new Vue时候传进来的router,我们可以判断当前实例是否含有router属性来判断,。
那么我们现在的代码长这样
class MiniRouter {
constructor(){
this.current = window.location.hash.slice(1);
}
}
MiniRouter.install = function(_Vue) {
Vue = _Vue;
Vue.mixin({
beforeCreate(){
if(this.$options.router){
Vue.prototype.$router = this.$options.router;
}
}
})
Vue.component('router-link',{
// 这里可以去获取到用户使用router-link标签时传入的属性to
...
})
}
获取路由表
我们的路由表是在路由实例创建的时候传进去的,就是MiniRouter文件夹下面index.js里面的这段代码
const router = new MiniRouter({
routes
})
那么我们可以在MiniRouter类的construtor里面通过options获取到路由表。我们获取到路由表之后,直接将它挂载到路由实例上,方便后序使用。
class MiniRouter {
constructor(options){
this.$options = options;
this.current = window.location.hash.slice(1);
}
}
由于我们之前已经将MiniRouter路由实例挂载到了Vue构函数的原型上了,那么所有组件实例都能通过this.$router去原型上获取到路由实例,进而也可以获取到路由实例里面的路由表,
那么通过这些操作,我们就成功获取到路由表了,即this.$router.routes
监听hash变化
这个容易,我们可以通过addEventListener去监听hashchange事件,我们获取到hash后,去修改路由实例的current属性即可
class MiniRouter {
constructor(options){
this.$options = options;
this.current = window.location.hash.slice(1);
window.addEventListener('hashchange',()=>{
this.current = window.location.hash.slice(1);
})
}
}
router-view的组件渲染
这里有个技巧,刚才我们在渲染router-link的时候,使用render函数的h函数,即createElement方法创建了a标签。
其实h函数可以直接传入组件模板并渲染出来,大家可以试一下
let Vue;
// 在MiniRouter.js最上面引入入Home组件
import Home from '../views/Home'
...
MiniRouter.install = function(_Vue) {
Vue = _Vue;
...
Vue.component('router-view',{
render(h) {
// 这里直接传入Home组件模板
return h(Home)
}
})
}
那么这就给了我们一个思路了,我们可以在注册router-view组件的时候,通过路由实例里面存放的当前hash去路由表里匹配出应该渲染的组件模板,然后传入h函数即可。
class MiniRouter {
constructor(options){
this.$options = options;
this.current = window.location.hash.slice(1);
window.addEventListener('hashchange',()=>{
this.current = window.location.hash.slice(1);
})
}
}
MiniRouter.install = function(_Vue) {
Vue = _Vue;
...
Vue.component('router-view',{
render(h) {
let component = null;
// 这里this是当前组件实例
// this.$router是Vue构造函数原型上的路由实例
let current = this.$router.current;
// this.$router.$options是我们上面在MiniRouter类的constructor里面绑在路由实例上的
let route = this.$router.$options.routes.find((route)=>{
return route.path === current
})
component = route.component;
return h(component)
}
})
}
刷新页面,组件已经出来了!!!
但是
点击Home和About,hash能变化,可页面不能切换。。。
我们进入下一个任务,go
当前hash的响应式处理
我们之所以点击Home和About,组件不能切换,是因为,Vue.component去注册router-view的render函数,只执行一次,执行完之后,它就不会再执行了。
那我们希望,只要current变化,router-view组件就能重新渲染一遍,听到这个是不是很耳熟,这不就是vue的数据响应式么。
我们只需要把路由实例的current属性变成响应式数据,那么所有使用current的组件都被vue自动的当做依赖收集起来,当current变化的时候,vue会去通知所有的依赖更新。
那我们用什么方法将属性变成响应式数据呢?
Object.defineProperty可以吗?
不行,因为这只能给该属性加一个数据劫持,就是拦截它的读取,但是我们需要的是修改后能触发依赖的更新,它做不到。
那Vue.set可以吗?众所周知它可以为对象添加响应式属性
但是不行,因为Vue.set(object, propertyName, value),需要object本来就是响应式的。
其实vue提供了一个很好用的创建响应式数据的方法,Vue.util.defineReactive,用该方法可以给某个对象注册一个响应式属性,那我们试一试,修改一下MiniRouter的constructor方法
class MiniRouter {
constructor(options){
this.$options = options;
Vue.util.defineReactive(this, 'current', window.location.hash.slice(1))
window.addEventListener('hashchange',()=>{
this.current = window.location.hash.slice(1);
})
}
}
你们再试一下,点击Home和About看能不能切换!!
总结
今天实现的这个MiniRouter,是一个简之又简的简版,旨在大概跑一遍vue-router是如何实现组件注册,响应式路由切换的。真正的vue-router比这个复杂的多。
大家有兴趣可以跟着敲一遍,感受一下。
完成代码如下
// MiniRouter.js
let Vue;
class MiniRouter {
constructor(options){
this.$options = options;
Vue.util.defineReactive(this, 'current', window.location.hash.slice(1))
window.addEventListener('hashchange',()=>{
this.current = window.location.hash.slice(1);
})
}
}
MiniRouter.install = function(_Vue) {
Vue = _Vue;
Vue.mixin({
beforeCreate(){
if(this.$options.router){
Vue.prototype.$router = this.$options.router;
}
}
})
Vue.component('router-link',{
// 这里可以去获取到用户使用router-link标签时传入的属性to
props: {
to: {
type: String,
require: true
}
},
render(h) {
return h('a',{
attrs: {
// 将组件的to写入到a标签的href属性里
href: '#' + this.to
}
// 通过this.$slots.default获取当前组件默认插槽内的文本内容
}, this.$slots.default)
}
})
Vue.component('router-view',{
render(h) {
let component = null;
let current = this.$router.current;
let route = this.$router.$options.routes.find((route)=>{
return route.path === current
})
component = route.component;
return h(component)
}
})
}
export default MiniRouter;