【vue-rouer源码】系列文章
- 【vue-router源码】二、createWebHistory、createWebHashHistory、createMemoryHistory源码解析
目录
前言
【源码】定制小程序开发费用系列文章将带你从0开始了解vue-router定制小程序开发费用的具体实现。定制小程序开发费用该系列文章源码参考vue-router v4.0.15。
源码地址:
定制小程序开发费用阅读该文章的前提是你最好了解vue-router定制小程序开发费用的基本使用,定制小程序开发费用如果你没有使用过的话,定制小程序开发费用可通过学习下。
在vue-router 4.x中创建router时,需要使用createWebHistory、createWebHashHistory、createMemoryHistory定制小程序开发费用中的一个创建一个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+:\/\/[^\/]+/, '')操作,该操作是去除base的http(s)://xxx部分(如果base是https://example.com/floder/child,base最终会变成/floder/child);非浏览器环境下,base直接取/。在最后会将base的末尾/去除,然后返回base,这样做的目的是后续我们可以通过base + fullPath的形式建立一个href。
base标准化后,会声明一个historyNavigation和historyListeners变量:
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相对base的location。举几个例子(以下几个例子的base都经过标准化):如果window.location.pathname为/a/b/c,base为/a,那么通过createCurrentLocation得到的location为/b/c;如果是有hash的情况,window.location.hash为#/a/b/c,base为#/a,那么通过createCurrentLocation得到的location为/b/c;window.location.hash为#/a/b/c,base为#,那么通过createCurrentLocation得到的location为/a/b/c。
state:一个包含value属性的对象,value存储的是当前的history.statepush:向历史记录中添加一条记录。在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)、historyState、currentLocation、replace(后三个参数来自useHistoryStateNavigation的返回值)。
在useHistoryListeners中,会监听、。
useHistoryListeners同样返回一个对象,该对象包含三个属性:
pauseListeners:一个暂停监听的函数。listen:接收一个回调函数,并返回一个删除监听的函数。该回调函数会被加入listeners数组中,并向teardowns数组中添加卸载函数。destroy:销毁函数,清空listeners与teardowns,移除popstate、beforeunload监听。
现在我们知道了useHistoryStateNavigation、useHistoryListeners的实现后。现在我们回到createWebHistory中,创建完historyNavigation、historyListeners之后,紧跟着声明一个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
和createWebHistory、createWebHashHistory一样,createMemoryHistory同样返回一个RouterHistory类型的对象。与前面两个方法不同的是,createMemoryHistory维护一个队列queue和一个position,来保证历史记录存储的正确性。
总结
createWebHistory、createWebHashHistory中通过window.history.state来管理历史记录,;而createMemoryHistory是通过维护一个队列和一个位置来实现对路由记录的管理,这也保证了在SSR中vue-router能够正常进行。