定制小程序开发费用【vue-router源码】二、createWebHistory、createWebHashHistory、createMemoryHistory源码解析

【vue-rouer源码】系列文章

  1. 【vue-router源码】二、createWebHistory、createWebHashHistory、createMemoryHistory源码解析

目录


前言

【源码】定制小程序开发费用系列文章将带你从0开始了解vue-router定制小程序开发费用的具体实现。定制小程序开发费用该系列文章源码参考vue-router v4.0.15
源码地址:
定制小程序开发费用阅读该文章的前提是你最好了解vue-router定制小程序开发费用的基本使用,定制小程序开发费用如果你没有使用过的话,定制小程序开发费用可通过学习下。

vue-router 4.x中创建router时,需要使用createWebHistorycreateWebHashHistorycreateMemoryHistory定制小程序开发费用中的一个创建一个history,定制小程序开发费用这篇文章将就这三个函数进行解析。

使用

import { createWebHistory, createRouter } from 'vue-router'const routerHistory = createWebHistory()const router = createRouter({	history: routerHistory,	routes: [ ... ]})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

createWebHistory

createWebHistory源码所处位置:src/history/html5.ts
首先来看createWebHistory的参数,函数可以接受一个base字符串可选参数,该参数提供了一个基础路径。
createWebHistory中首先会调用normalizeBase函数对传入的base进行标准化。

base = normalizeBase(base)
  • 1

来看下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)}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

如果没有配置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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这两个变量是什么呢?接下来看下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:\` +          `history.replaceState(history.state, '', url)\` +          `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,  }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116

这个函数接收一个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}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

可以看到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,  }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90

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

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)}
  • 1
  • 2
  • 3
  • 4

最后创建一个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.stateObject.defineProperty(routerHistory, 'state', {  enumerable: true,  get: () => historyNavigation.state.value,})return routerHistory
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

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 "#":\"${base}" should be "${base.replace(        /#.*$/,        '#'      )}".`    )  }  return createWebHistory(base)}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

createMemoryHistory

createMemoryHistory会创建一个基于内存历史记录,主要用来处理。

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}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106

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

总结

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

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发