vue-router v5.x 路由模式关于 createWebHistory、 createWebHashHistory的实现

一、createWebHistory 历史模式

1、做了什么?

创建基于 HTML5 History API 的路由历史管理对象(RouterHistory)。封装浏览器原生的 history 对象,实现了无哈希(#)的 URL 路由管理,支持前进 / 后退导航、历史记录修改和路由变化监听。

2、函数返回?

  • RouterHistory 对象
js 复制代码
/**
 * 创建基于 HTML5 History API 的路由历史对象(RouterHistory)
 * Creates an HTML5 history. Most common history for single page applications.
 *
 * @param base -
 */
export function createWebHistory(base?: string): RouterHistory {
  // 标准化基础路径
  base = normalizeBase(base)

  // 处理路由导航(push/replace 等),管理 state/location 响应式状态
  const historyNavigation = useHistoryStateNavigation(base)

  // 处理路由监听(popstate 事件),管理监听器的暂停/恢复
  const historyListeners = useHistoryListeners(
    base,
    historyNavigation.state,
    historyNavigation.location,
    historyNavigation.replace
  )

  /**
   * 跳转历史记录(go 方法)
   * 1、触发监听器时,路由系统会完整执行「URL 变化 → 路由匹配 → 视图更新」的链路;
   * 2、不触发时,仅修改浏览器 URL / 历史记录,路由系统无任何响应
   * 
   * 在触发浏览器前进 / 后退操作的同时,支持「是否触发路由监听器」的控制,
   * 实现「静默导航」(不触发监听回调)或「正常导航」(触发监听)
   * @param delta 跳转步数(可正可负)
   * @param triggerListeners 是否触发路由监听器(默认 true),false 时为「静默导航」
   */
  function go(delta: number, triggerListeners = true) {

    // triggerListeners 为 false 时,暂停所有路由监听器
    if (!triggerListeners) historyListeners.pauseListeners()

    // 都会执行,触发popstate事件,调用 popStateHandler
    history.go(delta)
  }

  // 组装 RouterHistory 对象
  const routerHistory: RouterHistory = assign(
    {
      // it's overridden right after
      location: '',
      base,
      go,
      createHref: createHref.bind(null, base),
    },

    historyNavigation, // push/replace/state/location 
    historyListeners // listen/pauseListeners/destroy 
  )

  // 重写 location 和 state 为响应式 getter
  Object.defineProperty(routerHistory, 'location', {
    enumerable: true,
    get: () => historyNavigation.location.value,
  })

  Object.defineProperty(routerHistory, 'state', {
    enumerable: true,
    get: () => historyNavigation.state.value,
  })

  return routerHistory
}

RouterHistory 接口

js 复制代码
interface RouterHistory {
  /**
   * Base path that is prepended to every url. This allows hosting an SPA at a
   * sub-folder of a domain like `example.com/sub-folder` by having a `base` of
   * `/sub-folder`
   */
  readonly base: string // 路由基础路径	用于 SPA 部署在子目录
  /**
   * Current History location
   */
  readonly location: HistoryLocation // 当前路由地址, 字符串
  /**
   * Current History state
   */
  readonly state: HistoryState // 当前历史记录状态	对应浏览器原生 history.state
  // readonly location: ValueContainer<HistoryLocationNormalized>

  /**
   * Navigates to a location. In the case of an HTML5 History implementation,
   * this will call `history.pushState` to effectively change the URL.
   * 新增一条历史记录,对应浏览器 history.pushState(无刷新跳转)
   * @param to - location to push
   * @param data - optional {@link HistoryState} to be associated with the
   * navigation entry
   */
  push(to: HistoryLocation, data?: HistoryState): void
  /**
   * Same as {@link RouterHistory.push} but performs a `history.replaceState`
   * instead of `history.pushState`
   * 替换当前历史记录,对应浏览器 history.replaceState(无刷新替换,不新增记录)
   * @param to - location to set
   * @param data - optional {@link HistoryState} to be associated with the
   * navigation entry
   */
  replace(to: HistoryLocation, data?: HistoryState): void

  /**
   * Traverses history in a given direction.
   *
   * @example
   * ```js
   * myHistory.go(-1) // equivalent to window.history.back()
   * myHistory.go(1) // equivalent to window.history.forward()
   * ```
   * 前进 / 后退历史记录,对应浏览器 history.go
   * @param delta - distance to travel. If delta is \< 0, it will go back,
   * if it's \> 0, it will go forward by that amount of entries.
   * @param triggerListeners - whether this should trigger listeners attached to
   * the history
   */
  go(delta: number, triggerListeners?: boolean): void

  /**
   * Attach a listener to the History implementation that is triggered when the
   * navigation is triggered from outside (like the Browser back and forward
   * buttons) or when passing `true` to {@link RouterHistory.back} and
   * {@link RouterHistory.forward}
   * 监听路由变化(如浏览器后退 / 前进、手动修改 URL)
   * @param callback - listener to attach
   * @returns a callback to remove the listener
   */
  listen(callback: NavigationCallback): () => void

  /**
   * Generates the corresponding href to be used in an anchor tag.
   * 生成可用于 <a> 标签的完整 URL
   * @param location - history location that should create an href
   */
  createHref(location: HistoryLocation): string

  /**
   * Clears any event listener attached by the history implementation.
   * 清理历史管理器的所有事件监听(如 popstate 事件)、重置状态
   */
  destroy(): void
}

useHistoryStateNavigation

1、做了什么? 初始化路由默认状态。利用 replaceState 更新地址栏。

2、函数返回?

  • location,当前路由位置(响应式)
  • state, 历史状态(响应式)
  • push函数,新增历史记录(pushState)
  • replace函数, 替换当前历史记录(replaceState)
js 复制代码
/**
 * 管理 History 模式路由导航的核心组合式函数,
 * 封装了浏览器原生 history.pushState/replaceState API,处理路由地址的变更、历史状态(state)的维护
 * @param base 
 * @returns 
 */
function useHistoryStateNavigation(base: string) {

  const { history, location } = window

  // private variables
  // 维护当前路由路径(响应式容器)
  const currentLocation: ValueContainer<HistoryLocation> = {
  
    value: createCurrentLocation(base, location),
  }
  // 维护当前历史状态(响应式容器)
  const historyState: ValueContainer<StateEntry> = { value: history.state }

  // build current history entry as this is a fresh navigation
  // 如果浏览器原生 history.state 为空(首次加载),初始化默认状态
  if (!historyState.value) {
    // 修改路由地址
    changeLocation(
      currentLocation.value,
      {
        back: null,
        current: currentLocation.value,
        forward: null,
        // the length is off by one, we need to decrease it
        position: history.length - 1,
        replaced: true,
        // don't add a scroll as the user may have an anchor, and we want
        // scrollBehavior to be triggered without a saved position
        scroll: null,
      },
      true
    )
  }

  return {
    location: currentLocation, // 当前路由位置(响应式)
    state: historyState, // 历史状态(响应式)
    push, // 新增历史记录(pushState)
    replace, // 替换当前历史记录(replaceState)
  }
}
push
js 复制代码
  /**
   * 新增历史记录
   * @param to 目标路由路径
   * @param data 新的历史状态数据
   */
  function push(to: HistoryLocation, data?: HistoryState) {
  
    // Add to current entry the information of where we are going
    // as well as saving the current position
    // 更新当前历史记录的 forward 指向(标记要跳转到的路径)
    const currentState = assign(
      {},
      // use current history state to gracefully handle a wrong call to
      // history.replaceState
      // https://github.com/vuejs/router/issues/366
      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://router.vuejs.org/guide/migration/#Usage-of-history-state`
      )
    }

    // 先替换当前历史记录(更新 forward 和 scroll)
    changeLocation(currentState.current, currentState, true)

    // 构建新的历史记录状态(包含 back、forward、current、replaced)
    const state: StateEntry = assign(
      {},
      buildState(currentLocation.value, to, null),
      { position: currentState.position + 1 },
      data
    )

    // 推送新记录(pushState)
    changeLocation(to, state, false)
    currentLocation.value = to
  }
replace
js 复制代码
  /**
   *  替换当前历史记录
   * @param to 目标路由路径
   * @param data 新的历史状态数据
   */
  function replace(to: HistoryLocation, data?: HistoryState) {
    // 1、构建新的 StateEntry:复用原有 back/forward,仅替换 curren
    const state: StateEntry = assign(
      {},
      history.state, // 浏览器原生历史状态 window.history.state
      // 状态数据包含: back 、forward、current、replaced
      buildState(
        historyState.value.back,
        // keep back and forward entries but override current position
        to, // 新的 current 路径
        historyState.value.forward,
        true // 标记为替换操作
      ),
      data,
       // 保持原位置(替换不新增记录)
      { position: historyState.value.position } 
    )

    // 2、更新浏览器历史记录
    changeLocation(to, state, true)
    currentLocation.value = to
  }
changeLocation
js 复制代码
  /**
   * 修改路由地址 
   * @param to 目标路由路径 string
   * @param state 新的历史状态
   * @param replace 是否使用 replaceState 替换当前状态
   */
  function changeLocation(
    to: HistoryLocation,
    state: StateEntry,
    replace: boolean
  ): void {
    /**
     * if a base tag is provided, and we are on a normal domain, we have to
     * respect the provided `base` attribute because pushState() will use it and
     * potentially erase anything before the `#` like at
     * https://github.com/vuejs/router/issues/685 where a base of
     * `/folder/#` but a base of `/` would erase the `/folder/` section. If
     * there is no host, the `<base>` tag makes no sense and if there isn't a
     * base tag we can just use everything after the `#`.
     */
    // 1、处理 base 
    const hashIndex = base.indexOf('#')
    const url =
      hashIndex > -1
        // 场景1:base 包含 #(Hash 模式)
        // 浏览器环境(有域名)+ 页面存在 <base> 标签 ------>	直接用完整 base + to
        ? (location.host && document.querySelector('base')
            ? base
            // 两种情况:
            // 1. file:// 协议(无 location.host)
            // 2. 无 <base> 标签	截取 base 中 # 及之后的部分 + to
            : base.slice(hashIndex)
          ) + to
        // 场景2:base 无 #(History 模式)
        : createBaseLocation() + base + to
    try {
      // BROWSER QUIRK
      // NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
      // 调用原生 pushState/replaceState
      history[replace ? 'replaceState' : 'pushState'](state, '', url)
      historyState.value = state // 更新当前历史状态
    } catch (err) {
      // 异常兜底:Safari 限制 30 秒内调用 100 次 pushState 会抛错
      if (__DEV__) {
        warn('Error with push/replace State', err)
      } else {
        console.error(err)
      }
      // Force the navigation, this also resets the call count
      // 降级使用 location.assign/replace 强制导航
      // 降级为 location.assign/replace(会触发页面刷新,但保证导航生效)
      location[replace ? 'replace' : 'assign'](url)
    }
  }
createCurrentLocation
js 复制代码
/**
 * Creates a normalized history location from a window.location object
 * 将浏览器 location 对象解析当前路由路径
 * @param base - The base path 路由基础路径
 * @param location - The window.location object // 当前浏览器 location 对象
 */
function createCurrentLocation(
  base: string,
  location: Location
): HistoryLocation {
  // 解析路径名、查询参数、哈希值
  const { pathname, search, hash } = location
  // allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
  const hashPos = base.indexOf('#')

  // 处理 hash
  if (hashPos > -1) {
    // 计算需要截取的 hash 长度
    let slicePos = hash.includes(base.slice(hashPos))
      // 如果当前 hash 包含 base 中 # 及之后的部分(如 base=#/app,hash=#/app/home → 截取 4 位)
      ? base.slice(hashPos).length
      : 1 // 否则仅截取 1 位(即 # 本身)

    // 截取 hash 中「基础路径之后」的部分
    let pathFromHash = hash.slice(slicePos)

    // prepend the starting slash to hash so the url starts with /#
    // 保证路径以 / 开头
    if (pathFromHash[0] !== '/') pathFromHash = '/' + pathFromHash

    return stripBase(pathFromHash, '')
  }
  // 处理 History 模式(base 无 #)
  // 从 pathname 中移除 base 前缀(如 /admin/home → /home)
  const path = stripBase(pathname, base)
  return path + search + hash // 拼接路径 + 查询参数 + 哈希值,返回完整标准化路径
}

useHistoryListeners

1、做了什么?

  • 监听事件 popstate,负责同步路由状态、计算导航方向,并通知所有路由监听器。
  • 监听事件 pagehide,将当前页面的滚动位置保存到浏览器历史记录的状态中。
  • 监听事件 visibilitychange,将当前页面的滚动位置保存到浏览器历史记录的状态中。

2、函数返回?

  • pauseListeners, 暂停监听导航
  • listen, 注册导航回调
  • destroy, 销毁所有监听器
js 复制代码
/**
 * 路由历史监听的核心组合式函数,
 * 负责一站式管理「浏览器原生导航事件监听、自定义导航回调注册 / 销毁、滚动位置保存、监听器暂停 / 恢复」等能力
 * @param base - 路由基础路径
 * @param historyState - 路由历史状态容器
 * @param currentLocation - 当前路由位置容器
 * @param replace - 替换当前路由的函数
 * @returns 
 */
function useHistoryListeners(
  base: string,
  historyState: ValueContainer<StateEntry>,
  currentLocation: ValueContainer<HistoryLocation>,
  replace: RouterHistory['replace']
) {
  let listeners: NavigationCallback[] = [] // 存储所有导航回调函数
  let teardowns: Array<() => void> = [] // 存储销毁监听器的函数
  // TODO: should it be a stack? a Dict. Check if the popstate listener
  // can trigger twice
  let pauseState: HistoryLocation | null = null  // 暂停监听时保存的当前位置

  const popStateHandler: PopStateListener = ({
    state, // 从 popstate 事件中获取的状态对象
  }: {
    state: StateEntry | null
  }) => {
    // 目标路由位置 字符串
    const to = createCurrentLocation(base, location)
    const from: HistoryLocation = currentLocation.value // 当前路由位置 字符串
    const fromState: StateEntry = historyState.value
    let delta = 0  // 初始化导航步数

    // 处理 popstate 事件
    if (state) {
      currentLocation.value = to // 更新目标路由为当前路由
      historyState.value = state 

      // ignore the popstate and reset the pauseState
      // 忽略「暂停状态」的导航(内部防抖:避免重复触发)
      if (pauseState && pauseState === from) {
        pauseState = null
        return
      }
      // 计算导航步数 delta:目标状态位置 - 原状态位置
      delta = fromState ? state.position - fromState.position : 0
    } else {
      // 状态无效(如手动修改 URL 导致 history.state 丢失)
      // 调用 replace 重置 URL 并恢复状态(避免路由状态异常)
      replace(to)
    }

    // Here we could also revert the navigation by calling history.go(-delta)
    // this listener will have to be adapted to not trigger again and to wait for the url
    // to be updated before triggering the listeners. Some kind of validation function would also
    // need to be passed to the listeners so the navigation can be accepted
    // call all listeners
    listeners.forEach(listener => {
      // 执行导航回调函数,传递当前路由、上一个路由、导航信息
      listener(currentLocation.value, from, {
        delta,
        type: NavigationType.pop, // 导航类型:popstate 事件触发
        direction: delta
          ? delta > 0
            ? NavigationDirection.forward // 前向导航(步数 > 0)
            : NavigationDirection.back // 后向导航(步数 < 0)
          : NavigationDirection.unknown, // 未知方向(步数 = 0)
      })
    })
  }

  // 临时暂停导航监听
  function pauseListeners() {
    pauseState = currentLocation.value // 标记当前路由位置为「暂停状态」,
  }

  function listen(callback: NavigationCallback) {
    // set up the listener and prepare teardown callbacks
    listeners.push(callback) // 添加回调到队列

    // 生成「移除该回调」的函数
    const teardown = () => {
      // 从队列中移除该回调
      const index = listeners.indexOf(callback)
      if (index > -1) listeners.splice(index, 1)
    }

    // 存储销毁函数,用于批量清理
    teardowns.push(teardown)
    return teardown
  }

  function beforeUnloadListener() {
    // 仅当页面「隐藏」时执行(避免无意义的状态更新)
    if (document.visibilityState === 'hidden') {
      const { history } = window
      if (!history.state) return
      // 更新当前历史记录的状态:合并原有状态与当前滚动位置
      history.replaceState(
        assign({}, history.state, { scroll: computeScrollPosition() }),
        ''
      )
    }
  }

  // 销毁所有监听器
  function destroy() {
    for (const teardown of teardowns) teardown()
    teardowns = []
    // 移除 popstate 事件监听器
    window.removeEventListener('popstate', popStateHandler)
    // 移除页面隐藏事件监听器
    window.removeEventListener('pagehide', beforeUnloadListener)
    // 移除文档可见性变化事件监听器(visibilitychange)
    document.removeEventListener('visibilitychange', beforeUnloadListener)
  }

  // set up the listeners and prepare teardown callbacks
  window.addEventListener('popstate', popStateHandler)

  // 兼容性考量:iOS Safari 不触发 beforeunload,因此用 pagehide + visibilitychange 兜底
  // https://developer.chrome.com/blog/page-lifecycle-api/
  // note: iOS safari does not fire beforeunload, so we
  // use pagehide and visibilitychange instead
  // 兼容 pagehide(移动端 /iOS Safari)
  window.addEventListener('pagehide', beforeUnloadListener)
  // 兼容 visibilitychange(桌面端)
  document.addEventListener('visibilitychange', beforeUnloadListener)

  return {
    pauseListeners, // 暂停监听导航
    listen, // 注册导航回调
    destroy, // 销毁所有监听器
  }
}

二、createWebHashHistory 哈希模式

js 复制代码
/**
 * 创建基于 Hash 模式的路由历史对象
 * @param base - 选的基础路径(比如 /admin),用于给 Hash 路由添加前缀,默认值为 `location.pathname + location.search`
 * @returns 路由历史对象
 */
function createWebHashHistory(base?: string): RouterHistory {
  // Make sure this implementation is fine in terms of encoding, specially for IE11
  // for `file://`, directly use the pathname and ignore the base
  // location.pathname contains an initial `/` even at the root: `https://example.com`
  // 1. 区分环境
  // 如果是 file:// 协议(本地文件),忽略 base,直接用 pathname + search;
  // 否则用传入的 base 或空字符串
  base = location.host ? base || location.pathname + location.search : ''
  // allow the user to provide a `#` in the middle: `/base/#/app`
  // 2、确保 base 中包含 #:如果用户没传 #,自动追加到 base 末尾
  if (!base.includes('#')) base += '#'

  // 3. 开发环境校验:base 必须以 # 或 #/ 结尾,否则给出警告
  if (__DEV__ && !base.endsWith('#/') && !base.endsWith('#')) {
    warn(
      `A hash base must end with a "#":\n"${base}" should be "${base.replace(/#.*$/, '#')}".`
    )
  }
  // 复用 createWebHistory 实现,只是 base 不同
  return createWebHistory(base)
}

三、应用

js 复制代码
import { createRouter, createWebHistory } from 'vue-router'

const history = createWebHistory(import.meta.env.VITE_BASE_URL)

// const history = createWebHashHistory();

console.log('history', history)

const router = createRouter({
  history,
  routes: []
})

四、历史模式 与 hash模式

1、历史模式

SPA 应用的核心是「所有路由都由前端 index.html 解析」,但 History 模式下:

  • 当用户首次访问 http://xxx.com/home → 浏览器发送 GET /home 请求 → 服务器默认会查找 /home 目录 / 文件,若不存在则返回 404。
  • 当用户刷新页面 (已进入 /home 路由后刷新)→ 浏览器再次发送 GET /home 请求 → 同样会 404。

服务器需要配置:无论收到任何路径的 GET 请求(如 /home/user/detail),只要找不到对应文件,就返回 index.html

2、hash模式

txt 复制代码
http://localhost:5173/#/home

Hash 模式的 URL 中,# 后的路由路径不会被发送到服务器:

  • 无论用户访问 /#/home/#/user,服务器收到的请求永远是 GET /
  • 服务器只需返回 index.html(静态服务器默认行为),前端 Vue Router 会解析 #/home 部分,匹配对应的路由组件。

五、最后

官网:router.vuejs.org/

相关推荐
本末倒置1831 小时前
Bun 内置模块全解析:告别第三方依赖,提升开发效率
前端·javascript·node.js
踩着两条虫1 小时前
AI 驱动的 Vue3 应用开发平台 深入探究(二):核心概念之DSL模式与数据模型
前端·vue.js·ai编程
牛奶1 小时前
200 OK不是"成功"?HTTP状态码潜规则
前端·http·浏览器
Hilaku2 小时前
OpenClaw 很爆火,但没人敢聊它的权限安全🤷‍♂️
前端·javascript·程序员
ConardLi3 小时前
OpenClaw 完全指南:这可能是全网最新最全的系统化教程了!
前端·人工智能·后端
丁哥3 小时前
99.9%纯AI 做了一个ICO图标生成器(免费 全尺寸 不限文件大小)2ICO.CN欢迎品鉴
前端
兆子龙4 小时前
React Native 完全入门:从原理到实战
前端·javascript
哇哇哇哇4 小时前
vue3 watch解析
前端
SuperEugene4 小时前
Vite 实战教程:alias/env/proxy 配置 + 打包优化避坑|Vue 工程化必备
前端·javascript·vue.js