路由是什么?
自从网站,web等诞生开始,路由就一直存在;在前后端分离之前,一般提到的路由都是后端路由;路由通过一个请求,然后分发到指定的路径,匹配对应的处理程序;它的作用就是分发请求,把对应的请求分发到对应的位置
前端路由与后端路由
后端路由
后端路由可以理解为服务器将浏览器请求的url解析之后映射成对应的函数,这个函数会根据资源类型的不同进行不同的操作,如果是静态资源,那么就进行文件读取,如果是动态数据,那么就会通过数据库进行一些增删查改的操作
后端路由的优点是利于SEO且安全性较高;缺点就是代码耦合度高,加大了服务器压力,且http请求受限于网络环境,影响用户体验
前端路由
随着前端单页应用(SPA)的兴起,前端页面完全变成了组件化,不同的页面就是不同的组件,页面的切换就是组件的切换;页面切换的时候不需要再通过http请求,直接通过JS解析url地址,然后找到对应的组件进行渲染
前端路由与后端路由最大的不同就是不需要再经过服务器,直接在浏览器下通过JS解析页面之后就可以拿到相应的页面
前端路由的优点就是组件切换不需要发送http请求,切换跳转快,用户体验好;缺点就是没有合理的利用缓存且不利于SEO
前端路由模式
hash模式
hash模式是vue-router
的默认路由模式,它的标志是在域名之后带有一个#
http://localhost:8888/#/home
通过window.location.hash
获取到当前url的hash;hash模式下通过hashchange
方法可以监听url中hash的变化
window.addEventListener("hashchange", function(){}, false)
hash模式的特点是兼容性更好,并且hash的变化会在浏览器的history中增加一条记录,可以实现浏览器的前进和后退功能;
缺点由于多了一个#
,所以url整体上不够美观
history模式
history模式是另一种前端路由模式,它基于HTML5的history对象
通过location.pathname
获取到当前url的路由地址;history模式下,通过pushState
和replaceState
方法可以修改url地址,结合popstate
方法监听url中路由的变化
history模式的特点是实现更加方便,可读性更强,同时因为没有了#
,url也更加美观;
它的劣势也比较明显,当用户刷新或直接输入地址时会向服务器发送一个请求,所以history模式需要服务端同学进行支持,将路由都重定向到根路由
vue-router工作流程
- url改变
- 触发事件监听
- 改变vue-router中的current变量
- 监视current变量的监视者
- 获取新的组件
- render
Vue插件基础知识
Vue.use()
Vue.use()
方法用于插件安装,通过它可以将一些功能或API入侵到Vue内部;
它接收一个参数,如果这个参数有install方法,那么Vue.use()
会执行这个install方法,如果接收到的参数是一个函数,那么这个函数会作为install方法被执行
install方法在执行的时候也会接收到一个参数,这个参数就是当前Vue的实例
通过接收到的Vue实例,可以定义一些全局方法或属性,也可以通过prototype对Vue的实例方法进行扩展
class vueRouter {
constructor(){
}
}
vueRouter.install = function(Vue) {
}
Vue.mixin()
Vue.mixin()
方法用于注册全局混入,它接收一个对象作为参数,我们将这个对象称为混入对象;混入对象可以包含组件的任意选项;通过混入对象定义的属性和方法在每一个组件中都可以访问到
<!-- router.js -->
class vueRouter {
constructor(){
}
}
vueRouter.install = function(Vue) {
Vue.mixin({
data(){
return {
name: '阿白smile'
}
}
})
}
<!-- home.vue -->
// 省略代码
<script>
export default {
created(){
console.log(name) // '阿白smile'
}
}
</script>
实现一个routerJs
通过前面的前置知识,已经对路由有了一些了解,接下来就开始实现一个routerJs
先来看一下vue-router的使用方法,然后再基于此进行一步一步的拆解分析
<!-- index.js -->
import vueRouter from './router'
import App from 'app.vue'
Vue.use(vueRouter)
const router = new vueRouter({
routes: []
})
new Vue({
router,
render: h => h(App)
})
在上面的使用示例中可以看出,通过Vue.use()
方法将vueRouter安装为插件;通过插件的安装即可以在全局使用vueRouter的方法及相关组件;
install方法
首先需要先实现install方法,通过install向全局注入vueRouter
<!-- router.js -->
class vueRouter {
constructor(){}
}
vueRouter.install = function(Vue) {
Vue.mixin({
beforeCreate(){
// $options.router存在则表示是根组件
if (this.$options && this.$options.router) {
this._root = this
this._router = this.$options.router
Vue.util.defineReactive(this, 'current', this._router.history)
} else {
// 不是根组件则从父组件中获取
this._root = this.$parent._root
}
// 使用$router代理对this._root._router的访问
Object.defineProperty(this, '$router', {
get() {
return this._root._router
}
})
}
})
}
install方法接收一个Vue实例作为参数,通过Vue.mixin()
全局混入beforeCreated
生命周期钩子;通过Vue实例暴露的工具方法defineReactive
将current
属性变成一个监视者
为了避免在使用过程中对_router
的修改,所以通过Object.defineProperty
设置一个只读属性$router
,并使用它代理对this._root._router
的访问
router初始化
vue-router在初始化的时候需要通过new操作符,所以需要提供一个vueRouter类并暴露给外部使用;同时还需要一个history类来保存当前的路由路径
class HistoryRoute {
constructor() {
this.current = null
}
}
class vueRouter {
// options 为初始化时的参数
constructor(options) {
this.mode = options.mode || 'hash'
this.routes = options.routes || []
this.history = new HistoryRoute
this.init()
}
init() {
if (this.mode === 'hash') {
// 初始化一个#
location.hash ? '' : location.hash = '/'
// 页面加载完成获取当前路由
window.addEventListener('load', () => {
this.history.current = location.hash.slice(1)
})
window.addEventListener('hashchange', () => {
this.history.current = location.hash.slice(1)
})
} else {
window.addEventListener('load', () => {
this.history.current = location.pathname
})
window.addEventListener('popstate', () => {
this.history.current = location.pathname
})
}
}
}
export default vueRouter
在上面的代码中,创建一个HistoryRoute
类,HistoryRoute
类current
属性储存当前路由,在install方法会让这个值实现可响应并监视它的变化,并根据它的变化渲染不同的组件
vueRouter在实例化时接收到一个options对象作为初始化的参数,options中指定了路由模式(mode
)和路由表(routes
);如果options中没有指定mode
和routes
,则mode
默认为hash模式,routes
默认为[]
init方法会根据不同的路由模式在页面加载完成后设置current
,同时还会为路由的变化添加事件监听,确保及时更新current
属性然后渲染组件
router-view组件与路由渲染
router-view
组件的实现依赖于Vue.component()
方法,通过这个方法向全局注册一个组件,需要注意的是Vue的全局组件注册需要在Vue实例化之前进行;
Vue.component
方法接收两个参数,第一个是组件的名称,另一个是组件的选项对象;
router-view
组件的作用是根据路由的变化渲染出路由所对应的组件,所以在注册时候主要是使用到选项对象中的render
函数
Vue.component('router-view', {
render(h) {
}
})
接下来就需要实现router-view
组件最重要的功能,如何找到需要渲染的组件?
可以知道的是,当路由变化的时候可以获取到最新的路由地址,同时也可以访问到routes
(路由表)的数据
所以只需要根据路由地址从路由表中拿到相应的组件然后交给render
函数执行就可以了
根据路由从路由表中获取组件有两种方式:
一种是路由每次变化的时候都是用find方法从路由表中查询一次,获取到路由对象,这种方式虽然可行,但是每次路有变化都去查询一次性能消耗太大;
另一种方式则是将路由与它所对应的组件以键值对的方式进行储存,路由变化的时候只需要根据路由地址进行查询即可;这种方式只需要遍历一次,路由变化时直接使用键值对的方式获取组件,能够非常有效的提高渲染速度
class vueRouter {
constructor(options) {
// 省略其他代码
this.routes = options.routes || []
this.routeMap = this.createMap(this.routes)
}
// 省略其他代码
createMap(routes) {
return routes.reduce((memo, current) => {
memo[current.path] = current.component
return memo
}, {})
}
}
至此,所有的路由都已经使用键值对的方式存入routeMap
中,接下来就可以使用render
函数进行组件渲染了
vueRouter.install = function(_Vue) {
// 省略其他代码
Vue.component('router-view', {
render(h) {
let current = this._self._root._router.history.current // 当前路由
let routerMap = this._self._root._router.routeMap
return h(routerMap[current])
}
})
}
到这里,router-view
组件就封装完成了,它可以在任何一个组件中使用,并根据路由的变化而渲染不同的组件
push方法和replace方法
在vue-router
中,无论是声明式的路由跳转还是编程式的路由跳转,都需要通过这两个方法参与来完成;
history模式下, 路由切换通过window.history.pushState
方法完成;在hash模式下,路由的切换是直接通过hash值的变化来实现
pushState
pushState
是H5引入的新方法,主要用于添加历史记录条目;
它接收三个参数,分别是状态对象,标题和URL;由于第二个参数(标题)在部分浏览器上会被忽略,所以在这里主要了解一下状态对象和URL
stateObj
状态对象,它会与历史记录条目相关联;popstate
事件触发时,状态对象会传入回调函数;浏览器会将这个状态对象序列化以后保存在本地,重新载入这个页面的时候可以拿到这个对象
URL 新历史记录的URL,必须与当前页面处在同一个域;浏览器的地址栏会显示这个地址
class vueRouter {
constructor(options) {}
push(url) {
if (this.mode === 'hash') {
location.hash = url
} else {
pushState(url)
}
}
replace(url) {
if (this.mode === 'hash') {
location.hash = url
} else {
pushState(url, true)
}
}
}
function pushState(url, replace) {
const history = window.history
if (replace) {
history.replaceState({key: history.state.key}, '', url)
} else {
history.pushState({key: Date.now()}, '', url)
}
}
在上面的代码中,hash模式下直接通过location修改hash值,通过hash值的变化去改变视图组件,另外还封装了一个pushState
方法统一负责history模式下的页面跳转,并通过一个replace参数判断使用哪种方式进行跳转;
router-link的实现
router-link
也是通过Vue.component()
方法注册的一个全局组件
Vue.component('router-link', {
render(h) {
}
})
router-link
接收一些props参数,这里列举几个常用的,全部参数可以查看官方文档
to: 目标路由地址
tag: 渲染的标签
replace: 使用replace方式进行路由跳转,不留下history记录
接下来开始实现一个简单router-link
组件
Vue.component('router-link', {
props: {
to: {
type: [Object, String],
required: true
},
tag: {
type: String,
default: 'a'
},
replace: Boolean
},
render(h) {
let data = {}
if (this.tag === 'a') {
data.attrs = {href: this.to}
} else {
data.on = {click: () => {
if (this.replace) {
this._self._root._router.replace(this.to)
} else {
this._self._root._router.push(this.to)
}
}}
}
return h(this.tag, data, this.$slots.default)
}
})
router-link
组件通过参数to设置目标路由,tag参数负责组件在页面上渲染的标签,默认为a标签,replace参数则负责控制在路由跳转时是否使用replace方法
在render
函数中根据不同的tag进行不同的数据拼接,在改变路由时,默认的a标签可以直接设置href属性,而其他标签则需要监听事件,然后使用router
的路由跳转方法进行路由切换
到此就已经实现了vue-router
的核心的路由监听,跳转,切换,组件渲染等功能,先到浏览器中看一下效果
写在最后
本文从路由的起源开始说起,到前后端路由的区别及优缺点,然后介绍了vue-router
的工作流程并实现了vue-router
中一些核心的方法及其原理
文章中涉及到的源码已提交到我的Github
End