赞
踩
前一篇文章,我们说了vueRouter的两种路由模式,以及两种路由模式的特点,区别,今天我们通过来自己写一个简易版的vueRouter来剖析vueRouter内部的核心实现原理
首先,我们先来总结下vueRouter需要实现哪些基本功能
好了,根据我们列出来的功能,我们现在开始来写我们自己的vueRouter
我们先准备一个新创建的vue项目,然后新建自己vueRouter.js文件,然后将router.js中引入vueRouter插件的代码改成引入我们自己创建的vueRouter.js文件
准备工作做好后,我们开始来写自己的vueRouter
我们都知道,我们是通过 new 关键字 去创建一个router实例,那么,很显然VueRouter是一个构造函数或者是一个类
export default class VueRouter {
}
我们每次引入VueRouter后,都会先执行一条命令Vue.use(VueRouter),这其实是注册插件
了解Vue.use()的朋友应该就知道,调用use方法,Vue内部实际是会去调用插件內部的install方法去进行注册。调用install时,会将Vue构造函数作为参数传入install方法中
所以,我们首先需要去顶一个install 静态方法
export default class VueRouter {
static install(Vue) {
}
}
此时,在这里我们可以做3件事
之后,我们可以来写vueRouter的构造器了,大家都知道,构造器中的代码是在new一个实例的时候会执行的。那我们总结下,在这里,我们需要做些什么。
下面,我们来一一实现这些功能
constructor(options) {
this.mode = options.mode || 'history'
}
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) } }) } }
上面写的第五点,我们把它提到第二步来写,因为后面会用到这个变量
constructor(options) {
// 记录当前的路由模式,如果没传,就默认hash模式
this.mode = options.mode || 'hash'
// 定义一个响应式对象,当后续current的值发生变化时,vue可以监测到
this.data = _Vue.observable({
current: this.mode === 'history' ? '/' : '#/' // 存放当前url地址
})
}
相关代码解释我都写在代码里面了,这里就不再过多的说组件注册逻辑了
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) } }) } }
这里有几点需要说明下,
注册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事件
所以,我们可以在这两个事件中,去获取到最新的路由地址,然后赋值给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,我们就写到这了。各位多多指教
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。