前言
【vue-router 源码】系列文章将带你从 0 开始理解 vue-router
的具体实现。该系列文章源码参考 vue-router v4.0.15
。
源码地址:https://github.com/vuejs/router
浏览该文章的前提是你最好理解 vue-router
的根本应用,如果你没有应用过的话,可通过 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: [...]
})
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+:\/\/[^\/]+/, '')
操作,该操作是去除 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
)
这两个变量是什么呢?接下来看下 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
绝对 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.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
)、historyState
、currentLocation
、replace
(后三个参数来自useHistoryStateNavigation
的返回值)。
在 useHistoryListeners
中,会监听 popstate、beforeunload。
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)
}
最初创立一个 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
}
和 createWebHistory
、createWebHashHistory
一样,createMemoryHistory
同样返回一个 RouterHistory
类型的对象。与后面两个办法不同的是,createMemoryHistory
保护一个队列 queue
和一个position
,来保障历史记录存储的正确性。
总结
createWebHistory
、createWebHashHistory
中通过 window.history.state
来治理历史记录,;而 createMemoryHistory
是通过保护一个队列和一个地位来实现对路由记录的治理,这也保障了在 SSR 中 vue-router
可能失常进行。