VueRouter的实现(history模式)
首先来看一下VueRouter
的简单的使用过程来推断一下它大概是一个什么东西👇
然后在main.js
页面:
可以看出来,在引入VueRouter
之后,先使用Vue.use
注册了这个插件。use
方法中可以传入一个包含install
方法的类或者是一个函数:当传入类时会调用install
方法;当传入函数时会直接执行这个函数。下面new
操作符就直接告诉我们VueRouter
是一个包含install
方法的类。
这边是VueRouter
的一个类图,里面包含了它内部的属性(中间的部分)和方法(下面的部分):
各个属性的意思:
-
options
记录传入构造函数的对象;
-
routeMap
一个对象,记录路由与组件之间的对应关系,它是由路由规则中解析出来的;
-
data
一个对象,里面有一个
current
属性,它是用来记录当前路由地址。data
对象是一个响应式的对象,它可以使用observable
方法来创建:当路由发生变化时,会更新对应的组件。
各个方法的意思:
-
Constructor
构造器;
-
install
静态方法,它是用来实现
Vue
的插件机制; -
init
用来调用下面的几个方法;
-
initEvent
用来注册
popstate
这个事件,监听浏览器历史的变化; -
createRouteMap
初始化
routeMap
属性,将options
里面的路由规则按照键值对的形式将路由与组件存入routeMap
属性里; -
initComponents
创建
router-view
以及router-link
组件。
明白了这属性及方法,就可以来实现这些方法以及VueRouter
。
准备工作
先创建一个空vue2.x
版本的项目,在src
文件夹下创建一个vueRouter
文件夹里面有index.js
这样一个文件用来书写我们的router
。
这个js
文件里面先简单导出默认成员,该成员是一个类,类里面包含install
这个静态方法:
// VueRouter
export default class VueRouter {
static install() {
}
}
在router
文件夹下的index.js
文件里面VueRouter
的引入改成刚刚创建的:
这样就可以来慢慢编写自己的代码以满足需求。
注意,由于需要在
initComponents
方法里面使用模板创建组件,所以需要将项目中runtime only
的vue
替换成完全版的(包含compiler
)。通过vue-cli
创建的项目可以在根目录下创建一个vue.config.js
,在导出的成员中配置如下:
module.exports = { runtimeCompiler: true }
或者在使用
runtime only
版本的vue
时,使用render
函数替代tamplate
模板(在下面创建组件时有示例)。
install方法
在使用use
方法时,会向install
方法里面传传入两个参数,一个是Vue
对象,一个是可选配置对象(此时并不需要,所以可以不用写):
export default class VueRoute {
static install(Vue) {
}
}
在install
方法里面要做的事情有这些:
-
判断当前插件有没有安装过;
-
把
Vue
构造函数记录到全局变量中;因为当前的
install
是一个静态方法,在后面VueRouter
中的实例方法中还需要使用到传入的Vue
的构造函数(创建router-view
、router-link
组件时需要调用Vue.component
方法来创建)。 -
把创建
Vue
实例时传入的router
对象注入到所有的Vue
实例上;开发中使用的
this.$router
是在这个时候注入到实例上的。
实现起来就是这样:
// VueRouter
// 初始化
let _Vue = null
export default class VueRouter {
static install(Vue) {
// 1.判断VueRouter是否已经安装
// 若想判断该插件是否已经被安装,这就需要有个变量来记录该插件安装与否
// 在js中万物皆对象,所以可以将该变量挂载到这个函数下
// ⛔不使用局部变量或全局变量的原因:
// (1)这个变量肯定不能为局部变量,
// (2)因为会有外部的依赖,所以全局变量也不行
// 若已经安装就直接返回
if (VueRouter.install.installed) {
return
}
// 没有安装的话就在install方法下增加一个installed属性,值设置为true
VueRouter.install.installed = true
// 2.将vue构造函数记录到全局
_Vue = Vue
// 3.将创建的Vue实例时传入router注入到Vue实例上
// 所有的组件都是一个实例,若想要所有的实例共享一个成员,可以将其设置到构造函数的原型上
// 但是此时无法获取到Vue实例,所以还不可以使用_Vue.prototype.$router = this.$options.router这样的方式
// 因此我们需要在能够获取到Vue实例的时候使用这个方式
// 所以可以使用vue中的混入(mixin),给所有的组件混入一个beforeCreate,这样就可以在其中获取到Vue实例
_Vue.mixin({
beforeCreate() {
// 这里有一个问题是:
// 一个项目中会有很多个组件,并且所有的组件都会执行_Vue.prototype.$router = this.$options.router这句代码
// 而这句代码实际上只是需要执行一次就行了
// 所以可以判断一下当前的this是否有$options.router这个属性
// 因为只有vue中有这个属性,组件里面是没有这个属性的
if (this.$options.router) {
// 这里的this指向的是当前的vue实例,所以就可以使用上面的方式来注入router
_Vue.prototype.$router = this.$options.router
}
}
})
}
}
constructor
constructor
是new
的时候执行的方法,这里主要就是初始化上面类图中的几个属性:
// 构造器
constructor(options) {
// 根据之前的类图,构造器里面是用来初始化一些属性的
// 所以可以在这里将options、routeMap和data进行初始化
this.options = options
this.routeMap = {}
// 由于data是需要响应式的,所以可以使用vue的observable方法来创建对象
this.data = _Vue.observable({
// 当前路由,默认是/
current: '/'
})
}
createRouteMap
createRouteMap
方法会将创建实例时传入的路由规则转换成由路由和组件组成的键值对形式,并将其存在routeMap
中:
createRouteMap() {
// 遍历routes,将path属性作为键、component属性作为值存入routeMap之中
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
initComponents
initComponents
方法会创建router-link
以及router-view
组件:
router-link
使用带有compiler
的vue
创建router-link
组件:
initComponents(Vue) {
Vue.component('router-link', {
props: {
to: String
},
template: '<a :href="to"><slot/></a>'
})
}
使用runtime only
的vue
创建router-link
组件:
initComponents(Vue) {
Vue.components('router-link', {
props: {
to: String
},
// h函数是vue传入的函数,它能将其中的结构转换成虚拟dom
// h函数第一项是选择器,可以为标签名
// 第二项可给标签添加属性
// 第三项给标签设置子元素
render(h) {
return h('a', {
attrs: {
href: this.to
}
// this.$slots.default获取默认插槽的内容
}, [this.$slots.default])
}
})
}
init方法
这个时候如果想要看看router-link
有没有成功,将其调用一下,同时再把createRouteMap
方法也调用一下:
// 在这个方法里统一调用要在初始化时调用的方法
init() {
this.createRouteMap()
this.initComponents(_Vue)
}
同时在将router
注入到vue
实例上时调用init
方法:
static install(Vue) {
if (VueRouter.install.installed) {
return
}
VueRouter.install.installed = true
_Vue = Vue
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
// 在这里调用初始化方法
this.$options.router.init()
}
}
})
}
运行一下项目,可以看到超链接已经能够在页面上展示了:
而且在HTML
结构中也能看到a
标签:
router-view
router-view
是需要根据当前路由来展示对应的组件,所以可以通过之前routeMap
和data
里面的current
属性来获取到当前要展示的组件,同时通过render
函数渲染成虚拟dom
:
initComponents(Vue) {
Vue.component('router-link', {
props: {
to: {
type: String,
}
},
render(h) {
return h('a', {
attrs: {
href: this.to
}
}, [this.$slots.default])
}
})
// 保存一下当前的this,以便在组件中访问routeMap以及data
const self = this
// router-view是需要根据当前路由来展示对应的组件
Vue.component('router-view', {
render(h) {
// 通过routeMap以及data.current取得当前组件,并渲染成虚拟dom
const component = self.routeMap[self.data.current]
return h(component)
}
})
}
这个时候刷新一下项目就会发现,对应的组件已经能展示出来了:
但是点击一下切换路由的超链接就会发现一个问题:当点击About
这个链接想要切换到about
页面时页面并不会变化,但是浏览器会有一次刷新。
这是因为点击超链接时会向服务器发送一次请求,但是在像vue
这种单页面应用程序中并不需要像服务器发送请求。因此需要再给超链接注册点击事件,阻止这种默认行为,同时再将地址栏中的链接替换成对应的路由。
为了解决这个问题,可以使用浏览器的history
的pushState
方法: pushState
这个方法只会改变浏览器中的地址,组件的切换还需要自己动手。
Vue.component('router-link', {
props: {
to: {
type: String,
}
},
render(h) {
return h('a', {
attrs: {
href: this.to
},
// 给超链接注册点击事件,阻止超链接的默认跳转行为,再实现浏览器地址栏路由的切换以及组件的切换
on: {
click: this.handleDefaultBehavior
}
// this.$slots.default获取默认插槽的内容
}, [this.$slots.default])
},
// 方法都可以写在这里
methods: {
// 传入事件对象
handleDefaultBehavior(e) {
// 切换浏览器的地址栏
history.pushState({}, '', this.to)
// 将data中的current改变成to中的地址,以便组件的切换
this.$router.data.current = this.to
e.preventDefault()
}
}
})
initEvent
这个时候主要的功能都已经实现了,点击切换路由时地址栏中的地址以及组件也会随之切换,但是还有个问题就是:当点击浏览器自带的前进后退按钮时,页面并不会随着地址变化。
所以需要给window
添加popstate
事件监听(popstate),当地址栏中的地址发生变化时,将其地址赋值给data.current
:
// initEvent是给window添加事件监听popstate,在地址栏发生变化时将地址赋值给data.current
initEvent() {
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
}
这样在点击前进后退时,组件也能正常切换了。
代码整理
// VueRouter
// 初始化
let _Vue = null
export default class VueRouter {
// use调用install方法时,会传入一个Vue的构造函数
static install(Vue) {
// 1.判断VueRouter是否已经安装
// 若想判断该插件是否已经被安装,这就需要有个变量来记录该插件安装与否
// 在js中万物皆对象,所以可以将该变量挂载到这个函数下
// ⛔不使用局部变量或全局变量的原因:
// (1)这个变量肯定不能为局部变量,
// (2)因为会有外部的依赖,所以全局变量也不行
// 若已经安装就直接返回
if (VueRouter.install.installed) {
return
}
// 没有安装的话就在install方法下增加一个installed属性,值设置为true
VueRouter.install.installed = true
// 2.将vue构造函数记录到全局
_Vue = Vue
// 3.将创建的Vue实例时传入router注入到Vue实例上
// 所有的组件都是一个实例,若想要所有的实例共享一个成员,可以将其设置到构造函数的原型上
// 但是此时无法获取到Vue实例,所以还不可以使用_Vue.prototype.$router = this.$options.router这样的方式
// 因此我们需要在能够获取到Vue实例的时候使用这个方式
// 所以可以使用vue中的混入(mixin),给所有的组件混入一个beforeCreate,这样就可以在其中获取到Vue实例
_Vue.mixin({
beforeCreate() {
// 这里有一个问题是:
// 一个项目中会有很多个组件,并且所有的组件都会执行_Vue.prototype.$router = this.$options.router这句代码
// 而这句代码实际上只是需要执行一次就行了
// 所以可以判断一下当前的this是否有$options.router这个属性
// 因为只有vue中有这个属性,组件里面是没有这个属性的
if (this.$options.router) {
// 这里的this指向的是当前的vue实例,所以就可以使用上面的方式来注入router
_Vue.prototype.$router = this.$options.router
// 在这里调用初始化方法
this.$options.router.init()
}
}
})
}
constructor(options) {
// 根据之前的类图,构造器里面是用来初始化一些属性的
// 所以可以在这里将options、routeMap和data进行初始化
this.options = options
this.routeMap = {}
// 由于data是需要响应式的,所以可以使用vue的observable方法来创建对象
this.data = _Vue.observable({
// 当前路由,默认是/
current: '/'
})
}
// 在这个方法里统一调用要在初始化时调用的方法
init() {
this.createRouteMap()
this.initComponents(_Vue)
this.initEvent()
}
// createRouteMap方法会将创建实例时传入的路由规则转换成由路由和组件组成的键值对形式,并将其存在routeMap中
createRouteMap() {
// 遍历routes,将path属性作为键、component属性作为值存入routeMap之中
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
initComponents(Vue) {
// 使用component方法创建RouterLink组件
// router-link组件的本质上就是一个超链接(a标签)
Vue.component('router-link', {
props: {
to: {
type: String,
}
},
// template: '<a :href="to"><slot/></a>'
// h函数是vue传入的函数,它能将其中的结构转换成虚拟dom
// h函数第一项是选择器,可以为标签名
// 第二项可给标签添加属性
// 第三项给标签设置子元素
render(h) {
return h('a', {
attrs: {
href: this.to
},
// 给超链接注册点击事件,阻止超链接的默认跳转行为,再实现浏览器地址栏路由的切换以及组件的切换
on: {
click: this.handleDefaultBehavior
}
// this.$slots.default获取默认插槽的内容
}, [this.$slots.default])
},
// 方法都可以写在这里
methods: {
// 传入事件对象
handleDefaultBehavior(e) {
// 切换浏览器的地址栏
history.pushState({}, '', this.to)
// 将data中的current改变成to中的地址,以便组件的切换
this.$router.data.current = this.to
e.preventDefault()
}
}
})
// 保存一下当前的this,以便在组件中访问routeMap以及data
const self = this
// router-view是需要根据当前路由来展示对应的组件
Vue.component('router-view', {
render(h) {
// 通过routeMap以及data.current取得当前组件,并渲染成虚拟dom
const component = self.routeMap[self.data.current]
return h(component)
}
})
}
// initEvent是给window添加事件监听popstate,在地址栏发生变化时将地址赋值给data.current
initEvent() {
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
}
}