这篇文章主要介绍一下vue-router插件的实现原理。
vue路由的工作流程
首先了解一下vue路由的工作流程,路由分为后端路由和前端路由。
后端路由
- 在地址栏中输入url
- 该请求会被直接发送到服务器
- 服务器解析请求中的路径并返回对应的页面
缺点:发送请求次数过多会对后台服务器造成比较大的压力,所以就出现了前端路由。
前端路由
- 在地址栏中输入url
- 该地址会被js进行解析
- 找到对应的页面后进行渲染
路由模式
前端路由的两种模式:Hash 和History
hash模式
- #号后面的就是hash值的内容
- 可以通过
location.hash
获取地址栏中的hash值 - 可以通过
window.onhashchange
监听hash值的改变
history模式
- 地址栏/后即为正常的路径
- 可以通过
location.pathname
获取地址栏中的路径 - 可以通过
window.onpopstate
监听路由值的改变
vue路由的工作原理
手写vue-router
列一下我们总共需要了解的知识点:
- vue.use()注册插件机制
- vueRouter构造函数
- 两个全局组件:router-link和router-view
- main.js中为什么需要注入vueRouter的实例
vue.use()
vue.use方法在注册插件机制时使用的,它接受两个参数,第一个参数是插件本身,第二个参数是要传递给插件的选项。
插件本身可以是一个函数也可以是一个对象,当是一个函数时use方法会立即执行该方法:
js
// 可以在main.js文件中进行测试
function a(){
console.log(100)
}
Vue.use(a) // 100
如果是一个对象的话则对象本身必须有一个install
的方法,然后use会直接执行传入对象中的install
方法,并且install
方法中的第一个参数为vue的构造函数,有了vue的构造函数我们就可以在里面使用vue相关的一些api:
js
let obj = {
install:function(vue){
console.log(120)
console.log(vue)
}
}
Vue.use(obj) // 120 function Vue(options) {...}
// 或者是
function a(){
console.log(100)
}
a.install = function(120){
console.log(120)
}
Vue.use(a) // 120
最后如果对同一个插件进行重复注册,那么该插件只会被安装一次,下面来看一下这块儿的源码:
可以直接在node_modules中搜索vue关键字,然后找到dist文件夹下的vue.js文件。
可以看到前三行代码就是用来防止重复注册插件,然后往下可以看到把this
直接放在了参数列表中的第一位也就是install
方法的第一个参数,再往下就是判断插件本身是否有install
属性并且是一个方法并进行执行。
vueRouter构造函数
在router.js中我们可以看到:
js
const router = new VueRouter({});
说明Vuerouter是一个构造函数,下面我们来自己实现一个VueRouter的构造函数。
首先新建一个myRouter的文件夹以及它下面的index.js文件并进行路径的修改:
在index.js文件中我们使用类来实现:
js
class VueRouter {
constructor() {
}
}
// 增加install方法
VueRouter.install = function (_vue) {
}
export default VueRouter;
这时页面提示有报错:
也就是缺少了router-link和router-view两个全局组件,下面来实现一下。
实现router-link标签
继续在之前的代码中进行扩展:
js
let Vue;
class VueRouter {
constructor() {
}
}
// 增加install方法
VueRouter.install = function (_vue) {
Vue = _vue;
// 创建两个全局组件
Vue.component('router-link', {
// 使用render函数创建 router-link相当于a标签
render(h) {
return h('a', 'router-link')
}
})
Vue.component('router-view', {
// 使用render函数创建 router-view相当于占位符
render(h) {
return h('div', 'router-view')
}
})
}
export default VueRouter;
先使用Vue.component方法创建两个全局组件,这样就不会再有报错信息了。
router-link作为一个a标签应该具有超链接的属性,所以我们可以使用h函数的第二个参数为其设置属性(不了解h函数的我就没办法喽h函数)
js
// 以下为关键代码,其余的同上面保持一致
Vue.component('router-link', {
render(h) {
return h('a', {
attrs: { href: '#' }
}, 'router-link')
}
})
如图所示:
解决完这个问题后接下来就是跳转的问题了,在vue中router-link的跳转是通过给组件传递一个to属性来实现跳转:
js
<router-link to="/">home</router-link> |
<router-link to="/about">about</router-link>
那我们可以在自己创建的全局组件中获取到该属性并实现地址栏路由的改变:
js
Vue.component('router-link', {
// 在当前组件中使用props接受传递的值
props:{
to:{
required:true,
type:String,
}
},
render(h) {
return h('a', {
// this指向的是当前router-link组件的实例
attrs: { href: '#'+this.to }
}, 'router-link')
}
})
这时点击就可以在地址栏中看到地址栏路由的变化。
那如何在页面中显示我们自定义的名称呢?
那自然而然应该想到插槽的概念了。
js
Vue.component('router-link', {
props: {
to: {
required: true,
type: String,
}
},
render(h) {
return h('a', {
attrs: { href: '#' + this.to }
}, this.$slots.default) // 使用默认插槽
}
})
router-link标签到这里就结束了,下面再来实现router-view标签。
实现router-view标签
实现router-view主要是通过监听路由的改变,使用当前的路由获取到对应的组件并进行渲染。
js
class VueRouter {
constructor(options) {
// 当前的路由
this.currentRoute = '';
// 用户定义的路由规则
this.routes = options.routes;
// 路由模式
this.mode = options.mode || 'hash';
// 监听路由变化
this.init()
}
init() {
if (this.mode == 'hash') {
window.addEventListener("hashchange", () => {
this.currentRoute = location.hash.slice(1); // 去除前面的#号与用户配置的路由规则表里保持一致
console.log(this.currentRoute);
})
}
}
}
这时候已经完成了路由的监听,在页面中切换会发现地址栏路由会发生变化并且可以获取到当前的路由值,接下来就是根据当前路由获取对应的组件。
那么现在的问题就是如何在router-view全局组件中获取路由信息,这时候我们可以使用Vue.mixin方法进行全局混入,通过全局混入的方法可以在里面给每个组件设置路由的相关信息。在Vue中我们会在main.js文件中把router实例传递给Vue构造函数:
js
new Vue({
router,
render: h => h(App)
}).$mount('#app')
通过this.$options
可以获取vue中所有实例组件的信息
js
VueRouter.install = function (_vue) {
Vue = _vue;
Vue.mixin({
// 在每个组件的beforeCreate钩子函数中设置
beforeCreate() {
console.log(this.$options.name)
}
})
Vue.component('router-view', {
render(h) {
return('div','router-view');
}
})
}
打印结果自上而下分别为main.js文件、App.vue、router-link、router-view组件,因为我们可以获取到main.js文件中的配置项,所以也就能获取到用户配置的路由信息,之后再通过mixin方法共享给其他所有的组件,这就是为什么需要在main.js文件中注入vueRouter的实例
js
VueRouter.install = function (_vue) {
Vue = _vue;
Vue.mixin({
// 在每个组件的beforeCreate钩子函数中设置
beforeCreate() {
if (this.$options.router) { // 判断是否是根实例
Vue.prototype.$router = this.$options.router // 让每个组件实例都可以访问路由信息
}
}
})
Vue.component('router-view', {
render(h) {
const currentRoute = this.$router.currentRoute;
const routes = this.$router.routes;
// 获取到当前的路由信息与所有的路由信息进行匹配
const obj = routes.find(item => {
return item.path == currentRoute
});
return h(obj.component)
}
})
}
这时候组件已经渲染成功了,但是切换的时候组件并没有更新,这是因为currentRoute
这个变量不是响应式的,所以我们需要把它变为响应式数据,可以直接使用Vue中提供的api:
js
let Vue;
class VueRouter {
constructor(options) {
// this.currentRoute = '/';
Vue.util.defineReactive(this, 'currentRoute', '/') // 使用defineReactive方法定义currentRoute变量
// 用户定义的路由规则
this.routes = options.routes;
// 路由模式
this.mode = options.mode || 'hash';
// 监听路由变化
this.init()
}
}
完整代码
js
let Vue;
class VueRouter {
constructor(options) {
// 当前的路由
// this.currentRoute = '/';
Vue.util.defineReactive(this, 'currentRoute', '/')
// 用户定义的路由规则
this.routes = options.routes;
// 路由模式
this.mode = options.mode || 'hash';
// 监听路由变化
this.init()
}
init() {
if (this.mode == 'hash') {
window.addEventListener("hashchange", () => {
this.currentRoute = location.hash.slice(1); // 去除前面的#号与用户配置的路由规则表里保持一致
})
}
}
}
VueRouter.install = function (_vue) {
Vue = _vue;
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
}
})
Vue.component('router-link', {
props: {
to: {
required: true,
type: String,
}
},
render(h) {
return h('a', {
attrs: { href: '#' + this.to }
}, this.$slots.default)
}
})
Vue.component('router-view', {
render(h) {
const currentRoute = this.$router.currentRoute;
const routes = this.$router.routes;
const obj = routes.find(item => {
return item.path == currentRoute
});
return h(obj.component)
}
})
}
export default VueRouter;
上述代码只是讲解了以下vue-router比较核心的一些东西,想了解更多的知识可以查看vue-router的源码。