通常我们使用Vue来开发单页面应用(SPA)时,通常都会使用vue-router来实现页面路由跳转。单页面应用采用前端路由系统,通过改变URL,在不重新请求页面的情况下,更新页面视图。
Vue-router提供了以下几种路由跳转方法
方法 | 作用 |
---|---|
router.push | 添加新路由 |
router.replace | 替换当前路由 |
router.go | 跳转到指定索引路由 |
router.back | 返回上一个路由 |
router.forward | 跳转下一个路由 |
下面我们通过vue-router源码来了解实现原理:
模式参数
通常通过mode参数来指定模式,默认为hash模式,代码中明确指定mode为history时为history模式
const router = new VueRouter({
mode: 'history',
routes: [...]
})
下面我们来看VueRouter类的代码
export default class VueRouter {
static install: () => void
static version: string
static isNavigationFailure: Function
static NavigationFailureType: any
app: any
apps: Array<any>
ready: boolean
readyCbs: Array<Function>
options: RouterOptions
mode: string
history: HashHistory | HTML5History | AbstractHistory
matcher: Matcher
fallback: boolean
beforeHooks: Array<?NavigationGuard>
resolveHooks: Array<?NavigationGuard>
afterHooks: Array<?AfterNavigationHook>
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
let mode = options.mode || 'hash'
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
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}`)
}
}
}
constructor构造方法中,通过判断传入的mode参数来初始化history属性
mode | history |
---|---|
history | HTML5History |
hash | HashHistory |
abstract | AbstractHistory |
初始化之前,会对mode做校验,如果浏览器不支持HTML5History方式,会强制修改模式为hash;如果不是在浏览器里面运行的,会强制修改模式为abstract
hash和history两种模式的主要区别是push和replace实现原理的区别,下面详细分析具体实现原理:
hashHistory
浏览器地址栏URL中的#则为hash,我们可以通过window.location.hash来获取hash值,通常带有hash值得URL类似于这样:http://www.xx.x/index.html#test
hash值有以下几个特点:
- hash虽然包含在URL中,但不会被包括在HTTP请求中。它是用来指导浏览器动作的,对服务器端完全无用,因此,改变hash不会重新加载页面
- 可以添加用于监听hash值变化的事件
window.addEventListener("hashchange", funcRef, false)
- 每一次改变hash值,都会在浏览器的访问历史中添加一条记录
利用以上几个特点,前端路由可以实现更新视图但不重新请求页面
push()方法的实现
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
//直接赋值hash
window.location.hash = path
}
}
transitionTo()方法是父类中定义的是用来处理路由变化中的基础逻辑的,push方法主要通过直接对window.location.hash赋值,hash值得改变会自动记录到浏览器访问历史记录中
replace()方法的实现
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
replaceHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
function getUrl (path) {
//window.location.href为浏览器当前URL
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
//返回更新hash值后的URL
return `${base}#${path}`
}
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
replace()方法中,通过更新当前路由地址中hash值来覆盖当期路由
添加监听事件
setupListeners () {
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
this.listeners.push(setupScroll())
}
const handleRoutingEvent = () => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
const eventType = supportsPushState ? 'popstate' : 'hashchange'
window.addEventListener(
eventType,
handleRoutingEvent
)
this.listeners.push(() => {
window.removeEventListener(eventType, handleRoutingEvent)
})
}
在浏览器中,用户还可以直接在浏览器地址栏中输入改变路由,因此VueRouter还需要能监听浏览器地址栏中路由的变化,并具有与通过代码调用相同的响应行为。
setupListeners()方法设置监听了浏览器事件hashchange,调用的函数为replaceHash,即在浏览器地址栏中直接输入路由相当于代码调用了replace()方法
HTML5History
history模式通过window.history提供的方法来实现路由跳转
pushState和replaceState方法修改浏览器历史记录栈后,虽然当前URL改变了,但浏览器不会立即发送请求该URL(the browser won’t attempt to load this URL after a call to pushState()),这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {
if (replace) {
// preserve existing history state as it could be overriden by the user
const stateCopy = extend({}, history.state)
stateCopy.key = getStateKey()
history.replaceState(stateCopy, '', url)
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
export function replaceState (url?: string) {
pushState(url, true)
}
分别通过调用history.pushState()和history.replaceState()方法来实现push和replace方法
setupListeners () {
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
this.listeners.push(setupScroll())
}
const handleRoutingEvent = () => {
const current = this.current
// Avoiding first `popstate` event dispatched in some browsers but first
// history route not updated since async guard at the same time.
const location = getLocation(this.base)
if (this.current === START && location === this._startLocation) {
return
}
this.transitionTo(location, route => {
if (supportsScroll) {
handleScroll(router, route, current, true)
}
})
}
window.addEventListener('popstate', handleRoutingEvent)
this.listeners.push(() => {
window.removeEventListener('popstate', handleRoutingEvent)
})
}
hash和history模式go、back和forward
两种模式的go、back和forward三个方法都是通过window.history.go()来实现的
back和forward通过改变go方法的入参来实现
abstract模式
vue-router为非浏览器环境准备了一个abstract模式,其原理为用一个数组stack模拟出浏览器历史记录栈的功能
hash和history模式对比
- hash模式在浏览器地址栏中会带有’#’,可能不是很美观
- history模式可以设置整个URL,而hash模式只可修改#后面的部分
- history模式新URL与当前URL一样的情况下也会把记录添加到栈中;而hash模式只有设置的新值必须与原来不一样才会触发记录添加到栈中
- history模式可以添加任意类型的数据到记录中,而hash模式只能添加短字符串
- history模式可额外设置title属性供后续使用
- history模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问不存在的地址时就会返回 404,这就不好看了,具体配置可以参考官方文档链接