vue 路由的两种模式:hash与history
一、前言
对于Vue
渐进式前端开发框架,为了构建SPA
(单页面应用),需要引入前端路由系统,这也就是Vue-router
存在的意义。前端路由的核心,就在于改变视图的同时不会向后端发出请求。
一、为了达到这个目的,浏览器提供了 hash
与history
两种支持:
为什么这两种模式实现了更新视图但不请求页面?
-
hash
——即地址栏URL
中的#
符号(此hash
不是密码学里的散列运算)。
比如这个URL:http://www.abc.com/#/hello
,hash
的值为#/hello
。它的特点在于:hash
虽然出现URL
中,但不会被包含在HTTP
请求中,它是用来指导浏览器动作的,对后端完全没有影响,因此改变hash
不会重新加载页面。 -
history
——利用了HTML5 History Interface
中新增的pushState()
和replaceState()
方法。(需要特定浏览器支持)
这两个方法应用于浏览器的历史记录栈,在当前已有的back
、forward
、go
基础上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的URL,但浏览器不会立即向后端发送请求。
history
模式,会出现404 的情况,需要后台配置。
二、404 错误
-
hash
模式下,仅hash
符号之前的内容会被包含在请求中,如http://www.abc.com
, 因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回404错误; -
history
模式下,前端的url
必须和实际向后端发起请求的url
一致,如http://www.abc.com/book/id
。如果后端缺少对/book/id
的路由处理,将返回404错误。
三、延伸阅读
vue-router
实例化如下:
const router = new VueRouter({
mode: "XXX",
routes: [...]
})
VueRouter
实例化如下:
export default class VueRouter {
mode: string; // 传入的字符串参数,指示history类别
history: HashHistory | HTML5History | AbstractHistory; // 实际起作用的对象属性,必须是以上三个类的枚举
fallback: boolean; // 如浏览器不支持,'history'模式需回滚为'hash'模式
constructor (options: RouterOptions = {}) {
let mode = options.mode || 'hash' // 默认为'hash'模式
this.fallback = mode === 'history' && !supportsPushState // 通过supportsPushState判断浏览器是否支持'history'模式
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract' // 不在浏览器环境下运行需强制为'abstract'模式
}
this.mode = mode
// 根据mode确定history实际的类并实例化
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
// 初始化操作, app表示组件实例
init (app: any /* Vue component instance */) {
// ...
this.apps.push(app)
// main app already initialized.
if (this.app) {
return
}
this.app = app
const history = this.history
// 根据history的类别执行相应的初始化操作和监听
if (history instanceof HTML5History) {
// html5模式
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
// hash模式
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
// 切换路由
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
// 一些钩子函数 beforeEach、 afterEach等
// ...
// ...
// VueRouter类暴露的以下方法实际是调用具体history对象的方法
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.push(location, onComplete, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.replace(location, onComplete, onAbort)
}
}
// 相对于当前页面向前或向后跳转多少个页面,类似 window.history.go(n)。n可为正数可为负数。正数返回上一个页面
go (n: number) {
this.history.go(n)
}
// 后退到上一个页面
back () {
this.go(-1)
}
// 前进到下一个页面
forward () {
this.go(1)
}
// ...
// ...
代码解读:
- 浏览器默认是
hash
模式,当浏览器不支持html5的history
模式时,也会强制为hash
模式;当环境不是浏览器时,强制为abstract
模式。 - 当创建
VueRouter
实例后,VueRouter
构造函数会通过传入对象options
的mode
参数,来调用对应的(HashHistory / HTML5History / AbstractHistory
)构造函数,进而创建对应的history
实例对象。 - 创建相应的
history
实例后,可以看到init
函数里面会有对应的初始化操作。
$router
实例有两个常见的跳转方法:push
与replace
方法,源码的最下面,就暴露出来这两个方法。很显然这两种方法都是对不同模式下的方法的封装,本质还是执行的对应模式上的方法。
HashHistory
中的push()
方法:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 执行transitionTo函数
this.transitionTo(location, route => {
// 改变hash值
pushHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}
function pushHash (path) {
window.location.hash = path
}
pushHash
直接对window.location.hash
赋值,hash
值变化之后,浏览器访问历史中就会增加一个记录。
记录增加了,如何更新视图呢?接着看父类History中transitionTo
函数是如何实现的。
transtitionTo
函数具体实现如下:
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
//找到匹配路由(match函数)
const route = this.router.match(location, this.current)
this.confirmTransition(route, () => { //确认是否转化
this.updateRoute(route) //更新route
// ...
})
}
//更新路由
updateRoute (route: Route) {
const prev = this.current // 跳转前路由
this.current = route // 准备跳转路由
this.cb && this.cb(route) // 回调函数,这一步很重要,这个回调函数在index文件中注册,会更新被劫持的数据 _router
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
}
// this.cb函数的定义
listen (cb: Function) {
this.cb = cb
}
在transitionTo
函数中,执行了updateRoute
更新路由函数,这个函数中执行了cb
这个函数,cb
这个函数是在VueRouter
实例中的init
函数中通过history.listen
传入的。
init (app: any /* Vue component instance */) {
// ...
this.apps.push(app)
// ...
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
上面的代码,从$router.push
分析到了cb()
函数, 再接着看cb函数,这个函数中有一个_route
属性,并且将匹配的路由route
赋给了app._route
。(这里app是组件实例,apps是所有组件实例一个数组) 。
那么_route
属性是什么,组件本身是没有定义这个属性的,那么这个属性从哪里来的呢?源码中在install()
方法中使用Vue.mixin()
方法添加一个全局的混合对象:
install.js
代码里面做了四件事:
- 混入
beforeCreate
函数,在里面定义_route
这个响应式属性。 - 混入
destroyed
函数。 - 在
Vue.prototype
上定义$router
、$route
这两个对象,以便每个组件都可以获得。 - 在Vue
上
注册router-link
与router-view
这两个组件。
// install.js代码:
export function install (Vue) {
if (install.installed && _Vue === Vue) return
install.installed = true
_Vue = Vue
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
// 使用mixin会在每个.vue文件中进行beforeCreate和destroyed
Vue.mixin({
//对每个Vue实例混入beforeCreate钩子操作
//验证Vue实例是否有router对象了,如果有,就不再初始化了
beforeCreate () {
// 如果没有router对象(this.$option.router来自于VueRouter实例router对象)
if (isDef(this.$options.router)) {
// 将_routerRoot指向当前组件
this._routerRoot = this
// 将router对象挂载到根组件的_router上
this._router = this.$options.router
// 调用VueRouter实例的init方法
this._router.init(this)
// 劫持数据_route,一旦_route数据变化之后,通知router-view执行render方法
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 如果有router对象,则将每一个组件的_routerRoot指向根Vue实例
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 注册vuecomponent实例
registerInstance(this, this)
},
destroyed () {
// 销毁vuecomponent实例
registerInstance(this)
}
})
//通过Vue.prototype定义$router、$route 属性(方便所有组件可以获取这两个属性)
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
//Vue上注册router-link和router-view两个组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
在beforeCreate
钩子中,通过Vue.util.defineReactive()
创建一个响应式_route
属性。router-view
组件挂载时,会生成一个watcher
,当hash
值变化时,匹配到新的路由route
后,_route
属性跟着改变,触发_route
属性的getter
,进行依赖收集watcher
,最后触发_route
的setter
,执行watcher
的更新函数,进而触发<router-view>
的render
函数,更新视图。
大致流程为:
$router.push-> history.push -> transitionTo->updateRoute->cb(即 app._route = route) -> render
- 触发
$router.push()
。 - 触发
HashHistory.push()
。 - 触发
transitionTo()
,根据传入的location
找到匹配的route
,触发updateRoute()
。 - 触发
updateRoute()
, 将匹配的route
赋给实例app
的_route
响应式属性, 当_route
属性变化时,就会触发实例的render
函数,更新视图。
四、拓展阅读
- 点赞
- 收藏
- 关注作者
评论(0)