vueRouter 实现原理

前置知识

想了解vueRouter,需要先了解vue中的插件混入Vue.observable插槽render函数运行时和完整版Vue的概念,如果没有了解,可以先在官网复习一波。

vueRouter两种模式

总所周知,vueRouter分为hash模式和history模式;两种区别如下:

模式名称 实现原理
Hash模式 hash模式以url中#号后面的内容作为路径,通过监听hashchange事件获取到当前路由地址然后找到对应的组件进行渲染
history模式 通过popstate事件,根据当前路由地址找到相应的组件进行渲染(需要服务端支持)

实现思路

先看一下我们平时使用vueRouter的关键代码:

// router/index.js
// 1.引入VueRouter
import VueRouter from 'vue-router'
// 2.引入vue
import Vue from 'vue'
// 3. 注册路由插件
Vue.use(VueRouter)
// 4.添加路由规则
const routes = [
    {
        path: '/',
        name: 'index',
        component: Layout
    },
]
// 5.创建路由对象,传入路由规则
const router = new VueRouter({routes})
// 6.导出router
export default router

// main.js
import Vue from 'vue'
import router from './router'
new Vue({
  // 7. 注册 router 对象
  router,
  render: h => h(App)
}).$mount('#app')

  • 这里我们需要解一下Vue.use()方法:该方法至少传入一个参数,该参数类型必须是 ObjectFunction,如果是 Object 那么这个 Object 需要定义一个 install 方法,如果是 Function 那么这个函数就被当做 install 方法。在Vue.use()执行时 install 会默认执行,当 install 执行时第一个参数就是 Vue,其他参数是 Vue.use()执行时传入的其他参数;同时Vue.use()会自动阻止多次注册相同插件。

了解完使用流程,我们来分析一下它的实现思路:

  • 创建VueRouter插件,静态方法install,在install方法中,判断插件是否已经被加载,同时在vue加载的时候把传入的router对象挂载到vue实例上。
  • 创建VueRouter类:
    • 初始化options(记录构造函数中传入的对象)、routeMap(记录路由地址和组件的对应关系)、data(相应式的对象,可以记录当前路由地址,当路由地址变化时候,对应的组件做相应的更新)
    • 创建initRouteMap()方法,遍历所有路由信息,把组件和路由的映射记录到routeMap对象中
    • 注册popstate事件,当路由地址发生变化,重新记录当前的路径
    • 创建router-linkrouter-view组件
    • 当路径改变的时候通过当前路径在routerMap对象中找到对应的组件,渲染router-view

分析完大概思路,我们就可以创建文件开始编写。

实现 install 方法

首先我们需要创建一个vueRouter文件夹,里面创建一个index.js 文件,我们在上面分析了一下VueRouter在使用时候有new的操作,所以我们使用ES6class来实现。具体步骤如下:

// 设置全局变量
let _Vue = null
class VueRouter {
    static install(Vue) {
        // 1.判断当前插件是否安装
        if(VueRouter.install.isInstalled) return;
        VueRouter.install.isInstalled = true
        // 2.把vue构造函数记录到全局变量
        _Vue = Vue
        // 3.把创建vue实例的时候传入的router对象挂载到vue全局对象上
        _Vue.mixin[{
            beforeCreate() {
              // 判断当前实例上是否有router
                if(this.$options.router) {
                    _Vue.prototype.$router = this.$options.router
                }
            }
        }]
    }
    constructor(options) {
        this.options = options;
        this.routerMap = {}
    }
}

constructor 构造函数

constructor构造函数中,首先我们先把传入的options保存,然后初始化routerMap,到时候需要便利传入进来的路由规则,然后以键值对的形式存入到routerMap中,最后,我们需要创建一个响应式的data对象,Vue中提供了 observable 方法来帮助我们创建响应式的对象,使用方法也比较简单,在data中我们设置一个变量current来记录当前路由。

constructor(options) {
	// 记录构造函数中传入的options属性
	this.options = options
	// 初始化routerMap,用于记录路由表
	this.routerMap = {}
	// 创建响应式的data对象
	this.data = _Vue.observable({
	// 当前路由对象,用于记录当前路由
		current: '/'
	})
}

createRouterMap 便利路由规则

在上一步我们已经在构造函数中初始化了routerMap对象,这里我们需要写个方法去便利路由规则,然后存储到routerMap中。代码如下:

createRouterMap() {
	// 便利路由规则,吧路由规则挂载到routerMap上
	this.$options.router.forEach(item => {
		this.routerMap[item.path] = item.component
	})
}

创建组件

在使用vueRouter时,我们会用到router-link组件和router-view组件,这俩功能就不细说了,创建组件也是比较简单的,vue提供了 Vue.component方法,使用方式如下:

initComponents(Vue) {
	Vue.component('router-link',{
		props: {
			to: String
		},
		render(h) {
			return h('a',{
				attrs: {
					href: this.to
				},
			},this.$slots.default)
		},
	})
	Vue.component('router-view',{
		render(h) {
			// 获取当前路由匹配到的组件
			const component = self.routerMap[self.data.current]
			return h(component)
		}
	})
}

创建完成后,我们还需要调用一下,目前初始化函数有两个:便利路由规则方法、创建组件方法,我们可以再写一个init方法,来调用初始化的函数:

// 初始化方法
init() {
	this.createRouterMap()
	this.initComponents(_Vue)
}
  • 同时我们需要在constructor构造函数中调用一下:
constructor(options) {
    this.options = options
    this.routerMap = {}
    this.data = _Vue.observable({
      current: '/'
    })
    // 初始化方法
    this.init()
}

接下来我们可以试着在router文件夹中把引入的VueRouter插件改成我们写的插件:

// router/index.js
import VueRouter from '../vueRouter'

然后运行项目看是否显示成功。

实现点击事件

前面我们已经添加了router-link组件,接下来我们需要点击的时候去更改data中的current来记录当前的路径:

Vue.component('router-link',{
	props: {
		to: String
	},
	render(h) {
		return h('router-link',{
			attrs: {
				href: this.to
			},
			on: {
				click: this.clickHandler
			}
		},this.$slots.default)
	},
	methods: {
		clickHandler(e) {
			// 改变url中的路径
			history.pushState({}, '', this.to)
			// 修改响应式data中的current
			self.data.current = this.to
			// 阻止a标签默认事件
			e.preventDefault()
		}
	}
})

我们还需要添加一个监听事件,如果点击浏览器左侧的前进和后退按钮,我们的页面也应该做出相应的变化:

initEvent() {
	window.addEventListener('popstate', () => {
		this.data.current = window.location.pathname
	})
}

最后在init方法中调用一下即可

init() {
	this.createRouterMap()
	this.initComponents(_Vue)
	this.initEvent()
}

这样一个小型的vueRouter基本就完成了,hash模式也比较简单,我们只需要把点击事件方法改变一下即可:

Vue.component('router-link',{
	props: {
		to: String
	},
	render(h) {
		return h('router-link',{
			attrs: {
				href: this.to
			},
			on: {
				click: this.clickHandler
			}
		},this.$slots.default)
	},
	methods: {
		clickHandler(e) {
			// 判断模式
			self.options.mode == 'history' && history.pushState({}, '', this.to)
            self.options.mode == 'hash' && (window.location.hash = `#${this.to}`)
			self.data.current = this.to
			e.preventDefault()
		}
	}
})

同时修改一下监听事件

initEvent() {
	if(this.options == 'history') {
		window.addEventListener('popstate', () => {
			this.data.current = window.location.pathname
		})
	}else{
		window.addEventListener('hashchange', () => {
			this.data.current = window.location.hash.substr(1) ? window.location.hash.substr(1) : '/'
		})
	}
}

结尾

这样我们的hash模式和history模式就实现完成了,关于一些细节方面的东西就不进行模拟了,感兴趣的可以尝试实现一下。