乐趣区

关于vue.js:vuerouter源码二createWebHistorycreateWebHashHistory等源码解析

前言

【vue-router 源码】系列文章将带你从 0 开始理解 vue-router 的具体实现。该系列文章源码参考 vue-router v4.0.15
源码地址:https://github.com/vuejs/router
浏览该文章的前提是你最好理解 vue-router 的根本应用,如果你没有应用过的话,可通过 vue-router 官网学习下。

vue-router 4.x 中创立 router 时,须要应用 createWebHistorycreateWebHashHistorycreateMemoryHistory 中的一个创立一个history,这篇文章将就这三个函数进行解析。

应用

import {createWebHistory, createRouter} from 'vue-router'

const routerHistory = createWebHistory()

const router = createRouter({
    history: routerHistory,
    routes: [...]
})

createWebHistory

createWebHistory源码所处地位:src/history/html5.ts
首先来看 createWebHistory 的参数,函数能够承受一个 base 字符串可选参数,该参数提供了一个根底门路。
createWebHistory 中首先会调用 normalizeBase 函数对传入的 base 进行标准化。

base = normalizeBase(base)

来看下 base 标准化的过程:

export function normalizeBase(base?: string): string {if (!base) {
      // 浏览其环境下尝试获取 base 标签的 href 属性
    if (isBrowser) {const baseEl = document.querySelector('base')
      base = (baseEl && baseEl.getAttribute('href')) || '/'
      // 去除 htttp(s)://xxx/,如 https://example.com/folder/ --> /folder/
      base = base.replace(/^\w+:\/\/[^\/]+/, '')
    } else {base = '/'}
  }
  // 确保 base 的前导 /
  if (base[0] !== '/' && base[0] !== '#') base = '/' + base

  return removeTrailingSlash(base)
}

如果没有配置 base 的话,在浏览器环境下会尝试获取 <base> 标签的 href 属性作为 base,如果没有<base> 标签或 <base> 标签的 href 属性没有值,base/,而后又对base 进行了 reaplce(/^\w+:\/\/[^\/]+/, '') 操作,该操作是去除 basehttp(s)://xxx局部(如果 basehttps://example.com/floder/childbase最终会变成 /floder/child);非浏览器环境下,base 间接取 /。在最初会将base 的开端 / 去除,而后返回 base,这样做的目标是后续咱们能够通过base + fullPath 的模式建设一个href

base标准化后,会申明一个 historyNavigationhistoryListeners变量:

const historyNavigation = useHistoryStateNavigation(base)
const historyListeners = useHistoryListeners(
  base,
  historyNavigation.state,
  historyNavigation.location,
  historyNavigation.replace
)

这两个变量是什么呢?接下来看下 useHistoryStateNavigation()useHistoryListeners() 的实现。

先看useHistoryStateNavigation:

function useHistoryStateNavigation(base: string) {
  // 获取 window.history、window.location
  const {history, location} = window

  const currentLocation: ValueContainer<HistoryLocation> = {value: createCurrentLocation(base, location),
  }
  const historyState: ValueContainer<StateEntry> = {value:}
  // 如果 history.state 是空的,构建一条新的历史记录
  if (!historyState.value) {
    changeLocation(
      currentLocation.value,
      {
        back: null,
        current: currentLocation.value,
        forward: null,
        position: history.length - 1,
        replaced: true,
        scroll: null,
      },
      true
    )
  }
  // 批改历史记录
  function changeLocation(
    to: HistoryLocation,
    state: StateEntry,
    replace: boolean
  ): void {const hashIndex = base.indexOf('#')
    // 获取 url,作为 history.replaceState/pushState 的参数
    // 如果 hashIndex > -1,url = `{location.host && document.querySelector('base') ? base : base 字符串 #及前面字符}${to}`
    // 否则 url = `${location.protocol}//${location.host}${base}${to}`
    const url =
      hashIndex > -1
        ? (location.host && document.querySelector('base')
            ? base
            : base.slice(hashIndex)) + to
        : createBaseLocation() + base + to
    try {
      // 利用 history.replaceState/pushState 批改历史记录
      history[replace ? 'replaceState' : 'pushState'](state, '', url)
      // historyState 更新为最新的历史记录
      historyState.value = state
    } catch (err) { // 如果历史记录批改过程中报错,则应用 location.reaplce/assign 导航到对应 url
      if (__DEV__) {warn('Error with push/replace State', err)
      } else {console.error(err)
      }
      location[replace ? 'replace' : 'assign'](url)
    }
  }

  function replace(to: HistoryLocation, data?: HistoryState) {
    const state: StateEntry = assign({},
      history.state,
      buildState(
        historyState.value.back,
        to,
        historyState.value.forward,
        true
      ),
      data,
      // 因为是 replace 操作,所以 position 不变
      {position: historyState.value.position}
    )

    changeLocation(to, state, true)
    // 批改以后历史为 to
    currentLocation.value = to
  }

  function push(to: HistoryLocation, data?: HistoryState) {
    const currentState = assign({},      historyState.value,
      history.state as Partial<StateEntry> | null,
      {
        forward: to,
        scroll: computeScrollPosition(),}
    )

    if (__DEV__ && !history.state) {
      warn(
        `history.state seems to have been manually replaced without preserving the necessary values. Make sure to preserve existing history state if you are manually calling history.replaceState:\n\n` +
          `history.replaceState(history.state, '', url)\n\n` +
          `You can find more information at https://next.router.vuejs.org/guide/migration/#usage-of-history-state.`
      )
    }

    // 第一次 changeLocation,应用 replace 刷新以后历史,目标是记录以后页面的滚动地位
    changeLocation(currentState.current, currentState, true)

    const state: StateEntry = assign({},
      buildState(currentLocation.value, to, null),
      // push 操作,历史记录的 position+1
      {position: currentState.position + 1},
      data
    )

    // 第二次跳转,跳转到须要跳转的地位
    changeLocation(to, state, false)
    currentLocation.value = to
  }

  return {
    location: currentLocation,
    state: historyState,

    push,
    replace,
  }
}

这个函数接管一个 base 参数,返回一个对象。这个对象中有四个属性:

  • location:一个蕴含 value 属性的对象,value值是 createCurrentLocation() 办法的返回值。那么这个 value 是什么呢?看下 createCurrentLocation 做了什么。

createCurrentLocation的作用是通过 window.location 创立一个规范化的 history location,办法接管两个参数:通过标准化的base 字符串和一个 window.location 对象

function createCurrentLocation(
  base: string,
  location: Location
): HistoryLocation {const { pathname, search, hash} = location
  // allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
  // 从 base 中获取 #的索引
  const hashPos = base.indexOf('#')
  // 如果 base 中蕴含#
  if (hashPos > -1) {
    // 如果 hash 蕴含 base 中的 #前面局部,slicePos 为 base 中# 及前面字符串的的长度,否则为 1
    let slicePos = hash.includes(base.slice(hashPos))
      ? base.slice(hashPos).length
      : 1
    // 从 location.hash 中获取 path,/#add, #add
    let pathFromHash = hash.slice(slicePos)
    // 在结尾加上 /,造成 /# 的格局
    if (pathFromHash[0] !== '/') pathFromHash = '/' + pathFromHash
    // stripBase(pathname, base):将 pathname 去除 base 局部
    return stripBase(pathFromHash, '')
  }
  // 如果 base 中不蕴含 #,把 pathname 中的 base 局部删除
  const path = stripBase(pathname, base)
  return path + search + hash
}

能够看到 createCurrentLocation 其实就是获取 window.location 绝对 baselocation。举几个例子(以下几个例子的 base 都通过标准化):如果 window.location.pathname/a/b/cbase/a,那么通过createCurrentLocation 失去的 location/b/c;如果是有 hash 的状况,window.location.hash#/a/b/cbase#/a,那么通过 createCurrentLocation 失去的 location/b/cwindow.location.hash#/a/b/cbase#,那么通过 createCurrentLocation 失去的 location/a/b/c

  • state:一个蕴含 value 属性的对象,value存储的是以后的history.state
  • push:向历史记录中增加一条记录。在 push 过程中你会发现调用了两次 changeLocation,在第一次调用changeLocation 时,目标是为了记录以后页面在的滚动地位,如果应用 history.back() 或浏览器回退 / 后退按钮回到这个页面,页面会滚动到对应地位,为了不再历史栈中保留新的记录,第一次记录应用的 reaplceState 替换以后历史记录。第二次调用 changeLocation 是会跳转到须要跳转的地位。
  • reaplce:替换以后历史记录。

接下来看下 useHistoryListeners 办法:

function useHistoryListeners(
  base: string,
  historyState: ValueContainer<StateEntry>,
  currentLocation: ValueContainer<HistoryLocation>,
  replace: RouterHistory['replace']
) {let listeners: NavigationCallback[] = []
  let teardowns: Array<() => void> = []
  let pauseState: HistoryLocation | null = null

  const popStateHandler: PopStateListener = ({state,}: {state: StateEntry | null}) => {const to = createCurrentLocation(base, location)
    const from: HistoryLocation = currentLocation.value
    const fromState: StateEntry = historyState.value
    let delta = 0

    if (state) {
      currentLocation.value = to
      historyState.value = state

      // 如果暂停监听了,则间接 return,同时 pauseState 赋为 null
      if (pauseState && pauseState === from) {
        pauseState = null
        return
      }
      // 计算挪动步数
      delta = fromState ? state.position - fromState.position : 0
    } else {replace(to)
    }
    // 执行监听函数列表
    listeners.forEach(listener => {
      listener(currentLocation.value, from, {
        delta,
        type: NavigationType.pop,
        direction: delta
          ? delta > 0
            ? NavigationDirection.forward
            : NavigationDirection.back
          : NavigationDirection.unknown,
      })
    })
  }

  function pauseListeners() {pauseState = currentLocation.value}

  function listen(callback: NavigationCallback) {listeners.push(callback)

    const teardown = () => {const index = listeners.indexOf(callback)
      if (index > -1) listeners.splice(index, 1)
    }

    teardowns.push(teardown)
    return teardown
  }

  function beforeUnloadListener() {const { history} = window
    if (!history.state) return
    // 当页面敞开时记录页面滚动地位
    history.replaceState(assign({}, history.state, {scroll: computeScrollPosition() }),
      ''
    )
  }

  function destroy() {for (const teardown of teardowns) teardown()
    teardowns = []
    window.removeEventListener('popstate', popStateHandler)
    window.removeEventListener('beforeunload', beforeUnloadListener)
  }

  window.addEventListener('popstate', popStateHandler)
  window.addEventListener('beforeunload', beforeUnloadListener)

  return {
    pauseListeners,
    listen,
    destroy,
  }
}

useHistoryListeners办法接管四个参数:base(标准化的 base)、historyStatecurrentLocationreplace(后三个参数来自useHistoryStateNavigation 的返回值)。
useHistoryListeners 中,会监听 popstate、beforeunload。

useHistoryListeners同样返回一个对象,该对象蕴含三个属性:

  • pauseListeners:一个暂停监听的函数。
  • listen:接管一个回调函数,并返回一个删除监听的函数。该回调函数会被退出 listeners 数组中,并向 teardowns 数组中增加卸载函数。
  • destroy:销毁函数,清空 listenersteardowns,移除 popstatebeforeunload 监听。

当初咱们晓得了 useHistoryStateNavigationuseHistoryListeners 的实现后。当初咱们回到 createWebHistory 中,创立完 historyNavigationhistoryListeners 之后,紧跟着申明一个 go 函数。该函数接管两个变量:delta历史记录挪动的步数,triggerListeners 是否触发监听。

function go(delta: number, triggerListeners = true) {if (!triggerListeners) historyListeners.pauseListeners()
  history.go(delta)
}

最初创立一个 routerHistory 对象,并将其返回。

const routerHistory: RouterHistory = assign(
  {
    location: '',
    base,
    go,
    createHref: createHref.bind(null, base),
  },
  historyNavigation,
  historyListeners
)

// 拦挡 routerHistory.location,使 routerHistory.location 返回以后路由地址
Object.defineProperty(routerHistory, 'location', {
  enumerable: true,
  get: () => historyNavigation.location.value,})

// 拦挡 routerHistory.state,使 routerHistory.state 返回以后的的 history.state
Object.defineProperty(routerHistory, 'state', {
  enumerable: true,
  get: () => historyNavigation.state.value,})

return routerHistory

createWebHashHistory

createWebHashHistory利用 createWebHashHistory 实现。

export function createWebHashHistory(base?: string): RouterHistory {
  // 对于应用文件协定关上的页面 location.host 是空字符串,这时的 base 为 ''// 也就是说在应用文件协定关上页面时,设置了 base 是不失效的,因为 base 始终是''
  base = location.host ? base || location.pathname + location.search : ''
  // 容许两头的 #: `/base/#/app`
  if (!base.includes('#')) base += '#'

  if (__DEV__ && !base.endsWith('#/') && !base.endsWith('#')) {
    warn(`A hash base must end with a "#":\n"${base}" should be "${base.replace(
        /#.*$/,
        '#'
      )}".`
    )
  }
  return createWebHistory(base)
}

createMemoryHistory

createMemoryHistory会创立一个基于内存历史记录,次要用来解决 SSR。

export function createMemoryHistory(base: string = ''): RouterHistory {
  // 用户存储监听函数的数组
  let listeners: NavigationCallback[] = []
  // 应用一个队列保护历史记录
  let queue: HistoryLocation[] = [START]
  // 以后历史记录在队列中的地位
  let position: number = 0
  // base 标准化
  base = normalizeBase(base)

  // 设置记录
  function setLocation(location: HistoryLocation) {
    position++
    // 队列长度等于 position 时,间接 push
    if (position === queue.length) {queue.push(location)
    } else {
      // 当历史记录在队列中的非开端地位时,删除 position 及之后的记录,而后再 push
      // 如果某一刻处在非结尾的历史记录时,这时要进行 push 或 reqlace 操作,此时 position 之后的记录就会生效
      queue.splice(position)
      queue.push(location)
    }
  }

  // 触发监听
  function triggerListeners(
    to: HistoryLocation,
    from: HistoryLocation,
    {direction, delta}: Pick<NavigationInformation, 'direction' | 'delta'>
  ): void {
    const info: NavigationInformation = {
      direction,
      delta,
      type: NavigationType.pop,
    }
    for (const callback of listeners) {callback(to, from, info)
    }
  }

  const routerHistory: RouterHistory = {
    location: START,
    state: {},
    base,
    createHref: createHref.bind(null, base),

    replace(to) {
      // 移除 queue 中索引为 position 的记录,并将 position--
      queue.splice(position--, 1)
      // 在 setLocation 会对 position 从新 ++ 操作,所以 position 会复原要之前的值
      setLocation(to)
    },

    push(to, data?: HistoryState) {setLocation(to)
    },

    listen(callback) {listeners.push(callback)
      return () => {const index = listeners.indexOf(callback)
        if (index > -1) listeners.splice(index, 1)
      }
    },
    destroy() {listeners = []
      queue = [START]
      position = 0
    },

    go(delta, shouldTrigger = true) {
      const from = this.location
      // go 的方向。delta < 0 为 back,相同为 forward
      const direction: NavigationDirection =
        delta < 0 ? NavigationDirection.back : NavigationDirection.forward
      // go 之后所处的 position:Math.min(position + delta, queue.length - 1)保障了 position<=queue.length - 1, 如果 position + delta 超出了数组最大索引,就取最大索引
      // Math.max(0, Math.min(position + delta, queue.length - 1))进一步保障了 position>=0,如果 position + delta < 0, 则取 0
      position = Math.max(0, Math.min(position + delta, queue.length - 1))
      // 依据 shouldTrigger 决定是否触发监听函数
      if (shouldTrigger) {
        triggerListeners(this.location, from, {
          direction,
          delta,
        })
      }
    },
  }

  Object.defineProperty(routerHistory, 'location', {
    enumerable: true,
    get: () => queue[position],
  })

  if (__TEST__) {routerHistory.changeURL = function (url: string) {
      const from = this.location
      queue.splice(position++ + 1, queue.length, url)
      triggerListeners(this.location, from, {
        direction: NavigationDirection.unknown,
        delta: 0,
      })
    }
  }

  return routerHistory
}

createWebHistorycreateWebHashHistory 一样,createMemoryHistory同样返回一个 RouterHistory 类型的对象。与后面两个办法不同的是,createMemoryHistory保护一个队列 queue 和一个position,来保障历史记录存储的正确性。

总结

createWebHistorycreateWebHashHistory中通过 window.history.state 来治理历史记录,;而 createMemoryHistory 是通过保护一个队列和一个地位来实现对路由记录的治理,这也保障了在 SSR 中 vue-router 可能失常进行。

退出移动版