最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 从零开始,用elementui躺坑Vue-Router原理分析

    正文概述 掘金(高志小鹏鹏)   2021-02-20   472

    上一篇,小编讲到在vue-router中是通过mode这一参数控制路由的实现模式的。今天就让我们深入去观摩vue-router源码是如何实现路由的

    路由起源 - 后端路由

    路由这个概念最先是后端出现的。在以前用模板引擎开发页面时,经常会看到这样的地址

    大致流程可以看成这样:

    1. 浏览器发起请求
    2. 服务器监听到端口请求,如(80, 443)请求,解析URL路径
    3. 根据服务器的配置,返回相应信息(html字符,json数据,图片...)
    4. 浏览器根据数据包的Content-Type来决定如何解析数据

    即:路由就是跟后端服务器的一种交互方式,通过不同的路径,来请求不同的资源,请求不同的页面是路由的其中一种功能

    前端路由

    随着前端应用的业务功能越来越复杂、用户对于使用体验的要求越来越高,单页应用(SPA)成为前端应用的主流形式。大型单页应用最显著特点之一就是采用前端路由系统,通过改变URL,在不重新请求页面的情况下,更新页面视图。

    "更新视图但不重新请求页面"是前端路由原理的核心之一,目前在浏览器环境中这一功能的实现主要有两种方式:

    1. hash模式: 利用浏览器中#
    2. history 模式: 利用History interface在 HTML5中新增的方法

    hash模式

    hash示例:

    hash模式:hash值是URL 的锚部分(从 # 号开始的部分)。hash值的变化并不会导致浏览器向服务器发起请求,浏览器不发起请求,从而不会刷新界面。另外每次 hash 值的变化,还会触发hashchange 这个事件,通过这个事件我们就可以知道 hash 值发生了哪些变化。然后我们便可以监听hashchange来实现更新页面部分内容的操作:

    function updateDom () {
       // todo 匹配 hash 做 dom 更新操作
    }
    
    window.addEventListener('hashchange', updateDom)
    

    history模式

    如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。

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

    源码分析

    我们找到VueRouter类的定义,摘录与mode参数有关的部分如下:

    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)
      }
    }
    
    1. 作为参数传入的字符串属性mode只是一个标记,用来指示实际起作用的对象属性history的实现类

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

    首先,我们来看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
    }
    
    

    可以看到,当路由变化时,调用了History中的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
      )
    }
    

    监听地址栏

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

    setupListeners () {
      window.addEventListener('hashchange', () => {
        if (!ensureSlash()) {
          return
        }
        this.transitionTo(getHash(), route => {
          replaceHash(route.fullPath)
        })
      })
    }
    

    该方法设置监听了浏览器事件hashchange,调用的函数为replaceHash,即在浏览器地址栏中直接输入路由相当于代码调用了replace()方法

    HTML5History

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

    window.history.pushState(stateObject, title, URL)
    window.history.replaceState(stateObject, title, URL)
    

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

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

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

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

    两种模式比较

    根据MDN的介绍,调用history.pushState()相比于直接修改hash主要有以下优势:

    1. pushState设置的新URL可以是与当前URL同源的任意URL;而hash只可修改#后面的部分,故只可设置与当前同文档的URL

    2. pushState设置的新URL可以与当前URL一模一样,这样也会把记录添加到栈中;而hash设置的新值必须与原来不一样才会触发记录添加到栈中

    3. pushState通过stateObject可以添加任意类型的数据到记录中;而hash只可添加短字符串

    4. pushState可额外设置title属性供后续使用

    AbstractHistory

    抽象模式是属于最简单的处理了,因为不涉及和浏览器地址相关记录关联在一起;整体流程依旧和 HashHistory 是一样的,只是这里通过数组来模拟浏览器历史记录堆栈信息。

    export class AbstractHistory extends History {
      index: number;
      stack: Array<Route>;
    // ...
    
      push (location: RawLocation) {
        this.transitionTo(location, route => {
          // 更新历史堆栈信息
          this.stack = this.stack.slice(0, this.index + 1).concat(route)
          // 更新当前所处位置
          this.index++
        })
      }
    
      replace (location: RawLocation) {
        this.transitionTo(location, route => {
          // 更新历史堆栈信息 位置则不用更新 因为是 replace 操作
          // 在堆栈中也是直接 replace 掉的
          this.stack = this.stack.slice(0, this.index).concat(route)
        })
      }
      // 对于 go 的模拟
      go (n: number) {
        // 新的历史记录位置
        const targetIndex = this.index + n
        // 超出返回了
        if (targetIndex < 0 || targetIndex >= this.stack.length) {
          return
        }
        // 取得新的 route 对象
        // 因为是和浏览器无关的 这里得到的一定是已经访问过的
        const route = this.stack[targetIndex]
        // 所以这里直接调用 confirmTransition 了
        // 而不是调用 transitionTo 还要走一遍 match 逻辑
        this.confirmTransition(route, () => {
          // 更新
          this.index = targetIndex
          this.updateRoute(route)
        })
      }
    
      ensureURL () {
        // noop
      }
    }
    

    小结

    整个的和 history 相关的代码到这里已经分析完毕了,虽然有三种模式,但是整体执行过程还是一样的,唯一差异的就是在处理location更新时的具体逻辑不同。

    欢迎拍砖哈

    参考资料

    Vue-router 从vue-router看前端路由的两种实现 前端路由简介以及vue-router实现原理


    起源地下载网 » 从零开始,用elementui躺坑Vue-Router原理分析

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元