从零开始,实现vue-router
本篇目标
- 实现
VueRouter
类和install
方法,使之作为一个插件存在
- 实现两个全局组件:
router-view
用于显示匹配组件内容,router-link
用于跳转
- 监控
url
变化:监听hashchange
或popstate
事件
- 响应最新
url
:创建一个响应式的属性current
,当它改变时获取对应的组件并显示
实现一个插件:创建VueRouter类和install方法
在src
目录下创jrouter
文件夹用于存放我们手写的路由的相关代码,在jrouter
下新建index.js
和jvue-router.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
let Vue;
class JVueRouter{ constructor(options){ this.$options=options } }
JVueRouter.install = function(_Vue){ Vue=_Vue; Vue.mixin({ beforeCreate(){ if(this.$options.router){ Vue.prototype.$router = this.$options.router } } }) } export default JVueRouter;
|
为什么要采用混入的方式:主要原因是use代码在前,Router实例创建在后,而install逻辑又需要用到该实例
修改jrouter
中index.js
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import Vue from 'vue' import VueRouter from './jvue-router' import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [ ...... ]
const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes })
export default router
|
最后我们只需要修改main.js
中对路由的引用就可以了
1 2 3 4 5 6 7 8
| ... import router from './jrouter' ... new Vue({ router, store, render: h => h(App) }).$mount('#app')
|
实现router-link和router-view
修改jrouter
中index.js
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| KVueRouter.install = function (_Vue) { ... Vue.component('router-link', { props: { to: { type: String, required: true }, }, render(h) { return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default) } }) Vue.component('router-view', { render(h){ return h('div','router-view') } }) }
|
监控url的变化
定义响应式的current,监听hashchange事件
1 2 3 4 5 6 7 8 9 10 11 12 13
| class JVueRouter{ constructor(options){ this.$options=options Vue.util.defineReactive(this,'current','/') window.addEventListener('hashchange', this.onHashChange.bind(this)) window.addEventListener('load', this.onHashChange.bind(this)) } onHashChange(){ this.current = window.location.hash.slice(1) } }
|
动态获取对应的组件
1 2 3 4 5 6 7 8 9 10 11 12
| Vue.component('router-view', { render(h){ let component=null this.$router.$options.routes.forEach(route=>{ if(route.path===this.$router.current){ component=route.component } }) return h(component) } })
|
提前处理路由表
提前处理路由表可以避免每次都循环
1 2 3 4 5 6 7 8 9 10 11
| class JVueRouter{ constructor(options){ ...... this.routeMap={} options.routes.forEach(route=>{ this.routeMap[route.path]=route }) ...... } }
|
修改router-view
根据path获取component的方法
1 2 3 4 5 6 7
| Vue.component('router-view', { render(h){ const {routeMap,current}=this.$router const component=routeMap[current].component || null return h(component) } })
|
代码结构优化
我们在jrouter
文件夹下新建jrouter-link.js
和jrouter-view.js
,调整jvue-router.js
中代码
1 2 3 4 5 6 7 8 9 10 11 12 13
| import Link from './jrouter-link' import View from './jrouter-view'
......
JVueRouter.install = function (_Vue) { ...... Vue.component('router-link', Link) Vue.component('router-view', View) ...... }
|
然后我们将原先写在jvue-router.js
中组件部分的代码分别写到对应的js文件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| export default { props: { to: { type: String, required: true }, }, render(h) { return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default) } }
export default { render(h) { const {routeMap, current} = this.$router; const component = routeMap[current].component || null; return h(component) } }
|
如果我们不用Vue.util.defineReactive(this,'current','/')
来实现监听,其实也可以用这种方式
1 2 3 4 5 6 7 8 9
| this.app = new Vue({ data(){ return { current:'/' } } })
this.app.current
|
嵌套路由
当用户的路由为类似如下的嵌套路由时,我们应该如何兼容呢
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const routes = [ ...... { path: '/about', name: 'about', component: () => import('../views/About.vue'), children:[ { path:'/about/info', component:() => import('../views/AboutInfo.vue'), } ...... ] } ...... ]
|
打开jrouter-view.js
文件,并修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| export default { render(h) { this.$vnode.data.routerView = true; let depth = 0 let parent = this.$parent while(parent){ const vnodeData = parent.$vnode&&parent.$vnode.data if(vnodeData&&vnodeData.routerView){ depth++ } parent = parent.$parent } let component = null; const route = this.$router.matched[depth]; if(route){ component = route.component } return h(component) } }
|
修改jvue-router.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| class JVueRouter{ constructor(options){ this.$options=options this.current = window.location.hash.slice(1)||'/' Vue.util.defineReactive(this,'matched',[]) this.match() window.addEventListener('hashchange', this.onHashChange.bind(this)) window.addEventListener('load', this.onHashChange.bind(this)) } onHashChange(){ this.current = window.location.hash.slice(1) this.matched = [] this.match()
} match(routes){ routes = routes||this.$options.routes for(const route of routes){ if(route.path === '/'&&this.current==='/'){ this.matched.push(route) return } if(route.path!=='/'&&this.current.indexOf(route.path)!=-1){ this.matched.push(route) if(route.children){ this.match(route.children) } return } } } }
|