窗外的绿色满满撞入我的眼睛,柳絮隔着纱窗热情的邀舞,可惜我不能出去。好了,这些都是题外话。
一、实现原理
- ps: “更新视图但不重新请求页面”是前端路由原理的核心之一
(参考:https://zhuanlan.zhihu.com/p/27588422)
vue-router 三种运行模式:
- hash: 使用 URL hash 值("#")来作路由。默认模式。
- history: 依赖 HTML5 History API 和服务器配置。
- abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。
- ps:在vue-router中是通过mode这一参数控制路由的实现模式的:
var router = new VueRouter({
mode: 'history', //默认是hash
routes: [
...
]
});
Hash模式
- hash(“
#
”)符号的本来作用是加在URL中指示网页中的位置; - hash虽然出现在URL中,但不会被包括在HTTP请求中。它是用来指导浏览器动作的,对服务器端完全无用,因此,改变hash不会重新加载页面;
-
hash
模式下通过hashchange
方法可以监听url中hash的变化,来实现更新页面部分内容的操作:
window.addEventListener("hashchange", function(){}, false)
- 每一次改变hash,都会在浏览器的访问历史中增加一个记录(利用
HashHistory.push()
或HashHistory.replace()
),可以实现浏览器的前进和后退功能。
1. HashHistory.push()
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
pushHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}
function pushHash (path) {
window.location.hash = path
}
- 由此可见,push()方法最主要的是对window的hash进行了直接赋值:
window.location.hash = route.fullPath
-
hash的改变会自动添加到浏览器的访问历史记录中。
2. HashHistory.replace()
- replace()方法与push()方法不同之处在于,它并不是将新路由添加到浏览器访问历史的栈顶,而是替换掉当前的路由:
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
replaceHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}
function replaceHash (path) {
const i = window.location.href.indexOf('#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
)
}
- 可以看出,它与push()的实现结构上基本相似,不同点在于它不是直接对window.location.hash进行赋值,而是调用window.location.replace方法将路由进行替换。
总得来说就是初始化vueRouter的时候,建立路由和组件的映射关系,监听hashChange事件来更新路由,进而渲染视图
HTML5History模式
- 通过
window.history.pushState(stateObject, title, URL)
与window.history.replaceState(stateObject, title, URL)
修改浏览器地址,触发更新; - 通过监听
popstate
事件监听浏览器前进或者后退,触发更新;
- stateObject: 当浏览器跳转到新的状态时,将触发popState事件,该事件将携带这个stateObject参数的副本。
- title: 所添加记录的标题(暂时没有用处)。
- URL: 所添加记录的URL。
这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前URL改变了,但浏览器不会立即发送请求该URL,这就为单页应用前端路由“更新视图但不重新请求页面 ”提供了基础。
两种模式比较
hash模式 | history模式 |
---|---|
于多了一个# ,所以url整体上不够美观 |
当用户刷新或直接输入地址时会向服务器发送一个请求,所以需要服务端同学进行支持, 将路由都重定向到根路由 |
hash只可修改#后面的部分,故只可设置与当前同文档的URL | pushState设置的新URL可以是与当前URL同源的任意URL |
hash设置的新值必须与原来不一样才会触发记录添加到栈中 | pushState设置的新URL可以与当前URL一模一样,这样也会把记录添加到栈中 |
hash只可添加短字符串 | pushState通过stateObject可以添加任意类型的数据到记录中 |
pushState可额外设置title属性供后续使用 |
vue-router源码分析:
install.js 分析
- 首先会对重复安装进行过滤
- 全局混入
beforeCreate
和destroyed
生命钩子,为每个Vue实例设置_routerRoot
属性,并为根实例设置_router
属性 - 调用Vue中定义的
defineReactive
对_route
进行劫持,其实是执行的依赖收集的过程,执行_route的get
就会对当前的组件进行依赖收集,如果对_route进行重新赋值触发setter
就会使收集的组件重新渲染,这里也是路由重新渲染的核心所在
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) { // 设置根路由-根组件实例
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
// 定义响应式的 _route 对象
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else { // 非根组件设置
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
- 为Vue原型对象定义router和router和router和route属性,并对两个属性进行了劫持,使我们可以直接通过Vue对象实例访问到
- 全局注册了Routerview和RouterLink两个组件,所以我们才可以在任何地方使用这两个组件,这两个组件的内容我们稍后分析
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
vue-router工作流程:
- 安装 vue-router 插件(参考 install.js分析)
- new Router 实例
- 根实例创建之前,执行init方法,初始化路由
- 执行transitionTo方法,同时hash模式下对浏览器hashChange事件进行了监听,执行history.listen方法,将对_route重新赋值的函数赋给History实例的callback,当路由改变时对_route进行重新赋值从而触发组件更新
- transitionTo方法根据传入的路径从我们定义的所有路由中匹配到对应路由,然后执行confirmTransition
- confirmTransition首先会有重复路由的判断。如果进入相同的路由,直接调用abort回调函数,函数退出,不会执行后面的各组件的钩子函数,这也是为什么我们重复进入相同路由不会触发组建的重新渲染也不会触发路由的各种钩子函数;如果判断不是相同路由,就会执行各组件的钩子函数
- 按顺序执行好导航守卫后,就会执行传入的成功的回调函数,从而对_route进行赋值,触发setter,从而使组件重新渲染
二、使用
1.路由文件的定义
/*
* 路由器模块
* */
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter);
import Home from '../views/home'
import Messages from '../views/messages'
import MessagesDetail from '../views/messagesDetail'
function loadView(view) {
return () => import(`@/views/${view}.vue`)
}
var router = new VueRouter({
mode: 'history',
routes: [
{
name: 'app',
path: '/',
// component: App,
redirect: '/home',
},
{
name: 'about',
path: '/about',
component: loadView('about'),
children: [
{
name: 'news',
path: '/about/news',
component: loadView('news')
},
{
name: 'messages',
path: '/about/messages',
component: Messages,
children: [
{
path: '/about/messages/:id',
component: MessagesDetail,
//主要用于写某个指定路由跳转时需要执行的逻辑
beforeEnter: (to, from, next) => {
console.log('beforeEnter-to: ', to)
console.log('beforeEnter-from: ', from)
next();
},
afterEnter: (to, from, next) => {
console.log('---afterEnter-to: ', to)
console.log('---afterEnter-from: ', from)
next()
}
},
],
/*
*某个路由的路由钩子函数
*/
//主要用于写某个指定路由跳转时需要执行的逻辑
beforeEnter: (to, from, next) => {
console.log('beforeEnter-to: ', to)
console.log('beforeEnter-from: ', from)
next();
},
afterEnter: (to, from, next) => {
console.log('-----afterEnter-to: ', to)
console.log('-----afterEnter-from: ', from)
next()
}
},
]
},
{
name: 'home',
path: '/home',
component: Home,
}
]
});
/*
*全局路由钩子
*/
//这类钩子主要作用于全局,一般用来判断权限,以及以及页面丢失时候需要执行的操作
router.beforeEach((to, from, next) => {
console.log('beforeEach-to: ', to)
console.log('beforeEach-from: ', from)
next();
})
router.afterEach((to, from, next) => {
console.log('afterEach-to: ', to)
console.log('afterEach-from: ', from)
})
export default router
- 另:需将路由全局注入main.js
import Vue from 'vue'
import App from './App.vue'
import router from '@/router'
import less from 'less'
Vue.use(less)
require('./assets/style/iconfont.css')
Vue.config.productionTip = false; //作用是阻止 vue 在启动时生成生产提示
new Vue({
router,
render: h => h(App)
}).$mount('#app')
三、有关知识点
1. router 与 route
先上一张图来:
- 由此可见,router是VueRouter的一个对象,通过Vue.use(VueRouter)和VueRouter的构造函数得到的实力对象,该对象是一个全局对象,包含了许多关键对象与属性,例如:history:
methods: {
go(num){
if (num === 1){
this.$router.replace({ name: 'news' })
}else if (num === 2){
this.$router.push({ name: 'messages' })
}
}
},
- $route是一个当前路由的路由对象,每个路由都会有一个route对象,是一个局部对象,可获取对应的name,path,params,query等
ID:{{$route.params.id}}
2. 路由钩子(分三类)
在使用那部分已经贴出完整的代码,以及应用场景,故此处只做简单的列举:
- 全局路由钩子
——这类钩子主要作用于全局,一般用来判断权限,以及以及页面丢失时候需要执行的操作
router.beforeEach((to, from, next) => {
next();
})
router.afterEach((to, from, next) => {
})
- 某个路由独有的路由钩子
——主要用于写某个指定路由跳转时需要执行的逻辑
beforeEnter: (to, from, next) => {
next();
},
afterEnter: (to, from, next) => {
next()
}
- 路由组件内的路由钩子
export default {
name: "messages",
data() {
return {}
},
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不能获取组件实例 `this`
// 因为当钩子执行前,组件实例还没被创建
next();
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
next()
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
next()
}
}
3. 子路由
{{v.content}}
.router-link-active{
color: burlywood !important;
}
- 在动态组件上使用
keep-alive
——当在多个组件之间切换的时候,有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。
(https://cn.vuejs.org/v2/guide/components-dynamic-async.html?_sw-precache=3b921049bd7cca2444e9efa512a9d9f5#%E5%9C%A8%E5%8A%A8%E6%80%81%E7%BB%84%E4%BB%B6%E4%B8%8A%E4%BD%BF%E7%94%A8-keep-alive "在动态组件上使用 keep-alive") - 当组件在
内被切换,它的 activated
和deactivated
这两个生命周期钩子函数将会被对应执行。 -
activated
钩子函数
——keep-alive 组件激活时调用。该钩子在服务器端渲染期间不被调用。 -
deactivated
钩子函数
——实例销毁之前调用。在这一步,实例仍然完全可用。该钩子在服务器端渲染期间不被调用。