在Vue中,vue-router占据重要的位置,是vue的核心插件,那么它的实现原理是什么呢? 在剖析原理之前,先来了解几个概念:SAP,路由模式 SPA(single page application):单一页面应用程序,有且只有一个完整的页面,当它在加载页面时,不会加载整个页面,而只更新某个指定的容器中内容(组件)
路由模式:hash模式、history模式,abstract模式
1.hash模式
随着ajax的流行,异步数据请求交互运行在不刷新浏览器的情况下进行。而异步交互体验更高的版本就是SPA--单页应用。单页应用不仅在页面交互时不用刷新,而且在页面跳转时也是不用刷新,于是就有浏览前端路由。实现原理就是根据不同的url进行解析,来匹配不同的组件;但当url发生变化时就会造成页面的刷新。这就出现了hash,使用hash在改变url的情况下,保证页面的不刷新。
http://www.xxx.com/#/login
这种#,后面hash值发生变化,并不会导致浏览器向服务器发出请求,进而不会发生页面刷新。而且每当hash发生变化时,都会触发hashchange事件,通过这个事件可以知道hash值发生了什么变化,然后可以监听hashchange来实现更新页面部分内容大都操作
2.history模式
因在HTML5标准发布后,多了两个API,pushState,replaceState,通过这两个API可以改变url地址且不会发送请求。同时还有popstate事件,通过这些API就能以另一种方式来实现前端路由,但原理与hash实现相同,但不会有**#**,因此当用户主动刷新页面之类操作时,还是会给服务器发送请求,为避免这种情况,所以这实现需要服务器支持,需把所有路由都重定向到根页面
3.abstract模式
abstract模式是使用一个不依赖于浏览器的浏览历史虚拟管理后端。 根据平台差异可以看出,在 Weex 环境中只支持使用 abstract 模式。 不过,vue-router 自身会对环境做校验,如果发现没有浏览器的 API,vue-router 会自动强制进入 abstract 模式,所以 在使用 vue-router 时只要不写 mode 配置即可,默认会在浏览器环境中使用 hash 模式,在移动端原生环境中使用 abstract 模式。 (当然,你也可以明确指定在所有情况下都使用 abstract 模式)
vue-router实现原理
原理核心:更新视图而不重新请求页面。 vue-router实现单页面跳转,提供了三种方式:hash模式、history模式 abstract模式,根据mode参决定使用哪种方式。 接下来详细剖析其原理
在这张流程图中分析vue-router实现原理,我们将官网的vue-router挂载到Vue实例上,打印出来增加了什么:
- options下的router对象很好理解,这个就是我们在实例化Vue的时候挂载的那个vue-router实例;
- _route是一个响应式的路由route对象,这个对象会存储我们路由信息,它是通过Vue提供的Vue.util.defineReactive来实现响应式的,下面的get和set便是对它进行的数据劫持;
- _router存储的就是我们从$options中拿到的vue-router对象;
- _routerRoot指向我们的Vue根节点;
- _routerViewCache是我们对View的缓存;
- router是定义在Vue.prototype上的两个getter。前者指向_routerRoot下的_route,后者指向_routerRoot下的_router
总结起来,vue-router就是一个类,里面封装了一个mixin,定义了两个‘原型’,注册了两个组件。 在mixin中beforeCreate中定义**_routerRoot、_router、_route**,使其每个组件都有_routerRoot,定义了两个原型是指在Vue.prototype上面定义的两个getter,也就是**route**,注册两个组件是指要用到的router-link、router-view
基本原理:
- Vue.use()时将vue-router挂载到Vue上时调用install方法,在install方法里调用mixin定义_routerRoot、_router、_route,使其每个组件都有_routerRoot,_router,在Vue原型上挂载route。
- 在初始化时调用init方法,根据mode参数决定使用哪种路由模式;
- 如果使用hash模式,则定义load页面加载方法,在页面加载时通过location.hash获取地址栏的url的hash值赋值给history对象的current,定义hashchange事件监听url的hash变化,将变化后的hash值赋值给history对象的current
- 如果使用的是history模式,则定义load页面加载函数,通过location.pathname,获取地址栏url中对应组件的地址如:http://localhost:8080/about,取得 /about,赋值给history对象的current, 定义popstate方法,当点击前进或后退时改变了当前活动历史项(history),触发此事件,通过location.pathname将历史中的url赋值给history对象的current
- 在定义全局组件时,
- router-link组件,调用render()函数时,通过mode确定使用哪种路由方式,获取传递的to赋值给a标签的href属性进行跳转,使用slot插槽传递中间的值
- router-view组件时,将之前赋值的组件名:history.current,通过将数据转换为对象方法,传递给render()函数中的h()方法进行渲染对应组件 详细原理已剖析完毕,代码如下: 目录结构如下:
app.vue
<template>
<div id="app">
<router-link to='/home'>首页</router-link>
<br>
<router-link to="/about">关于</router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {
name:'app'
};
</script>
Home.vue
<template>
<div>
home
</div>
</template>
<script>
export default {
name:'home'
}
</script>
About.vue
<template>
<div>
about
</div>
</template>
<script>
export default {
name:'about'
}
</script>
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './routers';
import store from './store'
Vue.config.productionTip = false
new Vue({
name:'main',
router,
store,
render: h => h(App)
}).$mount('#app')
index.js
import Vue from 'vue';
import routes from './routes';
import VueRouter from './vue-router';
// 使用Vue.use就会调用install方法
Vue.use(VueRouter)
export default new VueRouter({
mode:"history",//hash,history
routes
})
routes.js
import Home from '../views/Home.vue';
import About from '../views/About.vue';
export default [
{path:'/home',name:'home',component:Home},
{path:'/about',name:'about',component:About}
]
vue-router.js
class HistoryRoute{
constructor(){
this.current=null
}
}
class VueRouter{
constructor(options){
this.mode=options.mode||"hash";
this.routes=options.routes||[];
this.routesMap=this.createMap(this.routes)
this.history=new HistoryRoute();
this.init();
}
init(){
if(this.mode==="hash"){
//使用的是hash路由
// console.log('使用的是hash模式')
// console.log(location.hash)
location.hash?"":location.hash="/"
window.addEventListener("load",()=>{
this.history.current=location.hash.slice(1);
// console.log('load==>',this.history.current)
})
window.addEventListener("hashchange",()=>{
this.history.current=location.hash.slice(1)
// console.log('hashchange==>',this.history.current)
})
}else{
//使用的history
location.pathname?"":location.pathname='/'
window.addEventListener("load",()=>{
this.history.current=location.pathname;
// console.log('load==>',this.history.current)
})
window.addEventListener("popstate",()=>{
this.history.current=location.pathname
// console.log('popstate==>',this.history.current)
})
}
}
push(){}
go(){}
back(){}
// createMap将数组转换为对象结构
createMap(routes){
return routes.reduce((memo,current)=>{
// memo刚开始是个空对象
memo[current.path]=current.component
return memo;
},{})
}
}
VueRouter.install=function(Vue){
Vue.mixin({
// 将每个组件都混入一个 beforeCreate
beforeCreate() {
// 获取根组件
if(this.$options&&this.$options.router){
// 找到根组件
// 把当前实例挂载到_router上面
this._router = this.$options.router
this._root=this;
Vue.util.defineReactive(this,"xxx",this._router,history)
}else{
// main---->app----->home/about
this._root = this.$parent._root; //所有组件都有了router
}
Object.defineProperty(this,"$router",{
get(){
// console.log(this._root)
return this._root._router
}
})
Object.defineProperty(this,"$route",{
get(){
// console.log(this._root._router.history.current)
return {
current:this._root._router.history.current
}
}
})
},
}),
Vue.component("router-link",{
props: {
to:String
},
render(h){
let mode = this._self._root._router.mode;
return <a href={mode==='hash'?`#${this.to}`:this.to}>{this.$slots.default}</a>
}
}),
Vue.component("router-view",{
render(h) {
let current=this._self._root._router.history.current;
// console.log(this)
let routesMap=this._self._root._router.routesMap
return h(routesMap[current])
},
})
}
export default VueRouter