首先回顾一下我们使用vue-router的过程。
// 1. 注册路由插件(这里执行的是对应插件的install方法,所以要实现一个静态的install方法)
Vue.use(VueRouter)
// 2.编写路由规则
const routes = [
{
path: '/',
name: 'Index',
component: Index
}
]
// 3. 创建 router 对象并且导出(由此得出VueRouter是一个类)
const router = new VueRouter({
mode: 'hash',
routes
})
export default router
// 4. 引入router对象,并注册 router 对象
import VueRouter from 'vue-router'
new Vue({
router,
render: h => h(App)
}).$mount('#app')
// 5.router-link,router-view的使用
单页面的应用始终秉承一个原则:修改访url但是不发送请求,然后视图进行更新。
首先解决第一个问题,如何让浏览器知道用户修改了url呢?
在hash模式下,主要通过hashchange的事件去监听路径的变化。然后通过window.location.hash来获取#,包括#后面的所有字符串,那么我们就知道了用户想要跳转到哪一个url了,然后根据url我们就找到了对应的组件了。在history模式下,通过popstate事件去监听用户前进,后退的操作,通过history.pushState方法去修改浏览器的url。
第二个问题:我们虽然知道用户要跳转到什么url,也知道对应的组件,那么如何让视图更新呢?
这里需要用到vue设计的原理:响应式。其实很简单理解,数据修改了,视图要更新,那么怎么更新呢,那就要做一些事情让视图更新,那这些事情其实是一系列的函数。响应式数据被修改后,会执行对应所依赖的渲染函数,这些渲染函数一般就是用来做视图更新操作的。所以我们要定义一个响应式的数据,来维护当前的路由的url以及相关的信息,当这个数据改变的时候就会执行对应的渲染函数,视图就会更新。
第三个问题:上面所说的渲染函数是什么?
我们都知道,我们对应的组件显示,是使用router-view去实现的。那么router-view其实是一个vue-router所注册的组件,当然组件就会有自己的渲染函数,所以上面所提到的响应式数据的改变然后执行对应的渲染函数,这个渲染函数就是router-view的渲染函数。
事已至此,直接上代码,每个方法都有注释。
let _Vue = null
export default class VueRouter {
// Vue.use(VueRouter) Vue的插件机制其实是使用对应插件的install方法
static install (Vue) {
// 1.判断当前插件是否已经安装
if (VueRouter.install.installed) {
return
}
VueRouter.install.installed = true
// 2.把Vue构造函数记录到全局变量(因为后面要注册router-link router-view需要用到全局的组件)
_Vue = Vue
// 3.把创建的router对象注入到全局对象上
// 混入,这样使得每一个每一个vue实例的组件都会执行在beforeCreate生命周期中执行对应
// 的方法,所以需要加一层判断,只要在根实例上面使用这个方法就好了。
_Vue.mixin({
beforeCreate () {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
this.$options.router.init()
}
}
})
}
// 构造函数,options是用户所定义的参数,routerMap是路由地址到组件的映射,data是个响应式数据,记录了当前路由的信息。
constructor(options) {
this.options = options
this.routerMap = {}
this.data = _Vue.observable({
current: '/'
})
}
// 初始化路由,执行三个方法。
// createRouterMap创建路由到图的映射
// initComponents创建router-link,router-view组件
// initEvent绑定对应的事件来触发页面更新
init () {
this.createRouterMap()
this.initComponents(_Vue)
this.initEvent()
}
createRouterMap () {
// 遍历options(路由规则),把路由规则解析成键值对
this.options.routes.forEach(route => {
this.routerMap[route.path] = route.component
})
}
initComponents (Vue) {
const self = this
// 使用Vue实例下的component方法来创建组件
Vue.component('router-link', {
props: {
to: String
},
// 组件的渲染函数,提供了一个h参数,这个h参数其实是一个函数,用来创建虚拟dom的
render (h) {
// router-link渲染出来其实是a标签
return h('a', {
// attrs是这个dom节点的属性
attrs: {
// 哈希模式和history模式的判断
href: self.options.mode == 'hash' ? `#${this.to}` : this.to
},
on: {
// history模式则需要绑定点击事件
click: self.options.mode == 'hash' ? () => { } : this.clickHandler
}
}, [this.$slots.default])
},
methods: {
// 其实底层是使用history.pushState来修改路由(浏览器上面的地址),但是不会触发页面刷新
// 所以我们还要手动修改data(维护了当前路由的信息),这个data是个响应式对象,修改它会自动
// 触发所依赖的对应渲染函数,因此页面更新。
clickHandler (e) {
history.pushState({}, '', `${this.to}`)
this.$router.data.current = this.to
e.preventDefault()
}
}
// template: '<a :href="to"><slot></slot></a>'
})
Vue.component('router-view', {
// data所依赖的渲染函数,所以data改变了,页面才刷新。
render (h) {
const component = self.routerMap[self.data.current]
return h(component)
}
})
}
// hash模式下,主要通过hashchange事件来监听,然后修改对应的data值。
// window.location.hash指的是浏览器地址中 #包括#后面的字符串,是个
// 可读可写的值。
initEvent () {
if (this.options.mode == 'hash') {
window.addEventListener('hashchange', () => {
this.data.current = window.location.hash.slice(1) || '/'
})
window.addEventListener('load', () => {
this.data.current = window.location.hash.slice(1) || '/'
})
} else {
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
}
}
}