前言
在我们使用 spa (单页面应用)开发时,路由是我们开发的基石,那么我们平时不应该只是会使用,我们也应该了解一下它的基本构造以及有时候配置为何会这么书写。
你是否平常会有这些疑惑?
- 为什么我们在使用 vue-router 会有一个 vue.use 的写法,它到底干了什么?
- 为什么 new Vue 的时候需要将 rouer 加入到配置项中?
如果你也有相似的疑问,那么本文可以带着你解开这些问题。
友情提示:本文只是想阐述一个简单的 vue-router 如何实现,关于嵌套路由等功能并不会实现。
实现一个 vue-router
在实现一个 vue-router 之前,我们首先需要想一想,一个基本的 vue-router 需要些什么功能
- 需将 $router 实例挂载到 Vue 的原型链上
- 全局注册 router-link 和 router-view 组件
- 实现一个 VueRouter 类
- ...
至此一个简单的 vue-router 已完成,若需了解更多功能,请移步源码观看。
vue.use
通常我们在使用 vue-router 的时候,一般我们需要在 main.js 头部引入 vue-router,然后 router 里的 index.js 里执行Vue.use(VueRouter)。
Vue.use 会调用当前对象的 install 方法,并将 Vue 类传递至 install 函数中(请注意这一步十分重要)。
为什么说这一步十分重要呢,因为这样我们的插件将不再需要 Vue 依赖,我们在书写一个插件的时候,如果还连带着很多依赖这是不合理的。
挂在到原型链上
在执行 install 函数时,我们可以借助 mixin(混入)进行挂载(当然,这里得判断一下是否为根组件或者是否具有 router)
VueRouter.install = function(_Vue) {
Vue = _Vue
// 1.挂载$router属性
// 全局混入目的:延迟下面逻辑到 router 创建完毕并且附加到选项上时才执行
Vue.mixin({
beforeCreate() {
// 次钩子在每个组件创建实例时都会调用
// 根实例才有该选项
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
}
}
这里采用混入的最大原因是要等到 router 创建完毕。
创建 router 组件
因为 vue-router 具有router-link 和 router-view组件,所以我们需要在 install 这里,进行组件挂载。
router-link 这个组件很简单,我们只需要把它当做 a 标签进行设计即可。
router-view 这个组件需要根据路由表进行渲染,我们需要从路由表查到相应的组件(当前这里可以使用 es6 推荐的按需加载)。
注意:这里注册组件为什么没有使用 template 的写法,这里涉及到一个概念,叫做 runtime-only 和 runtime-compiler。runtime-compiler 和 runtime-only 功能差不多,但是多了编辑的功能可以解析 template。而我们平时使用 vue-cli3,一般都是runtime-only,我们在以 .vue 后缀书写代码时,在 webpack 编译是已被转译为相应的 render 函数能读取的格式。runtime-only 比 runtime-compiler 环境更小,并且速度更快(因为少了编译的过程)。
VueRouter.install = function(_Vue) {
Vue = _Vue
// 1.挂载$router属性
// this.$router.push()
// 全局混入目的:延迟下面逻辑到router创建完毕并且附加到选项上时才执行
Vue.mixin({
beforeCreate() {
// 次钩子在每个组件创建实例时都会调用
// 根实例才有该选项
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
},
})
// 2.注册实现两个组件router-view,router-link
Vue.component('router-link', {
props: {
to: {
type: String,
required: true,
},
},
render(h) {
// <a href="to">xxx</a>
// return <a href={'#'+this.to}>{this.$slots.default}</a>
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)
},
})
}
实现一个 VueRouter 类
- 在 VueRouter 类中,我们首先需要获取到当前的组件名称,这里我们通过 window.location.hash 获取并且截取。
- 我们需要新增一个事件,用于监听 hash 路由的更改
- 新增响应式数据,这里借助到了 Vue 里面的工具函数(Vue.util.defineReactive)
特别注意:这里使用 Object.defineProperty 是不可以的,Object.defineProperty 仅仅只是进行数据的劫持;当然使用 $set 也是不可以的,使用 $set 的前提必须是,第一个参数也必须是响应式的。
class VueRouter {
constructor(options) {
this.$options = options
// 把current作为响应式数据
// 将来发生变化,router-view的render函数能够再次执行
const initial = window.location.hash.slice(1) || '/'
Vue.util.defineReactive(this, 'current', initial)
// 监听hash变化
window.addEventListener('hashchange', () => {
console.log(this.current)
this.current = window.location.hash.slice(1)
})
}
}
完整代码
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
// 添加到配置项中,为什么?
router,
render: (h) => h(App),
}).$mount('#app')
router/index.js
import Vue from 'vue'
import VueRouter from './vue-router'
import Home from '../views/Home.vue'
// 1.VueRouter是一个插件?
// 内部做了什么:
// 1)实现并声明两个组件router-view router-link
// 2) install: this.$router.push()
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue'),
},
]
// 2.创建实例
const router = new VueRouter({
mode: 'hash',
routes,
})
export default router
vue-router.js
// 保存Vue构造函数,插件中要使用,不导入还能用
let Vue
class VueRouter {
constructor(options) {
this.$options = options
// 把current作为响应式数据
// 将来发生变化,router-view的render函数能够再次执行
const initial = window.location.hash.slice(1) || '/'
Vue.util.defineReactive(this, 'current', initial)
// 监听hash变化
window.addEventListener('hashchange', () => {
console.log(this.current)
this.current = window.location.hash.slice(1)
})
}
}
// 参数1是Vue.use调用时传入的
VueRouter.install = function(_Vue) {
Vue = _Vue
// 1.挂载$router属性
// this.$router.push()
// 全局混入目的:延迟下面逻辑到router创建完毕并且附加到选项上时才执行
Vue.mixin({
beforeCreate() {
// 次钩子在每个组件创建实例时都会调用
// 根实例才有该选项
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
},
})
// 2.注册实现两个组件router-view,router-link
Vue.component('router-link', {
props: {
to: {
type: String,
required: true,
},
},
render(h) {
// <a href="to">xxx</a>
// return <a href={'#'+this.to}>{this.$slots.default}</a>
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
}
console.log(this.$router.current, component)
return h(component)
},
})
}
export default VueRouter