前言
Vue Router是 Vue.js官方的路由管理器。它和Vue.js的核心深度集成。让构建单页面应用变得易如反掌。
Vue-Router源码手写实现
单页面应用程序中,url发生变化,不能刷新,显示对应视图内容
需求分析
spa
- Hash ---> #/home
- History ---> /home
根据url显示对应内容
- router-view
- 数据响应式:current变量的url,一旦发生变化,动态重新执行render
任务
实现一个插件
实现VueRouter类
- 处理路由选项
- 监控url变化,hashchange。注:这边用hash演示,history中的pushState, popState一样的原理。
- 响应变化
实现install方法
- 注册全局的$router方法
- 实现两个全局组件,router-link, router-view
实现
-
实现VueRouter类并处理路由选项,install方法。
类要想实现插件需要内部有install方法
let Vue; // 引用构造函数,一会注册方法 class VueRouter { constructor(options) { this.$options = options } } VueRouter.install =. function (_Vue) { Vue = _Vue } export default VueRouter -
注册全局的$router方法
let Vue; // 引用构造函数,一会注册方法 class VueRouter { constructor(options) { this.$options = options } } VueRouter.install =. function (_Vue) { Vue = _Vue // 为什么要⽤混⼊⽅式写?主要原因是use代码在前,Router实例创建在后,⽽install逻辑⼜需要⽤到该实例 Vue.mixin({ beforeCreate() { // 只有根组件拥有router实例 if (this.$options.router) { Vue.prototype.$router = this.$options.router } }, }) } export default VueRouter -
实现router-link全局组件
let Vue; // 引用构造函数,一会注册方法 class VueRouter { constructor(options) { this.$options = options } } VueRouter.install =. function (_Vue) { Vue = _Vue // 为什么要⽤混⼊⽅式写?主要原因是use代码在前,Router实例创建在后,⽽install逻辑⼜需要⽤到该实例 Vue.mixin({ beforeCreate() { // 只有根组件拥有router实例 if (this.$options.router) { Vue.prototype.$router = this.$options.router } }, }) Vue.component('router-link', { props: { to: { type: String, require: true, }, }, render(h) { return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default) }, }) } export default VueRouter -
监控url变化并响应变化
let Vue; // 引用构造函数,一会注册方法 class VueRouter { constructor(options) { this.$options = options // 定义初始化url const initial = window.location.hash.slice(1) || '/' // 使用Vue.util.defineReactive响应这个变化 Vue.util.defineReactive(this, 'current', initial) // 监控url变化,hashchange window.addEventListener('hashchange', this.onHashChange.bind(this)) } onHashChange() { this.current = window.location.hash.slice(1) } } VueRouter.install =. function (_Vue) { Vue = _Vue // 为什么要⽤混⼊⽅式写?主要原因是use代码在前,Router实例创建在后,⽽install逻辑⼜需要⽤到该实例 Vue.mixin({ beforeCreate() { // 只有根组件拥有router实例 if (this.$options.router) { Vue.prototype.$router = this.$options.router } }, }) Vue.component('router-link', { props: { to: { type: String, require: true, }, }, render(h) { return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default) }, }) } export default VueRouter -
实现router-view全局组件
let Vue; // 引用构造函数,一会注册方法 class VueRouter { constructor(options) { this.$options = options // 定义初始化url const initial = window.location.hash.slice(1) || '/' // 使用Vue.util.defineReactive响应这个变化 Vue.util.defineReactive(this, 'current', initial) // 监控url变化,hashchange window.addEventListener('hashchange', this.onHashChange.bind(this)) } onHashChange() { this.current = window.location.hash.slice(1) } } VueRouter.install =. function (_Vue) { Vue = _Vue // 为什么要⽤混⼊⽅式写?主要原因是use代码在前,Router实例创建在后,⽽install逻辑⼜需要⽤到该实例 Vue.mixin({ beforeCreate() { // 只有根组件拥有router实例 if (this.$options.router) { Vue.prototype.$router = this.$options.router } }, }) Vue.component('router-link', { props: { to: { type: String, require: true, }, }, render(h) { return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default) }, }) Vue.component('router-view', { render(h) { let Component = null const route = this.$router.$options.routes.find(route => route.path === this.$router.current) if (route) { Component = route.component } return h(Component) }, }) } export default VueRouter -
兼容嵌套路由
let Vue; // 引用构造函数,一会注册方法 class VueRouter { constructor(options) { this.$options = options // 不支持嵌套写法 // 定义初始化url // const initial = window.location.hash.slice(1) || '/' // 使用Vue.util.defineReactive响应这个变化 // Vue.util.defineReactive(this, 'current', initial) // 支持嵌套的写法 // 定义响应式的属性current this.current = window.location.hash.slice(1) || '/' // 响应这个变化 Vue.util.defineReactive(this, 'matched', []) this.match() // 监控url变化,hashchange window.addEventListener('hashchange', this.onHashChange.bind(this)) } // 定义match函数递归遍历url,存放在this.matched中 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 } } } onHashChange() { this.current = window.location.hash.slice(1) // url变化时清空并重新执行 this.matched = [] this.match() } } VueRouter.install =. function (_Vue) { Vue = _Vue // 为什么要⽤混⼊⽅式写?主要原因是use代码在前,Router实例创建在后,⽽install逻辑⼜需要⽤到该实例 Vue.mixin({ beforeCreate() { // 只有根组件拥有router实例 if (this.$options.router) { Vue.prototype.$router = this.$options.router } }, }) Vue.component('router-link', { props: { to: { type: String, require: true, }, }, render(h) { return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default) }, }) // 实现router-view Vue.component('router-view', { 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) { if (vnodeData.routerView) { depth++ } } parent = parent.$parent } let Component = null const route = this.$router.matched[depth] if (route) { Component = route.component } return h(Component) }, }) } export default VueRouter
最终实现代码
// 引用构造函数,一会注册方法
let Vue
// 实现VueRouter类
class VueRouter {
constructor(options) {
// 处理路由选项
this.$options = options
// 定义响应式的属性current
this.current = window.location.hash.slice(1) || '/'
// 响应这个变化
Vue.util.defineReactive(this, 'matched', [])
this.match()
// 监控url变化,hashchange
window.addEventListener('hashchange', this.onHashChange.bind(this))
}
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
}
}
}
onHashChange() {
this.current = window.location.hash.slice(1)
this.matched = []
this.match()
}
}
// 实现install⽅法
VueRouter.install = function(_Vue) {
Vue = _Vue
// * $router注册
// * 为什么要⽤混⼊⽅式写?主要原因是use代码在前,Router实例创建在后,⽽install逻辑⼜需要⽤到该实例
// 任务1 挂载this.$router方法
Vue.mixin({
beforeCreate() {
// 只有根组件拥有router实例
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
},
})
//
// 两个全局组件
// 任务2 实现两个全局组件router-view,router-link
// router-link
Vue.component('router-link', {
props: {
to: {
type: String,
require: true,
},
},
render(h) {
return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
},
})
// 实现router-view
Vue.component('router-view', {
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) {
if (vnodeData.routerView) {
depth++
}
}
parent = parent.$parent
}
let Component = null
const route = this.$router.matched[depth]
if (route) {
Component = route.component
}
return h(Component)
},
})
}
export default VueRouter
结语
- 获取url信息并响应化,使其在变化的时候能监听到,从而找到url对应的路由信息。并在全局组件的router-view中进行渲染。
- 嵌套路由需要记录深度(层级),并在初始化和变化的时候,递归获取对应的路由信息,并通过深度标记找到进行渲染。
- Vue Router使用Vue中的响应式数据,所在它是和Vue强耦合的,这也就是只能在Vue中使用的原因。