当前位置:   article > 正文

【源码拾遗】从vue-router看前端路由的两种实现_vue-router abstract

vue-router abstract

模式参数

在vue-router中是通过mode这一参数控制路由的实现模式的:

  1. const router = new VueRouter({
  2. mode: 'history',
  3. routes: [...]
  4. })

创建VueRouter的实例对象时,mode以构造函数参数的形式传入。带着问题阅读源码,我们就可以从VueRouter类的定义入手。一般插件对外暴露的类都是定义在源码src根目录下的index.js文件中,打开该文件,可以看到VueRouter类的定义,摘录与mode参数有关的部分如下:

  1. export default class VueRouter {
  2. mode: string; // 传入的字符串参数,指示history类别
  3. history: HashHistory | HTML5History | AbstractHistory; // 实际起作用的对象属性,必须是以上三个类的枚举
  4. fallback: boolean; // 如浏览器不支持,'history'模式需回滚为'hash'模式
  5. constructor (options: RouterOptions = {}) {
  6. let mode = options.mode || 'hash' // 默认为'hash'模式
  7. // this.fallback是用来判断当前mode = 'hash'是不是通过降级处理的
  8. this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false // 通过supportsPushState判断浏览器是否支持'history'模式
  9. if (this.fallback) {
  10. mode = 'hash'
  11. }
  12. if (!inBrowser) {
  13. mode = 'abstract' // 不在浏览器环境下运行需强制为'abstract'模式
  14. }
  15. this.mode = mode
  16. // 根据mode确定history实际的类并实例化
  17. switch (mode) {
  18. case 'history':
  19. this.history = new HTML5History(this, options.base)
  20. break
  21. case 'hash':
  22. this.history = new HashHistory(this, options.base, this.fallback)
  23. break
  24. case 'abstract':
  25. this.history = new AbstractHistory(this, options.base)
  26. break
  27. default:
  28. if (process.env.NODE_ENV !== 'production') {
  29. assert(false, `invalid mode: ${mode}`)
  30. }
  31. }
  32. }
  33. init (app: any /* Vue component instance */) {
  34. const history = this.history
  35. // 根据history的类别执行相应的初始化操作和监听
  36. if (history instanceof HTML5History) {
  37. history.transitionTo(history.getCurrentLocation())
  38. } else if (history instanceof HashHistory) {
  39. const setupHashListener = () => {
  40. history.setupListeners()
  41. }
  42. history.transitionTo(
  43. history.getCurrentLocation(),
  44. setupHashListener,
  45. setupHashListener
  46. )
  47. }
  48. history.listen(route => {
  49. this.apps.forEach((app) => {
  50. app._route = route
  51. })
  52. })
  53. }
  54. // VueRouter类暴露的以下方法实际是调用具体history对象的方法
  55. push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  56. this.history.push(location, onComplete, onAbort)
  57. }
  58. replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  59. this.history.replace(location, onComplete, onAbort)
  60. }
  61. }

可以看出:

  1. 作为参数传入的字符串属性mode只是一个标记,用来指示实际起作用的对象属性history的实现类,两者对应关系如下:

    modehistory 'history'HTML5History 'hash'HashHistory 'abstract'AbstractHistory
  2. 在初始化对应的history之前,会对mode做一些校验:若浏览器不支持HTML5History方式(通过supportsPushState变量判断),则mode强制设为'hash';若不是在浏览器环境下运行,则mode强制设为'abstract'

  3. VueRouter类中的onReady(), push()等方法只是一个代理,实际是调用的具体history对象的对应方法,在init()方法中初始化时,也是根据history对象具体的类别执行不同操作

在浏览器环境下的两种方式,分别就是在HTML5History,HashHistory两个类中实现的。他们都定义在src/history文件夹下,继承自同目录下base.js文件中定义的History类。History中定义的是公用和基础的方法,直接看会一头雾水,我们先从HTML5History,HashHistory两个类中看着亲切的push(), replace()方法的说起。

HashHistory

看源码前先回顾一下原理:

hash(“#”)符号的本来作用是加在URL中指示网页中的位置:

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()

我们来看HashHistory中的push()方法:

  1. push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  2. this.transitionTo(location, route => {
  3. pushHash(route.fullPath)
  4. onComplete && onComplete(route)
  5. }, onAbort)
  6. }
  7. function pushHash (path) {
  8. window.location.hash = path
  9. }

transitionTo()方法是父类中定义的是用来处理路由变化中的基础逻辑的,push()方法最主要的是对window的hash进行了直接赋值:

window.location.hash = route.fullPath

hash的改变会自动添加到浏览器的访问历史记录中。 

那么视图的更新是怎么实现的呢,我们来看父类History中transitionTo()方法的这么一段:

  1. transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  2. const route = this.router.match(location, this.current)
  3. this.confirmTransition(route, () => {
  4. this.updateRoute(route)
  5. ...
  6. })
  7. }
  8. updateRoute (route: Route) {
  9. this.cb && this.cb(route)
  10. }
  11. listen (cb: Function) {
  12. this.cb = cb
  13. }

可以看到,当路由变化时,调用了History中的this.cb方法,而this.cb方法是通过History.listen(cb)进行设置的。回到VueRouter类定义中,找到了在init()方法中对其进行了设置:

  1. init (app: any /* Vue component instance */) {
  2. this.apps.push(app)
  3. history.listen(route => {
  4. this.apps.forEach((app) => {
  5. app._route = route
  6. })
  7. })
  8. }

根据注释,app为Vue组件实例,但我们知道Vue作为渐进式的前端框架,本身的组件定义中应该是没有有关路由内置属性_route,如果组件中要有这个属性,应该是在插件加载的地方,即VueRouter的install()方法中混合入Vue对象的,查看install.js源码,有如下一段:

  1. export function install (Vue) {
  2. Vue.mixin({
  3. beforeCreate () {
  4. if (isDef(this.$options.router)) {
  5. this._router = this.$options.router
  6. this._router.init(this)
  7. Vue.util.defineReactive(this, '_route', this._router.history.current)
  8. }
  9. registerInstance(this, this)
  10. },
  11. })
  12. }

通过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()方法不同之处在于,它并不是将新路由添加到浏览器访问历史的栈顶,而是替换掉当前的路由:

  1. replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  2. this.transitionTo(location, route => {
  3. replaceHash(route.fullPath)
  4. onComplete && onComplete(route)
  5. }, onAbort)
  6. }
  7. function replaceHash (path) {
  8. const i = window.location.href.indexOf('#')
  9. window.location.replace(
  10. window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
  11. )
  12. }

可以看出,它与push()的实现结构上基本相似,不同点在于它不是直接对window.location.hash进行赋值,而是调用window.location.replace方法将路由进行替换。

监听地址栏

以上讨论的VueRouter.push()和VueRouter.replace()是可以在vue组件的逻辑代码中直接调用的,除此之外在浏览器中,用户还可以直接在浏览器地址栏中输入改变路由,因此VueRouter还需要能监听浏览器地址栏中路由的变化,并具有与通过代码调用相同的响应行为。

先来看看 hash的方式,当发生变得时候会判断当前浏览器环境是否支持 supportsPushState 来选择监听 popstate还是hashchange

  1. window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
  2. const current = this.current
  3. if (!ensureSlash()) {
  4. return
  5. }
  6. this.transitionTo(getHash(), route => {
  7. if (supportsScroll) {
  8. handleScroll(this.router, route, current, true)
  9. }
  10. if (!supportsPushState) {
  11. replaceHash(route.fullPath)
  12. }
  13. })
  14. })

对应的history其实也是差不多。只不过既然是history模式了,默认也就只用监听popstate就好了:

  1. window.addEventListener('popstate', e => {
  2. const current = this.current
  3. // Avoiding first `popstate` event dispatched in some browsers but first
  4. // history route not updated since async guard at the same time.
  5. const location = getLocation(this.base)
  6. if (this.current === START && location === initLocation) {
  7. return
  8. }
  9. this.transitionTo(location, route => {
  10. if (supportsScroll) {
  11. handleScroll(router, route, current, true)
  12. }
  13. })
  14. })

HTML5History

History interface是浏览器历史记录栈提供的接口,通过back(), forward(), go()等方法,我们可以读取浏览器历史记录栈的信息,进行各种跳转操作。

HTML5开始,History interface提供了两个新的方法:pushState(), replaceState()使得我们可以对浏览器历史记录栈进行修改:

  1. window.history.pushState(stateObject, title, URL)
  2. window.history.replaceState(stateObject, title, URL)
  • stateObject: 当浏览器跳转到新的状态时,将触发popState事件,该事件将携带这个stateObject参数的副本

  • title: 所添加记录的标题

  • URL: 所添加记录的URL

这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前URL改变了,但浏览器不会立即发送请求该URL(the browser won't attempt to load this URL after a call to pushState()),这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。

我们来看vue-router中的源码:

  1. push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  2. const { current: fromRoute } = this
  3. this.transitionTo(location, route => {
  4. pushState(cleanPath(this.base + route.fullPath))
  5. handleScroll(this.router, route, fromRoute, false)
  6. onComplete && onComplete(route)
  7. }, onAbort)
  8. }
  9. replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  10. const { current: fromRoute } = this
  11. this.transitionTo(location, route => {
  12. replaceState(cleanPath(this.base + route.fullPath))
  13. handleScroll(this.router, route, fromRoute, false)
  14. onComplete && onComplete(route)
  15. }, onAbort)
  16. }
  17. // src/util/push-state.js
  18. export function pushState (url?: string, replace?: boolean) {
  19. saveScrollPosition()
  20. // try...catch the pushState call to get around Safari
  21. // DOM Exception 18 where it limits to 100 pushState calls
  22. const history = window.history
  23. try {
  24. if (replace) {
  25. history.replaceState({ key: _key }, '', url)
  26. } else {
  27. _key = genKey()
  28. history.pushState({ key: _key }, '', url)
  29. }
  30. } catch (e) {
  31. window.location[replace ? 'replace' : 'assign'](url)
  32. }
  33. }
  34. export function replaceState (url?: string) {
  35. pushState(url, true)
  36. }

代码结构以及更新视图的逻辑与hash模式基本类似,只不过将对window.location.hash直接进行赋值window.location.replace()改为了调用history.pushState()和history.replaceState()方法。

在HTML5History中添加对修改浏览器地址栏URL的监听是直接在构造函数中执行的:

  1. constructor (router: Router, base: ?string) {
  2. window.addEventListener('popstate', e => {
  3. const current = this.current
  4. this.transitionTo(getLocation(this.base), route => {
  5. if (expectScroll) {
  6. handleScroll(router, route, current, true)
  7. }
  8. })
  9. })
  10. }

当然了HTML5History用到了HTML5的新特特性,是需要特定浏览器版本的支持的,前文已经知道,浏览器是否支持是通过变量supportsPushState来检查的:

  1. // src/util/push-state.js
  2. export const supportsPushState = inBrowser && (function () {
  3. const ua = window.navigator.userAgent
  4. if (
  5. (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
  6. ua.indexOf('Mobile Safari') !== -1 &&
  7. ua.indexOf('Chrome') === -1 &&
  8. ua.indexOf('Windows Phone') === -1
  9. ) {
  10. return false
  11. }
  12. return window.history && 'pushState' in window.history
  13. })()

以上就是hash模式与history模式源码的导读,这两种模式都是通过浏览器接口实现的,除此之外vue-router还为非浏览器环境准备了一个abstract模式,其原理为用一个数组stack模拟出浏览器历史记录栈的功能。当然,以上只是一些核心逻辑,为保证系统的鲁棒性源码中还有大量的辅助逻辑,也很值得学习。此外在vue-router中还有路由匹配、router-view视图组件等重要部分,关于整体源码的阅读推荐滴滴前端的这篇文章

参考

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/633242
推荐阅读
相关标签
  

闽ICP备14008679号