前一篇文章,我们说了vueRouter的两种路由模式,以及两种路由模式的特点,区别,今天我们通过来自己写一个简易版的vueRouter来剖析vueRouter内部的核心实现原理
vueRouter所需实现功能
首先,我们先来总结下vueRouter需要实现哪些基本功能
- 我们可以直接通过this.$router取到router实例,故我们需要将router挂载到vue实例下
- 我们需要实现一个router-link组件,用于去改变地址栏中的路由地址
- 我们需要实现一个router-view组件,用于渲染当前路由地址对应的路由组件
- 我们需要实现当我们首次进入页面时,或者手动修改浏览器url地址栏中的路由地址时,也会渲染该路由地址对应的路由组件
- 我们需要实现,当点击浏览器的前进后退时,也会自动渲染前进或者后退以后,当前路由地址对应的路由组件
好了,根据我们列出来的功能,我们现在开始来写我们自己的vueRouter
准备工作
我们先准备一个新创建的vue项目,然后新建自己vueRouter.js文件,然后将router.js中引入vueRouter插件的代码改成引入我们自己创建的vueRouter.js文件
准备工作做好后,我们开始来写自己的vueRouter
自定义VueRouter
1、定义一个VueRouter类
我们都知道,我们是通过 new 关键字 去创建一个router实例,那么,很显然VueRouter是一个构造函数或者是一个类
export default class VueRouter {
}
2、注册插件
我们每次引入VueRouter后,都会先执行一条命令Vue.use(VueRouter),这其实是注册插件
了解Vue.use()的朋友应该就知道,调用use方法,Vue内部实际是会去调用插件內部的install方法去进行注册。调用install时,会将Vue构造函数作为参数传入install方法中
所以,我们首先需要去顶一个install 静态方法
export default class VueRouter {
static install(Vue) {
}
}
此时,在这里我们可以做3件事
- 判断该插件是否注册过,如果未注册,那我们就继续走注册逻辑
- 用一个变量存储一下Vue, 方便后面在其他函数中使用
- 在vue实例上挂载$router,以后vue中可以直接通过this.$router访问router实例对象
代码如下:
3、vueRouter构造器
之后,我们可以来写vueRouter的构造器了,大家都知道,构造器中的代码是在new一个实例的时候会执行的。那我们总结下,在这里,我们需要做些什么。
- 其次,我们需要定义一个变量来接收当前的路由模式。
- 我们需要实现根据当前路由地址,渲染对应的路由组件,那么,我们就需要一个路由地址和组件的一个映射表。所以,我们还需要定义一个路由映射表。
- 我们需要去注册vue的两个全局组件 router-link 和 router-view,用来改变路由地址,已经渲染路由组件,这个毫无疑问。
- 我们还需要实现,当我们手动改变路由地址时,然后enter键重新加载页面时,页面也需要根据当前的路由地址来渲染对应的组件。故,我们需要注册一个load事件,来监听页面的加载。
- 最后,也是最关键的一点,大家有没有想过,当我们的路由地址发生变化后,我们如何让vue知道路由地址发生变化了,然后在router-view标签的位置那去重新渲染当前路由地址所对应的路由组件呢。很明显,vue实现数据发生变化,从而使页面无刷新更新视图,靠的就是将数据转化为响应式数据,那么当数据发生变化时,就会触发数据的set, 然后再去更新这个数据的所有依赖者。
所以,我们是不是可以定义一个变量,来存储当前的路由地址,然后在router-view这个组件内,去使用这个变量的值,去路由映射表中,找到对应的路由组件,然后渲染对应的路由组件啊。这么依赖,router-view是不是就是这个变量的一个依赖者啊,那么当变量发生变化时,router-view就会自动重新渲染,从而更新视图啊。
下面,我们来一一实现这些功能
3.1 定义一个变量接收路由模式
constructor(options) {
this.mode = options.mode || 'history'
}
3.2 定义一个路由映射表
constructor(options) {
this.mode = options.mode || 'history'
// 实现routerMap(用于存储路由与组件的映射关系)
this.routerMap = {}
this.createRouteMap(options.routes || [])
}
// 解析路由表,得到路由与组件的映射关系
createRouteMap (routes, parentPath) {
if (routes && routes.length && routes.length > 0) {
routes.forEach((item) => {
let cPath = ''
if (parentPath) {
cPath = `${parentPath}/${item.path}`
} else {
cPath = item.path
}
this.routerMap[cPath] = item.component
if (item.children && item.children.length > 0) {
this.createRouteMap(item.children, cPath)
}
})
}
}
3.3 定义一个响应式变量,用来存储当前路由地址
上面写的第五点,我们把它提到第二步来写,因为后面会用到这个变量
constructor(options) {
// 记录当前的路由模式,如果没传,就默认hash模式
this.mode = options.mode || 'hash'
// 定义一个响应式对象,当后续current的值发生变化时,vue可以监测到
this.data = _Vue.observable({
current: this.mode === 'history' ? '/' : '#/' // 存放当前url地址
})
}
3.4 注册router-link组件 和 router-view组件
相关代码解释我都写在代码里面了,这里就不再过多的说组件注册逻辑了
constructor(options) {
// 记录当前的路由模式,如果没传,就默认hash模式
this.mode = options.mode || 'hash'
// 实现routerMap(用于存储路由与组件的映射关系)
this.routerMap = {}
this.createRouteMap(options.routes || [])
// 定义一个响应式对象,当后续current的值发生变化时,vue可以监测到
this.data = _Vue.observable({
current: this.mode === 'history' ? '/' : '#/' // 存放当前url地址
})
// 注册全局组件
this.initComponent()
}
initComponents () {
// 初始化router-link组件
this.initLink()
// 初始化router-view组件
this.initView()
}
// 注册router-link组件
initLink () {
_Vue.component('router-link', {
props: {
to: String
},
render (h) {
return h('a', {
attrs: { href: this.to },
on: {
click: this.locationHref
}
}, [this.$slots.default])
},
methods: {
locationHref (e) {
if (this.$router.mode === 'history') {
/**
* @description pushState用于改变浏览器跳转地址
* 参数有3个 第一个参数:一个对象,后续触发popState事件时,传给popState的事件的事件对象
* 第二个参数:是title,网页标题
* 第三个参数:需要跳转的url地址
*/
history.pushState({}, '', this.to)
// 更新data下的current变量的值(该变量用于记录当前url地址,当url发生变化时,需要改变这个变量的值)
// 因current是响应式数据,故当值发生变化时,会触发对应组件的重新渲染,从而当url发生变化时,页面也会发生变化
this.$router.data.current = this.to
} else {
window.location.hash = `#${this.to}`
this.$router.data.current = `#${this.to}`
}
// 阻止a标签默认事件,这里需要阻止a标签的href跳转,因为a标签的href跳转是会让浏览器直接向服务器去发送请求的
e.preventDefault()
}
}
})
}
// 注册router-view组件
initView () {
const self = this
_Vue.component('router-view', {
render (h) {
// 从路由表中获取当前path对应的component组件
let component = null
if (this.$router.mode === 'history') {
// 找到跳转的路由地址对应的路由组件,这里依赖了data.current。故当current的值发生变化时,会触发router-view的重新渲染
component = self.routerMap[self.data.current]
} else {
// hash模式下时,截图#后面的地址作为path路径,然后再去路由表中匹配对应的组件
const path = self.data.current.slice(1, self.data.current.length)
component = self.routerMap[path]
}
// 渲染对应的组件
return h(component)
}
})
}
// 解析路由表,得到路由与组件的映射关系
createRouteMap (routes, parentPath) {
if (routes && routes.length && routes.length > 0) {
routes.forEach((item) => {
let cPath = ''
if (parentPath) {
cPath = `${parentPath}/${item.path}`
} else {
cPath = item.path
}
this.routerMap[cPath] = item.component
if (item.children && item.children.length > 0) {
this.createRouteMap(item.children, item.path)
}
})
}
}
这里有几点需要说明下,
- 注册router-link组件时,如果是hash模式,我们可以直接通过修改location.hash的值去改变浏览器地址栏中的地址,因为带#的是hash地址,不会触发浏览器向服务器去发送请求。
- 但是 如果是history模式时,我们不可以直接通过修改location.pathname或者直接location.href去改变地址栏中的路由地址,因为这样会导致浏览器直接向web服务器去发送请求。因为,此时,我们需要借助html5的window.history对象中的pushState方法,去改变浏览器地址栏中的路由地址
3.5 注册load事件
注册load事件,实现首次加载或者浏览器重新加载页面时,能监听到,从而去改变data.current 的值,从而触发router-view的重新更新
constructor(options) {
// 记录当前的路由模式,如果没传,就默认hash模式
this.mode = options.mode || 'hash'
// 实现routerMap(用于存储路由与组件的映射关系)
this.routerMap = {}
this.createRouteMap(options.routes || [])
// 定义一个响应式对象,当后续current的值发生变化时,vue可以监测到
this.data = _Vue.observable({
current: this.mode === 'history' ? '/' : '#/' // 存放当前url地址
})
// 注册全局组件
this.initComponent()
// 注册事件
this.initEvent()
}
// 注册相关事件
initEvent () {
if (this.mode === 'history') {
// 用户首次打开时,加载当前路由对应的组件
window.addEventListener('load', () => {
// 获取浏览器路由地址
const path = location.pathname ? location.pathname : '/'
// 将路由地址赋值给data.current,从而触发router-view的重新渲染
this.data.current = path
history.pushState({}, '', path)
})
} else {
// 页面首次加载时,加载当前路由对应的组件
window.addEventListener('load', () => {
// 页面加载时,如果没有hash符,添加hash符
location.hash = location.hash || '/'
this.data.current = location.hash
})
}
}
到这一步,我们就已经可以实现,点击router-link链接时,可以跳转对应的路由页面了。以及,手动修改路由地址,然后再刷新浏览器时,可以加载对应的路由页面了
但其实,到这里,还会有点问题。
当我们点击浏览器的前进和后退按钮时,我们会发现,浏览器地址栏中的路由地址发生变化了,但是,我们的页面却没有发生变化。这是为什么
大家想想,我们需要页面发生变化,是不是要data.current的值发生了变化,vue才能监测到变化啊,router-view才能重新渲染对应的路由组件啊
而当浏览器点前进,后退时,是浏览器去帮我们改变了地址栏中的路由地址,这时,我们的data.current的值是不是还没有发生改变啊,所以,我们的视图肯定就不会更新了
所以,我们是不是需要一个事件,来监听浏览器地址栏的变化啊。
此时,我们需要用到两个事件,popstate事件以及hashchange事件
- popstate事件
在history模式下,当浏览器的历史发生变化时,会触发该事件 - hashchange事件
在hash模式下,只要浏览器地址栏中的hash地址发生了变化,就会触发该事件
所以,我们可以在这两个事件中,去获取到最新的路由地址,然后赋值给data.current,那么就会触发router-view的重新渲染
话不多说,上代码
// 注册相关事件
initEvent () {
if (this.mode === 'history') {
/**
* @description popstate事件,当浏览器历史发生变化时,会触发该事件,主要用于处理当浏览器点击前进后退时,
* 触发该事件,去改变current变量的值,从而触发对应组件的重新渲染,从而改变页面
* @ps history模式下,浏览器历史发生变化才会触发popstate事件
*/
// 用户首次打开时,加载当前路由对应的组件
window.addEventListener('load', () => {
const path = location.pathname ? location.pathname : '/'
this.data.current = path
})
window.addEventListener('popstate', () => {
this.data.current = location.pathname
})
} else {
/**
* @description hashchange事件,当浏览器url的hash地址发生变化时,会触发该事件,主要用于处理当浏览器url的hash地址发生变化时
* 触发该事件,去改变current变量的值,从而触发对应组件的重新渲染,从而改变页面
* @ps hash模式下,浏览器url的hash发生变化时才会触发hashchange事件
*/
// 页面首次加载时,加载当前路由对应的组件
window.addEventListener('load', () => {
// 页面加载时,如果没有hash符,添加hash符
location.hash = location.hash || '/'
this.data.current = location.hash
})
window.addEventListener('hashchange', () => {
this.data.current = location.hash
})
}
}
结尾,附完整代码
到这里,我们的代码算是都写完了,下面贴上全部完整代码
/**
* @description 自定义router
* @author chendada
*/
let _Vue = null
export default class VueRouter {
static install (Vue) {
// 1. 判断是否注册过,如果已注册过,不再注册
// 2. 缓存当前的Vue构造函数
// 3. 挂载$router到vue实例下,利用混入,混入到每个组件的实例中
if (VueRouter.install.installed) {
return
}
VueRouter.install.installed = true
_Vue = Vue
// Vue.prototype.$router = this.$options.router
// 为什么不能直接在Vue的原型对象上挂载$router呢,因为在当前函数中,this指向的是并不是vue实例,故this.$options是不存在的
// 故我们通过混入的形式,在vue实例的beforeCreate生命周期中去给vue原型添加$router
Vue.mixin({
beforeCreate () {
if (_Vue.prototype.$router) {
return
}
_Vue.prototype.$router = this.$options.router
}
})
}
constructor (options) {
// 实现routerMap(用于存储路由与组件的映射关系)
this.routerMap = {}
this.createRouteMap(options.routes || [])
// 记录当前路由模式,如果不传模式,默认给history模式
this.mode = options.mode || 'history'
// 定义一个响应式对象
this.data = _Vue.observable({
current: this.mode === 'history' ? '/' : '#/' // 存放当前url地址
})
// 初始化公用组件
this.initComponents()
// 注册相关事件
this.initEvent()
}
initComponents () {
// 初始化router-link组件
this.initLink()
// 初始化router-view组件
this.initView()
}
// 注册相关事件
initEvent () {
if (this.mode === 'history') {
/**
* @description popstate事件,当浏览器历史发生变化时,会触发该事件,主要用于处理当浏览器点击前进后退时,
* 触发该事件,去改变current变量的值,从而触发对应组件的重新渲染,从而改变页面
* @ps history模式下,浏览器历史发生变化才会触发popstate事件
*/
// 用户首次打开时,加载当前路由对应的组件
window.addEventListener('load', () => {
const path = location.pathname ? location.pathname : '/'
this.data.current = path
})
window.addEventListener('popstate', () => {
this.data.current = location.pathname
})
} else {
/**
* @description hashchange事件,当浏览器url的hash地址发生变化时,会触发该事件,主要用于处理当浏览器url的hash地址发生变化时
* 触发该事件,去改变current变量的值,从而触发对应组件的重新渲染,从而改变页面
* @ps hash模式下,浏览器url的hash发生变化时才会触发hashchange事件
*/
// 页面首次加载时,加载当前路由对应的组件
window.addEventListener('load', () => {
// 页面加载时,如果没有hash符,添加hash符
location.hash = location.hash || '/'
this.data.current = location.hash
})
window.addEventListener('hashchange', () => {
this.data.current = location.hash
})
}
}
// 注册router-link组件
initLink () {
_Vue.component('router-link', {
props: {
to: String
},
render (h) {
return h('a', {
attrs: { href: this.to },
on: {
click: this.locationHref
}
}, [this.$slots.default])
},
methods: {
locationHref (e) {
if (this.$router.mode === 'history') {
/**
* @description pushState用于改变浏览器跳转地址
* 参数有3个 第一个参数:一个对象,后续触发popState事件时,传给popState的事件的事件对象
* 第二个参数:是title,网页标题
* 第三个参数:需要跳转的url地址
*/
history.pushState({}, '', this.to)
// 更新data下的current变量的值(该变量用于记录当前url地址,当url发生变化时,需要改变这个变量的值)
// 因current是响应式数据,故当值发生变化时,会触发对应组件的重新渲染,从而当url发生变化时,页面也会发生变化
this.$router.data.current = this.to
} else {
window.location.hash = `#${this.to}`
this.$router.data.current = `#${this.to}`
}
// 阻止a标签默认事件,这里需要阻止a标签的href跳转,因为a标签的href跳转是会让浏览器直接向服务器去发送请求的
e.preventDefault()
}
}
})
}
// 注册router-view组件
initView () {
const self = this
_Vue.component('router-view', {
render (h) {
// 从路由表中获取当前path对应的component组件
let component = null
if (this.$router.mode === 'history') {
component = self.routerMap[self.data.current]
} else {
// hash模式下时,截取#后面的地址作为path路径,然后再去路由表中匹配对应的组件
const path = self.data.current.slice(1, self.data.current.length)
component = self.routerMap[path]
}
// 渲染对应的组件
return h(component)
}
})
}
// 解析路由表,得到路由与组件的映射关系
createRouteMap (routes, parentPath) {
if (routes && routes.length && routes.length > 0) {
routes.forEach((item) => {
let cPath = ''
if (parentPath) {
cPath = `${parentPath}/${item.path}`
} else {
cPath = item.path
}
this.routerMap[cPath] = item.component
if (item.children && item.children.length > 0) {
this.createRouteMap(item.children, cPath)
}
})
}
}
}
好了,一个简单的vueRouter,我们就写到这了。各位多多指教