1. 整体情况
该方法创建一个 HTML5 history,这是 SPA(Single Page Application, 单页应用程序)的常见 history。
1.1 方法源码
Copy // 文件 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
}
Copy // 文件 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
实例的各个属性的来源。如下:
RouterHistory 接口定义的属性和方法
来源
useHistoryStateNavigation()
1.3 逻辑分析
createWebHistory()
方法的底层是依赖 Web History API ,基于对 Web History API 的理解,关于该方法,需要重点关注以下逻辑:
对 popstate
事件的处理,以及如何和组件挂钩的
对 Router History 栈的维护,因为它必然不能直接用原生的 session history stack
与 Router Matcher 的配合,因为理论上是先匹配找到下标,然后再用原生的 history.go(num)
方法实现路由跳转的
2. 监听 popstate
事件
经过搜索发现,和 popstate
事件相关的逻辑都在 useHistoryListeners()
里。
2.1 useHistoryListeners()
简化后的代码,如下:
Copy // 文件 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
预期是在这里完成的前端视图切换。
Copy // 文件 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 的。
Copy // 文件 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()
useHistoryListeners()
暴露出去的公有方法 listen()
,最终是通过 createWebHistory()
方法暴露出去的,即 RouterHistory
实例的 listen()
方法。
Copy // 文件 history/html5.ts
export function createWebHistory (base ?: string ) : RouterHistory {
...
return routerHistory
}
搜索 routerHistory.listen
,结果如下:
Copy // 文件 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
以及如何切换前端视图的。
Copy // 形参
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
的处理,与之相关的代码如下:
Copy // 文件 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
事件时执行的监听回调中,切换前端视图的任务主要由这两个方法完成:
对于这两个方法的介绍,详见 createRouter()
。
3. 维护 Router History 栈
经过搜索发现,和 history.pushState()
, history.replaceState()
方法相关的逻辑都在 useHistoryStateNavigation()
里。
3.1 useHistoryStateNavigation()
简化后的代码,如下:
Copy // 文件 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
Copy // 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)
该方法根据 base
返回一个包含 location
, state
属性和 push
, replace
方法的对象。
Copy // src/history/html5.ts
function useHistoryStateNavigation (base : string ) {
...
return {
location : currentLocation ,
state : historyState ,
push ,
replace ,
}
}
与 location
属性相关的逻辑,如下:
Copy // 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
属性相关的逻辑,如下:
Copy // 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
方法的实现也用到了它。
Copy // 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 相关
Copy // 文件 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 > {}
Copy // 文件 history/html5.ts
interface StateEntry extends HistoryState {
back : HistoryLocation | null
current : HistoryLocation
forward : HistoryLocation | null
position : number
replaced : boolean
scroll : _ScrollPositionNormalized | null | false
}
Copy // 文件 history/common.ts
export type ValueContainer < T > = { value : T }
export type HistoryLocation = string
相关的 interface
定义如下:
Copy // 文件 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 = '' ,
}