4. createWebHistory()
1. 整体情况
该方法创建一个 HTML5 history,这是 SPA(Single Page Application, 单页应用程序)的常见 history。
1.1 方法源码
// 文件 history/html5.ts
export function createWebHistory(base?: string): RouterHistory {
// 规范化 base,兼容 <base> 标签,删除值尾部的 /
base = normalizeBase(base)
// 返回一个包含 location, state 属性和 push, replace 方法的对象
const historyNavigation = useHistoryStateNavigation(base)
// 返回一个包含 pauseListeners, listen, destroy 方法的对象
const historyListeners = useHistoryListeners(
base,
historyNavigation.state,
historyNavigation.location,
historyNavigation.replace
)
function go(delta: number, triggerListeners = true) {
if (!triggerListeners) historyListeners.pauseListeners()
history.go(delta) // 原生方法 window.history.go()
}
// 赋值
const routerHistory: RouterHistory = assign(
{
location: '',
base,
go,
createHref: createHref.bind(null, base), // 移除 hash 前的所有字符
},
historyNavigation,
historyListeners
)
// 两个 getter
Object.defineProperty(routerHistory, 'location', {
enumerable: true,
get: () => historyNavigation.location.value,
})
Object.defineProperty(routerHistory, 'state', {
enumerable: true,
get: () => historyNavigation.state.value,
})
return routerHistory
}
// 文件 history/common.ts
export interface RouterHistory {
readonly base: string // 附加到每个 url 前的 base path
readonly location: HistoryLocation // 当前路由
readonly state: HistoryState // 当前路由的状态
push(to: HistoryLocation, data?: HistoryState): void
replace(to: HistoryLocation, data?: HistoryState): void
go(delta: number, triggerListeners?: boolean): void
listen(callback: NavigationCallback): () => void // 路由事件监听器,当导航从 outside 触发时(比如浏览器的前进和后退按钮)
destroy(): void // 清除路由上所有事件的 listeners
createHref(location: HistoryLocation): string // 生成在 anchor tag 里使用的 href
}
1.2 属性来源
根据 createWebHistory()
方法的代码,我们可以知道其返回的 RouterHistory
实例的各个属性的来源。如下:
base
规范化go
createHref
createWebHistory()
location
state
push()
replace()
useHistoryStateNavigation()
listen
destroy
pauseListeners
useHistoryListeners()
1.3 逻辑分析
对
popstate
事件的处理,以及如何和组件挂钩的对 Router History 栈的维护,因为它必然不能直接用原生的 session history stack
与 Router Matcher 的配合,因为理论上是先匹配找到下标,然后再用原生的
history.go(num)
方法实现路由跳转的
2. 监听 popstate
事件
popstate
事件经过搜索发现,和 popstate
事件相关的逻辑都在 useHistoryListeners()
里。
2.1 useHistoryListeners()
useHistoryListeners()
简化后的代码,如下:
// 文件 history/html5.ts
function useHistoryListeners(
base: string,
historyState: ValueContainer<StateEntry>,
currentLocation: ValueContainer<HistoryLocation>,
replace: RouterHistory['replace']
) {
...
window.addEventListener('popstate', popStateHandler)
window.addEventListener('beforeunload', beforeUnloadListener)
return {
pauseListeners,
listen,
destroy,
}
}
2.2 私有方法 popStateHandler
popStateHandler
预期是在这里完成的前端视图切换。
// 文件 history/html5.ts
type PopStateListener = (this: Window, ev: PopStateEvent) => any
function useHistoryListeners(
base,
historyState,
currentLocation,
replace
) {
let listeners: NavigationCallback[] = []
let pauseState: HistoryLocation | null = null
const popStateHandler: PopStateListener = ({
state,
}: {
state: StateEntry | null
}) => {
// 1. to 是根据参数 base 和 window.location 现算的
// 从 window.location 对象创建一个规范化的 history location
const to = createCurrentLocation(base, location)
// 2. from, fromState 直接来源于参数
const from: HistoryLocation = currentLocation.value
const fromState: StateEntry = historyState.value
let delta = 0
// 3. 判断 popStateEvent.state
// 若 state 有值则根据 state 和 fromState 的 position 属性计算出 delta
// 否则就调用 replace(to),来自于参数 RouterHistory['replace']
if (state) {
currentLocation.value = to
historyState.value = state
// 4. 若 pauseState === from,则忽略 popstate,直接返回
if (pauseState && pauseState === from) {
pauseState = null
return
}
delta = fromState ? state.position - fromState.position : 0
} else {
replace(to)
}
// 5. 依次执行 listeners 里的回调函数
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
})
})
}
}
在这个事件监听器函数中,和切换前端视图相关的逻辑就一个,那就是:执行了 listeners
里的回调函数。
其中,listeners
里的回调函数是通过公有方法 listen()
动态 push 的。
// 文件 history/html5.ts
function useHistoryListeners(...) {
let listeners: NavigationCallback[] = []
let teardowns: Array<() => void> = [] // teardown, 拆除
function listen(callback: NavigationCallback) {
// 设置 listener
listeners.push(callback)
// teardown 回调
const teardown = () => {
const index = listeners.indexOf(callback)
if (index > -1) listeners.splice(index, 1) // 原地操作,摘除那一个
}
teardowns.push(teardown)
return teardown
}
return {
listen,
}
}
接下来我们重点看下 listeners
里的回调函数是什么样子的,以及它们是什么时候存进来的。
2.3 调用方法 listen()
listen()
useHistoryListeners()
暴露出去的公有方法 listen()
,最终是通过 createWebHistory()
方法暴露出去的,即 RouterHistory
实例的 listen()
方法。
// 文件 history/html5.ts
export function createWebHistory(base?: string): RouterHistory {
...
return routerHistory
}
搜索 routerHistory.listen
,结果如下:
// 文件 router.ts
export function createRouter(options: RouterOptions): Router {
...
// 将 listener 附加到 history,以触发 navigations
function setupListeners() {
...
removeHistoryListener = routerHistory.listen((to, _from, info) => {
...
navigate(toLocation, from)
.catch((error: NavigationFailure | NavigationRedirectError) => { })
.then((failure: NavigationFailure | void) => { })
.catch(noop)
})
}
...
return router
}
也就是说在 createRouter()
时就 push 好了 listeners。接下来,看看它是如何使用参数 delta
以及如何切换前端视图的。
// 形参
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
})
// 实参
routerHistory.listen((to, _from, info) => {})
在 routerHistory.listen()
中,和 info.delta
相关的逻辑大部分是在 navigate()
的异常处理里,故暂且忽略。我们重点关注对参数 to
的处理,与之相关的代码如下:
// 文件 router.ts
routerHistory.listen((to, _from, info) => {
...
// 不会是重新定向路由,因为它在 history 中
const toLocation = resolve(to) as RouteLocationNormalized
// 1. 如果需要重定向,比如动态路由,比如手动的 hash history(手动更改 url 或调用 history.hash = '#/somewhere'),
const shouldRedirect = handleRedirectRecord(toLocation)
if (shouldRedirect) {
pushWithRedirect(
assign(shouldRedirect, { replace: true }),
toLocation
).catch(noop)
return // 直接返回
}
// 2. 普通导航
navigate(toLocation, from)
.catch((error: NavigationFailure | NavigationRedirectError) => { })
.then((failure: NavigationFailure | void) => { })
.catch(noop)
})
至此,在响应 popstate
事件时执行的监听回调中,切换前端视图的任务主要由这两个方法完成:
pushWithRedirect()
重定向导航navigate()
普通导航
3. 维护 Router History 栈
经过搜索发现,和 history.pushState()
, history.replaceState()
方法相关的逻辑都在 useHistoryStateNavigation()
里。
3.1 useHistoryStateNavigation()
useHistoryStateNavigation()
简化后的代码,如下:
// 文件 history/html5.ts
function useHistoryStateNavigation(base: string) {
const { history, location } = window
const currentLocation: ValueContainer<HistoryLocation> = {
value: createCurrentLocation(base, location),
}
const historyState: ValueContainer<StateEntry> = { value: history.state }
if (!historyState.value) {
changeLocation(...)
}
// 内部方法
function changeLocation(to, state, isReplace) { }
function replace(to, data) {
// 内部调了 changeLocation()
}
function push(to, data) {
// 内部调了 changeLocation()
}
return {
location: currentLocation,
state: historyState,
push,
replace,
}
}
4. 路由跳转
经过搜索发现,和 history.back()
, history.forward()
, history.go()
方法相关的逻辑入口都在 RouterHistory
接口的定义里。
公共逻辑
处理 base 和 href
// src/history/common.ts
// 规范化 base,删除值尾部的 /
export function normalizeBase(base?: string): string {
if (!base) {
if (isBrowser) {
// 若 base 为空,则会考虑网页里的 <base> 标签
const baseEl = document.querySelector('base')
base = (baseEl && baseEl.getAttribute('href')) || '/'
base = base.replace(/^\w+:\/\/[^\/]+/, '') // 去除完整 URL origin
} else {
base = '/'
}
}
// 去除前导 /,比如形如 file://
if (base[0] !== '/' && base[0] !== '#') base = '/' + base
// 去除尾部 / 以便所有方法都可以通过 `base + fullPath` 来构建 href
return removeTrailingSlash(base)
}
// 移除 hash 前的所有字符
const BEFORE_HASH_RE = /^[^#]+#/
export function createHref(base: string, location: HistoryLocation): string {
return base.replace(BEFORE_HASH_RE, '#') + location
}
// src/location.ts
const TRAILING_SLASH_RE = /\/$/
export const removeTrailingSlash = (path: string) => {
path.replace(TRAILING_SLASH_RE, '')
}
useHistoryStateNavigation(base)
useHistoryStateNavigation(base)
该方法根据 base
返回一个包含 location
, state
属性和 push
, replace
方法的对象。
// src/history/html5.ts
function useHistoryStateNavigation(base: string) {
...
return {
location: currentLocation,
state: historyState,
push,
replace,
}
}
与 location
属性相关的逻辑,如下:
// src/history/html5.ts
function useHistoryStateNavigation(base: string) {
const { location } = window
// 从 window.location 对象创建一个规范化的 history location,
const currentLocation: ValueContainer<HistoryLocation> = {
value: createCurrentLocation(base, location),
}
return {
location: currentLocation,
}
}
// 从 window.location 对象创建一个规范化的 history location
function createCurrentLocation(
base: string,
location: Location
): HistoryLocation {
const { pathname, search, hash } = location
// 如果是 hash,支持形如 #, /#, #/, #!, #!/, /#!/, /folder#end
const hashPos = base.indexOf('#')
if (hashPos > -1) {
let slicePos = hash.includes(base.slice(hashPos))
? base.slice(hashPos).length
: 1
let pathFromHash = hash.slice(slicePos)
// prepend the starting slash to hash so the url starts with /#
if (pathFromHash[0] !== '/') pathFromHash = '/' + pathFromHash
return stripBase(pathFromHash, '')
}
// 去掉 location.pathname 开头的 base,不区分大小写
const path = stripBase(pathname, base)
return path + search + hash
}
// src/location.ts
// 去掉 location.pathname 开头的 base,不区分大小写
export function stripBase(pathname: string, base: string): string {
if (!base || !pathname.toLowerCase().startsWith(base.toLowerCase()))
return pathname
return pathname.slice(base.length) || '/'
}
与 state
属性相关的逻辑,如下:
// src/history/html5.ts
function useHistoryStateNavigation(base: string) {
const { history } = window
const historyState: ValueContainer<StateEntry> = { value: history.state }
return {
state: historyState,
}
}
其中,当 state
的值为 false 时,会调用 changeLocation
方法。该方法比较重要,因为 push
和 replace
方法的实现也用到了它。
// src/history/html5.ts
function useHistoryStateNavigation(base: string) {
// 构建当前 history 条目因为这是一个全新的导航
if (!historyState.value) {
changeLocation(
currentLocation.value,
{
back: null,
current: currentLocation.value,
forward: null,
position: history.length - 1, // length-1
replaced: true,
scroll: null,
},
true
)
}
function changeLocation(
to: HistoryLocation,
state: StateEntry,
replace: boolean
): void {
let createBaseLocation = () => location.protocol + '//' + location.host
const hashIndex = base.indexOf('#')
const url =
hashIndex > -1
? (location.host && document.querySelector('base')
? base
: base.slice(hashIndex)) + to
: createBaseLocation() + base + to
try {
// BROWSER QUIRK
// Safari 会抛出 SecurityError 当在30s内调用该函数100次时
history[replace ? 'replaceState' : 'pushState'](state, '', url)
historyState.value = state
} catch (err) {
if (__DEV__) {
warn('Error with push/replace State', err)
} else {
console.error(err)
}
// 强制导航,也会重置 call count
location[replace ? 'replace' : 'assign'](url)
}
}
}
附录
相关的 type
和 interface
定义
state 相关
// 文件 history/common.ts
// HTML5 history state 中允许的变量的 value,注意没有 symbol 和 function
export type HistoryStateValue =
| string
| number
| boolean
| null
| undefined
| HistoryState
| HistoryStateArray
// state 的 key 不能是 symbol
export interface HistoryState {
[x: number]: HistoryStateValue
[x: string]: HistoryStateValue
}
export interface HistoryStateArray extends Array<HistoryStateValue> {}
// 文件 history/html5.ts
interface StateEntry extends HistoryState {
back: HistoryLocation | null
current: HistoryLocation
forward: HistoryLocation | null
position: number
replaced: boolean
scroll: _ScrollPositionNormalized | null | false
}
// 文件 history/common.ts
export type ValueContainer<T> = { value: T }
export type HistoryLocation = string
相关的 interface
定义如下:
// 文件 history/common.ts
export interface NavigationCallback {
(
to: HistoryLocation,
from: HistoryLocation,
information: NavigationInformation
): void
}
export interface NavigationInformation {
type: NavigationType
direction: NavigationDirection
delta: number
}
export enum NavigationType {
pop = 'pop',
push = 'push',
}
export enum NavigationDirection {
back = 'back',
forward = 'forward',
unknown = '',
}
Last updated