# 4. createWebHistory()

## 1. 整体情况

该方法创建一个 HTML5 history，这是 SPA（Single Page Application, 单页应用程序）的常见 history。

### 1.1 方法源码

```typescript
// 文件 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
}
```

![](https://2598460105-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FGNkDWo1TzHEOBRUxCRfy%2Fuploads%2F6ytkVWYlFbfLF97mURGy%2Fimage.png?alt=media\&token=bf1175a7-37f8-4666-b9f2-981d80664f59)

```typescript
// 文件 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 接口定义的属性和方法                                                                                                         | 来源                            |
| -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| <ol><li><code>base</code> 规范化 </li><li><code>go</code> </li><li><code>createHref</code></li></ol>                                | `createWebHistory()`          |
| <ol><li><code>location</code> </li><li><code>state</code> </li><li><code>push()</code> </li><li><code>replace()</code></li></ol> | `useHistoryStateNavigation()` |
| <ol><li><code>listen</code> </li><li><code>destroy</code></li><li><code>pauseListeners</code></li></ol>                          | `useHistoryListeners()`       |

### 1.3 逻辑分析

`createWebHistory()` 方法的底层是依赖 [Web History API](https://anjia1.gitbook.io/web/browser/history)，基于对 Web History API 的理解，关于该方法，需要重点关注以下逻辑：

1. 对 `popstate` 事件的处理，以及如何和组件挂钩的
2. 对 Router History 栈的维护，因为它必然不能直接用原生的 session history stack
3. 与 Router Matcher 的配合，因为理论上是先匹配找到下标，然后再用原生的 `history.go(num)` 方法实现路由跳转的

## 2. 监听 `popstate` 事件

经过搜索发现，和 `popstate` 事件相关的逻辑都在 `useHistoryListeners()` 里。

### 2.1 `useHistoryListeners()`

简化后的代码，如下：

```typescript
// 文件 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`

预期是在这里完成的前端视图切换。

```typescript
// 文件 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 的。

```typescript
// 文件 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()` 方法。

```typescript
// 文件 history/html5.ts
export function createWebHistory(base?: string): RouterHistory {
  ...
  return routerHistory
}
```

搜索 `routerHistory.listen`，结果如下：

```typescript
// 文件 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` 以及如何切换前端视图的。

```typescript
// 形参
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` 的处理，与之相关的代码如下：

```typescript
// 文件 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` 事件时执行的监听回调中，切换前端视图的任务主要由这两个方法完成：

1. `pushWithRedirect()` 重定向导航
2. `navigate()` 普通导航

对于这两个方法的介绍，详见 [`createRouter()`](https://anjia1.gitbook.io/web/framework/vue-router/createrouter#3.-nei-bu-fang-fa)。

## 3. 维护 Router History 栈

经过搜索发现，和 `history.pushState()`, `history.replaceState()` 方法相关的逻辑都在 `useHistoryStateNavigation()` 里。

### 3.1 `useHistoryStateNavigation()`

简化后的代码，如下：

```typescript
// 文件 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

```javascript
// 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` 方法的对象。

```typescript
// src/history/html5.ts
function useHistoryStateNavigation(base: string) {  
  ...
  return {
    location: currentLocation,
    state: historyState,

    push,
    replace,
  }
}
```

与 `location` 属性相关的逻辑，如下：

```typescript
// 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` 属性相关的逻辑，如下：

```typescript
// 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` 方法的实现也用到了它。

```javascript
// 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 相关

```typescript
// 文件 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> {}
```

```typescript
// 文件 history/html5.ts
interface StateEntry extends HistoryState {
  back: HistoryLocation | null
  current: HistoryLocation
  forward: HistoryLocation | null
  position: number
  replaced: boolean
  scroll: _ScrollPositionNormalized | null | false
}
```

```typescript
// 文件 history/common.ts
export type ValueContainer<T> = { value: T }

export type HistoryLocation = string
```

相关的 `interface` 定义如下：

```typescript
// 文件 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 = '',
}
```
