随着前端应用的业务功能起来越复杂,用户对于使用体验的要求愈来愈高,单面(SPA
)成为前端应用的主流形式。大型单页应用最显著特色之一就是采用的前端路由系统,经过改变URL
,在不从新请求页面的状况下,更新页面视图。html
更新视图但不从新请求页面,是前端路由原理的核心之一,目前在浏览器环境中这一功能的实现主要有2
种方式:前端
URL
中的hash
("#"
);History interface
在HTML5
中新增的方法;vue-router
是Vue.js
框架的路由插件,它是经过mode
这一参数控制路由的实现模式的:vue
const router=new VueRouter({ mode:'history', routes:[...] })
建立VueRouter
的实例对象时,mode
以构造参数的形式传入。ajax
src/index.js 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}`) } } } init (app: any /* Vue component instance */) { const history = this.history // 根据history的类别执行相应的初始化操做和监听 if (history instanceof HTML5History) { history.transitionTo(history.getCurrentLocation()) } else if (history instanceof HashHistory) { const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) } history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) } // 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) } }
mode
只是一个标记,用来指示实际起做用的对象属性history
的实现类,二者对应关系:modehistory: 'history':HTML5History; 'hash':HashHistory; 'abstract':AbstractHistory;
history
以前,会对mode
作一些校验:若浏览器不支持HTML5History
方式(经过supportsPushState
变量判断),则mode
设为hash
;若不是在浏览器环境下运行,则mode
设为abstract
;VueRouter
类中的onReady()
,push()
等方法只是一个代理,实际是调用的具体history
对象的对应方法,在init()
方法中初始化时,也是根据history
对象具体的类别执行不一样操做HashHistory
hash
("#"
)符号的原本做用是加在URL
指示网页中的位置:vue-router
http://www.example.com/index.html#print
#
自己以及它后面的字符称之为hash
可经过window.location.hash
属性读取.后端
hash
虽然出如今url
中,但不会被包括在http
请求中,它是用来指导浏览器动做的,对服务器端彻底无用,所以,改变hash
不会从新加载页面。hash
的改变添加监听事件:window.addEventListener("hashchange",funcRef,false)
hash
(window.location.hash
),都会在浏览器访问历史中增长一个记录。利用hash
的以上特色,就能够来实现前端路由"更新视图但不从新请求页面"的功能了。数组
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 }
transitionTo()
方法是父类中定义的是用来处理路由变化中的基础逻辑的,push()
方法最主要的是对window
的hash
进行了直接赋值:浏览器
window.location.hash=route.fullPath
hash
的改变会自动添加到浏览器的访问历史记录中。
那么视图的更新是怎么实现的呢,咱们来看看父类History
中的transitionTo()
方法:前端框架
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { const route = this.router.match(location, this.current) this.confirmTransition(route, () => { this.updateRoute(route) ... }) } updateRoute (route: Route) { this.cb && this.cb(route) } listen (cb: Function) { this.cb = cb }
能够看到,当路由变化时,调用了Hitory
中的this.cb
方法,而this.cb
方法是经过History.listen(cb)
进行设置的,回到VueRouter
类定义中,找到了在init()
中对其进行了设置:服务器
init (app: any /* Vue component instance */) { this.apps.push(app) history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) }
app
为Vue
组件实例,可是Vue
做为渐进式的前端框架,自己的组件定义中应该是没有有关路由内置属性_route
,若是组件中要有这个属性,应该是在插件加载的地方,即VueRouter
的install()
方法中混入Vue
对象的,install.js
的源码:
export function install (Vue) { Vue.mixin({ beforeCreate () { if (isDef(this.$options.router)) { this._router = this.$options.router this._router.init(this) Vue.util.defineReactive(this, '_route', this._router.history.current) } registerInstance(this, this) }, }) }
经过Vue.mixin()
方法,全局注册一个混合,影响注册以后全部建立的每一个Vue
实例,该混合在beforeCreate
钩子中经过Vue.util.defineReactive()
定义了响应式的_route
属性。所谓响应式属性,即当_route
值改变时,会自动调用Vue
实例的render()
方法,更新视图。
$router.push()-->HashHistory.push()-->History.transitionTo()-->History.updateRoute()-->{app._route=route}-->vm.render()
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.push()
和VueRouter.replace()
是能够在vue
组件的逻辑代码中直接调用的,除此以外在浏览器中,用户还能够直接在浏览器地址栏中输入改变路由,所以还须要监听浏览器地址栏中路由的变化 ,并具备与经过代码调用相同的响应行为,在HashHistory
中这一功能经过setupListeners
监听hashchange
实现:
setupListeners () { window.addEventListener('hashchange', () => { if (!ensureSlash()) { return } this.transitionTo(getHash(), route => { replaceHash(route.fullPath) }) }) }
该方法设置监听了浏览器事件hashchange
,调用的函数为replaceHash
,即在浏览器地址栏中直接输入路由至关于代码调用了replace()
方法。
HTML5History
History interface
是浏览器历史记录栈提供的接口,经过back()
,forward()
,go()
等方法,咱们能够读取浏览器历史记录栈的信息,进行各类跳转操做。
从HTML5
开始,History interface
提供了2个新的方法:pushState()
,replaceState()
使得咱们能够对浏览器历史记录栈进行修改:
window.history.pushState(stateObject,title,url) window.history,replaceState(stateObject,title,url)
stateObject
:当浏览器跳转到新的状态时,将触发popState
事件,该事件将携带这个stateObject
参数的副本title
:所添加记录的标题url
:所添加记录的url
这2
个方法有个共同的特色:当调用他们修改浏览器历史栈后,虽然当前url
改变了,但浏览器不会当即发送请求该url
,这就为单页应用前端路由,更新视图但不从新请求页面提供了基础。
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) } replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { replaceState(cleanPath(this.base + route.fullPath)) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } // src/util/push-state.js 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) { history.replaceState({ key: _key }, '', url) } else { _key = genKey() history.pushState({ key: _key }, '', url) } } catch (e) { window.location[replace ? 'replace' : 'assign'](url) } } export function replaceState (url?: string) { pushState(url, true) }
代码结构以及更新视图的逻辑与hash
模式基本相似,只不过将对window.location.hash()
直接进行赋值window.location.replace()
改成了调用history.pushState()
和history.replaceState()
方法。
在HTML5History
中添加对修改浏览器地址栏URL
的监听popstate
是直接在构造函数中执行的:
constructor (router: Router, base: ?string) { window.addEventListener('popstate', e => { const current = this.current this.transitionTo(getLocation(this.base), route => { if (expectScroll) { handleScroll(router, route, current, true) } }) }) }
HTML5History
用到了HTML5
的新特性,须要浏版本的支持,经过supportsPushState
来检查:
src/util/push-state.js export const supportsPushState = inBrowser && (function () { const ua = window.navigator.userAgent if ( (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) && ua.indexOf('Mobile Safari') !== -1 && ua.indexOf('Chrome') === -1 && ua.indexOf('Windows Phone') === -1 ) { return false } return window.history && 'pushState' in window.history })()
以上就是hash
模式与history
模式源码导读,这2
种模式都是经过浏览器接口实现的,除此以外,vue-router
还为非浏览器环境准备了一个abstract
模式,其原理为用一个数组stack
模拟出浏览器历史记录栈的功能。
通常的需求场景中,hash
模式与history
模式是差很少的,根据MDN
的介绍,调用history.pushState()
相比于直接修改hash
主要有如下优点:
pushState
设置的新url
能够是与当前url
同源的任意url
,而hash
只可修改#
后面的部分,故只可设置与当前同文档的url
pushState
设置的新url
能够与当前url
如出一辙,这样也会把记录添加到栈中,而hash
设置的新值必须与原来不同才会触发记录添加到栈中pushState
经过stateObject
能够添加任意类型的数据记录中,而hash
只可添加短字符串pushState
可额外设置title
属性供后续使用history
模式的问题对于单页应用来讲,理想的使用场景是仅在进入应用时加载index.html
,后续在的网络操做经过ajax
完成,不会根据url
从新请求页面,可是若是用户直接在地址栏中输入并回车,浏览器重启从新加载等特殊状况。
hash
模式仅改变hash
部分的内容,而hash
部分是不会包含在http
请求中的(hash
带#
):
http://oursite.com/#/user/id //如请求,只会发送http://oursite.com/
因此hash
模式下遇到根据url
请求页面不会有问题
而history
模式则将url
修改的就和正常请求后端的url
同样(history
不带#
)
http://oursite.com/user/id
若是这种向后端发送请求的话,后端没有配置对应/user/id
的get
路由处理,会返回404
错误。
官方推荐的解决办法是在服务端增长一个覆盖全部状况的候选资源:若是 URL
匹配不到任何静态资源,则应该返回同一个 index.html
页面,这个页面就是你 app
依赖的页面。同时这么作之后,服务器就再也不返回 404
错误页面,由于对于全部路径都会返回 index.html
文件。为了不这种状况,在 Vue
应用里面覆盖全部的路由状况,而后在给出一个 404
页面。或者,若是是用 Node.js
做后台,可使用服务端的路由来匹配 URL
,当没有匹配到路由的时候返回 404
,从而实现 fallback
。