关于vue.js:从零开始用elementui躺坑vue-Router原理分析

55次阅读

共计 8534 个字符,预计需要花费 22 分钟才能阅读完成。

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

路由起源 – 后端路由

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

http://www.vueRouter.com/login

大抵流程能够看成这样:

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

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

前端路由

随着前端利用的业务性能越来越简单、用户对于应用体验的要求越来越高,单页利用 (SPA)成为前端利用的支流模式。大型单页利用最显著特点之一就是采纳前端路由零碎,通过扭转 URL,在 不从新申请页面 的状况下,更新页面视图。

更新视图但不从新申请页面“ 是前端路由原理的外围之一,目前在浏览器环境中这一性能的实现次要有两种形式:

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

hash 模式

hash 示例:

http://www.vueRouter.com/login

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 实现原理

正文完
 0