在手撕之前,可以先了解前端路由到底是什么,前端路由都有哪些方式实现?
推荐阅读大佬的解析:https://zhuanlan.zhihu.com/p/27588422
这里是我摘抄的一部分解析:
前端路由
随着前端应用的业务功能越来越复杂、用户对于使用体验的要求越来越高,单页应用(SPA
)成为前端应用的主流形式。大型单页应用最显著特点之一就是采用前端路由系统,通过改变URL
,在不重新请求页面的情况下,更新页面视图。
“更新视图但不重新请求页面”是前端路由原理的核心之一,目前在浏览器环境中这一功能的实现主要有两种方式:
-
利用
URL
中的hash(“#”)
-
hash(“#”)
符号的本来作用是加在URL中指示网页中的位置(也就是锚点),hash
虽然出现在URL
中,但不会被包括在HTTP
请求中。它是用来指导浏览器动作的,对服务器端完全无用,因此,改变hash
不会重新加载页面 - 读取:
window.location.hash
- 监听:
hashchange
事件 - 每次改变
hash
,都会在浏览器的访问历史中增加一个记录
-
利用
History interface
在HTML5
中新增的方法
跟hash
差不多,表现形式上少了个丑丑的“#”,具体移步大佬的解析
经过大佬的解析,加之自己的理解,这里会实现一个简易版的VueRouter
。
功能分析
在手撕vue-router
源码前,我们先看一下它的基本用法和要实现的功能:
import Vue from 'vue';
// 第1步,要导入并安装插件
import VueRouter from 'vue-router'
Vue.use(VueRouter)
// 第2步,创建一个vue-router实例
const router = new VueRouter({
mode: 'history',
base: process.env.Base_URL,
routes: [ {path: '/xxx', name: 'xxx', component: () => import("@/views/xxx.vue")}]
})
// 第3步,将实例挂载到Vue实例上
new Vue({
router, // 注意key是小写
store,
render: h => h(App),
}).$mount('#app')
<!-- 第4步,使用router-link和router-view组件 -->
<router-link :to="{name:'home'}" class="nav">主页</router-link>
<router-view></router-view>
那我们要实现的功能:
- 创建一个插件:实现
VueRouter
类和install
方法 - 实现两个全局组件:
router-view
用于显示匹配组件的内容,router-link
用于跳转 - 监听
url
变化:监听hashchange
或popstate
事件 - 响应
url
变化:创建一个响应式的属性current
, 当它改变时获取对应组件并显示
实现过程
1. 创建一个VueRouter插件
在这之前,我们要先了解下Vue
的一些特性:
- 通过
Vue
构造函数的配置项,都会被挂载到Vue
实例的$options
属性上 - 使用
Vue
插件时,会默认将Vue
构造函数传入Vue.use = function(Vue, options) {...}
- 使用
Vue.mixin
可以全局混入各个组件,从而在其生命周期中可以拿到实例对象
创建的过程,就是通过插件方式,获取到Vue
的构造函数,然后通过全局mixin
的beforeCreate
钩子中获取到router
实例,并将其挂载到Vue
的原型上
- Vue的构造函数为什么不用import的方式导入?
因为如果通过import Vue from 'vue'
方式导入的话,在打包时,会将Vue
包一起解析,插件过大,应尽可能让插件独立且体积小的前提下,可以使用传入的参数来获取Vue
构造函数 - 为什么不在
Vue.use
时,将router
直接绑定到Vue.prototype
上?
因为在入口文件main.js
中可以看到,是先执行Vue.use(VueRouter)
,才执行new Vue({router})
,所以在安装插件时,还并没有获取到router
实例,所以要在插件中绑定router
实例
// krouter.js
let Vue;
class KVueRouter {
constructor(options) {
this.$options = options
}
}
// 1. 实现插件
// 在使用插件时,会把Vue的构造函数当成参数传进来
KVueRouter.install = function(_Vue) {
Vue = _Vue; // 保存构造函数,方便在KVueRouter里面使用
// 使用mixin获取根实例上的router选项
Vue.mixin({
beforeCreate() {
console.log(this); // 每个vue组件实例对象
// 确保根实例时,才执行挂载
if(this.$options.router) {
Vue.prototype.$router = this.$options.router; // 将根实例上的router设在Vue.prototype.$router上,方便在每个组件中通过this.$router获取到这个router实例
// 只有Vue根实例上才有router对象,router对象会被挂载在this.$options上
// new Vue({
// router, // 注意key是小写
// store,
// render: h => h(App),
// }).$mount('#app')
}
}
})
}
export default KVueRouter;
2. 实现router-link组件
我们要实现下面这样一个组件,特点是传入一个to
的属性写入跳转路径,使用render
函数,生成a
标签,并设置href
属性方便跳转,中间的内容是用slot
插槽处理的。要注意:
to
属性,我们可能直接传字符串(比如:to="/home"
),也有可能传入对象(如下面这个例子),所以要做不同的解析
<router-link :to="{name:'home'}" class="nav">主页</router-link>
KVueRouter.install = function(_Vue) {
// 2. 实现router-link组件
Vue.component('router-link', {
props: {
to: {
required: true
}
},
// 不能使用template来,因为不能在只有运行时版本时候使用template,而是要使用render
render(h) {
// <a href="#/about">关于</a>
let path = ''
if(typeof this.to === 'stirng') {
path = this.to
}else if(typeof this.to === 'object') {
path = '/' + this.to.name
}
console.log(this.$slots) // 当前组件的插槽集合,default:[Vnode]为其默认插槽内容
return h('a', {attrs: { href: '#' + path}}, this.$slots.default) // 里面的内容使用一个匿名插槽
}
})
}
3. 实现router-view组件
- 通过监听
hashchange
事件,获取当前路由地址 - 通过与
routes
映射表中的路径比对,如相同,则渲染对应的组件
let Vue;
class KVueRouter {
constructor(options) {
this.$options = options
this.curPath = '/' // 记录当前路由地址
// 监听url变化
window.addEventListener('hashchange', this.onHashChange.bind(this)) // 这里注意改变this指向,不然this会默认指向window
window.addEventListener('load', this.onHashChange.bind(this)) // 在用户刷新时
}
// 获取当前的路由地址
onHashChange() {
console.log(window.location.hash); // #/about
console.log(window.location.hash.slice(1)); // /about
this.curPath = window.location.hash.slice(1) // 从第1位截取到最后,即去掉#号
}
// 创建一个路由的映射表,避免每次路由变化都要去遍历
this.routeMap = {}
options.routes.forEach(route => {
this.routeMap[route.path] = route.component // '/home': {() => import("@/views/Home.vue")}
})
}
KVueRouter.install = function(_Vue) {
Vue.component('router-view', {
render(h) {
const {routeMap, curPath} = this.$router; // 获取路由映射表,和当前路由值
let component = routeMap[curPath] || null
return h(component)
}
})
}
4. 需要创建响应式的curPath属性
使用Vue.util.defineRective
方法,来创建响应式的路由地址属性,这样,当路由curPath
变化时,才会让依赖这个属性的组件重新渲染;否则只会在初始值时渲染一次,不会随路由变化而重新渲染
class KVueRouter {
constructor(options) {
this.$options = options
// 5. 需要创建响应式的curPath属性
Vue.util.defineReactive(this, 'curPath', '/')
// this.curPath = '/' // 记录当前路由地址
}
}
完整代码:
// kvue-router.js
// 创建一个全局变量,用来保存Vue的构造函数
let Vue;
class KVueRouter {
constructor(options) {
this.$options = options
// 5. 需要创建响应式的curPath属性
Vue.util.defineReactive(this, 'curPath', '/')
// this.curPath = '/' // 记录当前路由地址
// 4. 监听url变化
window.addEventListener('hashchange', this.onHashChange.bind(this)) // 这里注意改变this指向,不然this会默认指向window
window.addEventListener('load', this.onHashChange.bind(this)) // 在用户刷新时
// 创建一个路由的映射表,避免每次路由变化都要去遍历
this.routeMap = {}
options.routes.forEach(route => {
this.routeMap[route.path] = route.component // '/home': {() => import("@/views/Home.vue")}
})
}
// 获取当前的路由地址
onHashChange() {
console.log(window.location.hash); // #/about
console.log(window.location.hash.slice(1)); // /about
this.curPath = window.location.hash.slice(1) // 从第1位截取到最后,即去掉#号
}
}
// 1. 实现插件
// 在使用插件时,会把Vue的构造函数当成参数传进来
KVueRouter.install = function(_Vue) {
Vue = _Vue; // 保存构造函数,在KVueRouter里面使用
// 使用mixin获取根实例上的router选项
Vue.mixin({
beforeCreate() {
console.log(this);
// 确保根实例时,才执行挂载
if(this.$options.router) {
Vue.prototype.$router = this.$options.router;
}
}
})
// 2. 实现router-link组件
Vue.component('router-link', {
props: {
to: {
required: true
}
},
// 不能使用template来,因为不能在只有运行时版本时候使用template,而是要使用render
render(h) {
// <a href="#/about">关于</a>
let path = ''
if(typeof this.to === 'stirng') {
path = this.to
}else if(typeof this.to === 'object') {
path = '/' + this.to.name
}
console.log(this.$slots) // 当前组件的插槽集合,default:[Vnode]为其默认插槽内容
return h('a', {attrs: { href: '#' + path}}, this.$slots.default) // 里面的内容使用一个匿名插槽
}
})
// 3. 实现router-view组件
Vue.component('router-view', {
render(h) {
const {routeMap, curPath} = this.$router; // 获取路由映射表,和当前路由值
let component = routeMap[curPath] || null
return h(component)
}
})
}
export default KVueRouter;