Vue 3源码深度解析之:`Vue Router`的`History`模式:`createWebHistory`的实现原理。

咳咳,各位靓仔靓女,欢迎来到今天的Vue源码深度游乐园。今天,我们来聊聊Vue Router里那个看似人畜无害,实则暗藏玄机的createWebHistory。 准备好你们的咖啡和瓜子,咱们发车咯!

一、 History模式:前端路由的浪漫邂逅

在没有前端路由的远古时代(大概也就十年前),我们每次切换页面都要向服务器发起请求,浏览器刷新,用户体验差到爆。后来,前端大佬们发现,我们可以通过一些骚操作,只更新页面的局部内容,而不用刷新整个页面。这就是SPA(Single Page Application)诞生的背景。

History模式,就是SPA实现路由的一种方式。它的核心思想是:利用浏览器提供的History APIpushStatereplaceState),修改浏览器的URL,但并不触发实际的页面跳转。这样,我们就可以在不刷新页面的情况下,改变URL,然后根据URL来渲染不同的内容。

举个栗子:

<!DOCTYPE html>
<html>
<head>
<title>History 模式演示</title>
</head>
<body>
  <h1>欢迎来到首页</h1>
  <a href="/about">去关于页面</a>

  <script>
    const link = document.querySelector('a');
    link.addEventListener('click', (event) => {
      event.preventDefault(); // 阻止默认的链接跳转

      const url = link.getAttribute('href');
      history.pushState({ page: 'about' }, 'About', url); // 修改URL

      // 根据 URL 加载不同的内容 (这里只是个示例)
      if (url === '/about') {
        document.body.innerHTML = '<h1>关于我们</h1><a href="/">返回首页</a>';
      }
    });
  </script>
</body>
</html>

在这个简单的例子中,点击“去关于页面”的链接,浏览器URL会变成/about,但页面不会刷新。我们通过pushState修改了URL,并手动更新了页面内容。

二、 Vue Router的createWebHistory:幕后英雄登场

createWebHistory是Vue Router提供的一个函数,用于创建History模式的路由实例。它的作用是封装了浏览器的History API,并提供了一些更高级的功能,比如监听URL变化、处理路由导航等。

让我们来看一下createWebHistory的源码(简化版,只保留核心逻辑):

// packages/history/src/html5.ts

import { createBaseHistory, NavigationCallback } from './common'
import { createHref } from './utils'
import { HistoryState } from './types'
import { START } from './constants'

export function createWebHistory(base: string = ''): History {
  return createBaseHistory({
    // 监听popstate事件
    listen(callback: NavigationCallback) {
      const popStateHandler = () => {
        callback({
          pathname: location.pathname,
          search: location.search,
          hash: location.hash,
        })
      }

      window.addEventListener('popstate', popStateHandler)

      return () => {
        window.removeEventListener('popstate', popStateHandler)
      }
    },

    // 修改URL
    push(to: string, data?: HistoryState) {
      const url = createHref(base, to);
      history.pushState(data, '', url);
    },

    // 替换URL
    replace(to: string, data?: HistoryState) {
      const url = createHref(base, to);
      history.replaceState(data, '', url);
    },

    // 获取当前URL
    getCurrentLocation() {
      return {
        pathname: location.pathname,
        search: location.search,
        hash: location.hash,
      }
    },

    // 获取初始state
    ensureLocation() {
      // ... 确保初始state存在
    }

  }, base)
}

可以看到,createWebHistory返回了一个History对象,这个对象包含了一系列方法,用于操作浏览器的历史记录:

  • listen(callback) 监听浏览器的popstate事件。当用户点击浏览器的前进/后退按钮时,会触发popstate事件。listen函数会注册一个回调函数,当popstate事件发生时,这个回调函数会被调用,从而可以更新页面内容。
  • push(to, data) 向历史记录中添加一个新的条目,并修改浏览器的URL。to参数是要跳转的URL,data参数可以传递一些自定义的数据。
  • replace(to, data) 替换当前历史记录中的条目,并修改浏览器的URL。to参数是要跳转的URL,data参数可以传递一些自定义的数据。
  • getCurrentLocation() 获取当前URL的信息,包括pathnamesearchhash
  • ensureLocation(): 确保History state存在,解决一些边缘情况下的问题。

三、 createBaseHistory:基石中的基石

你可能注意到,createWebHistory内部调用了createBaseHistorycreateBaseHistory是一个更底层的函数,它封装了操作历史记录的通用逻辑。

createBaseHistory接收一个options对象和一个base参数。options对象包含以下属性:

  • listen 监听URL变化的回调函数。
  • push 修改URL的回调函数。
  • replace 替换URL的回调函数。
  • getCurrentLocation 获取当前URL的回调函数。

base参数是应用的根路径。例如,如果你的应用部署在https://example.com/app/,那么base就是/app/

createBaseHistory返回一个History对象,这个对象包含了一些核心的属性和方法:

  • location 当前的URL信息。
  • state 当前历史记录的状态。
  • push(to, data) 向历史记录中添加一个新的条目,并修改浏览器的URL。
  • replace(to, data) 替换当前历史记录中的条目,并修改浏览器的URL。
  • go(delta) 在历史记录中前进或后退。
  • back() 后退一步。
  • forward() 前进一步。
  • listen(callback) 监听URL变化的回调函数。
  • destroy() 销毁History对象。

createBaseHistory的代码比较复杂,涉及到一些状态管理和事件处理的细节。这里我们只给出一个简化版的示例:

// packages/history/src/common.ts

import { Ref, ref, shallowRef } from 'vue'
import { NavigationCallback, HistoryState } from './types'

export function createBaseHistory(options: HistoryOptions, base: string): History {
  const { listen, push, replace, getCurrentLocation, ensureLocation } = options

  const location: Ref<Location> = ref(getCurrentLocation())
  const state: Ref<HistoryState | null> = shallowRef({} as HistoryState);

  let listeners: NavigationCallback[] = []
  let teardowns: (() => void)[] = []

  function triggerListeners(to: Location, from: Location,  data?: HistoryState) {
    for (const listener of listeners) {
      listener(to, from, data)
    }
  }

  function pushWithEvent(to: string, replaceMode: boolean = false, data?: HistoryState) {
    const from = location.value;
    if(replaceMode){
        replace(to, data);
    } else {
        push(to, data);
    }
    const newLocation = getCurrentLocation();
    location.value = newLocation;
    state.value = data || {}; // 更新state
    triggerListeners(newLocation, from, data);
  }

  const history: History = {
    location: location.value,
    state: state.value,

    push: (to: string, data?: HistoryState) => {
        pushWithEvent(to, false, data);
    },

    replace: (to: string, data?: HistoryState) => {
        pushWithEvent(to, true, data);
    },

    go: (delta: number) => history.push(delta.toString()), // 简化处理

    back: () => history.go(-1),
    forward: () => history.go(1),

    listen: (callback: NavigationCallback) => {
      listeners.push(callback)
      const teardown = () => {
        listeners = listeners.filter((cb) => cb !== callback)
      }
      teardowns.push(teardown)
      return teardown
    },

    destroy: () => {
      for (const teardown of teardowns) {
        teardown()
      }
      teardowns = []
      listeners = []
    },
  }

  ensureLocation(); // 确保初始location存在

  const stopListener = listen((newLocation, from, data) => {
    //  console.log("location changed", newLocation, from);
    location.value = newLocation;
    state.value = data || {}; // 更新state
    triggerListeners(newLocation, from, data);
  });
  teardowns.push(stopListener);

  return history
}

四、 createHref:URL的魔法师

createHref是一个辅助函数,用于生成完整的URL。它接收baseto两个参数,并将它们拼接在一起,生成一个完整的URL。

例如,如果base/app/to/about,那么createHref会返回/app/about

createHref的代码非常简单:

// packages/history/src/utils.ts

export function createHref(base: string, to: string): string {
  return base + to
}

五、 总结:createWebHistory的炼成之路

现在,我们已经了解了createWebHistory的实现原理。简单来说,它的作用就是:

  1. 封装浏览器的History API
  2. 提供一些更高级的功能,比如监听URL变化、处理路由导航等。
  3. 通过createBaseHistory封装操作历史记录的通用逻辑。
  4. 使用createHref生成完整的URL。

用一个表格来总结一下:

函数/对象 作用
createWebHistory 创建History模式的路由实例,是Vue Router提供的API。
createBaseHistory 封装操作历史记录的通用逻辑,是createWebHistory的底层实现。
History 包含了一系列方法,用于操作浏览器的历史记录,包括pushreplacegolisten等。
createHref 生成完整的URL,将baseto拼接在一起。
History API 浏览器提供的API,包括pushStatereplaceStatepopstate事件,用于操作浏览器的历史记录。
Location 表示当前URL的信息,包括pathnamesearchhash
HistoryState 存储与历史记录条目关联的自定义数据。可以用来保存路由状态或者其他需要在页面跳转时保留的信息。

六、 实战演练:在Vue项目中使用createWebHistory

在Vue项目中,我们可以这样使用createWebHistory

// src/router/index.ts

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
]

const router = createRouter({
  history: createWebHistory(), // 使用createWebHistory
  routes
})

export default router

这样,我们就创建了一个使用History模式的Vue Router实例。

七、 踩坑指南:History模式的注意事项

在使用History模式时,需要注意以下几点:

  1. 服务器配置: 由于History模式修改的是浏览器的URL,但并没有触发实际的页面跳转,因此,当用户直接访问某个URL时,服务器需要能够正确地处理这个请求。通常,我们需要将服务器配置为:对于所有未匹配到的请求,都返回index.html。这样,Vue Router就可以接管路由,并渲染正确的页面。

  2. base参数: 如果你的应用部署在子目录中,需要设置base参数。例如,如果你的应用部署在https://example.com/app/,那么base应该设置为/app/

  3. URL编码: 在URL中传递参数时,需要注意URL编码。例如,空格应该编码为%20,特殊字符应该编码为相应的URL编码。

八、 进阶修炼:自定义History模式

如果你对Vue Router的默认History模式不满意,还可以自定义History模式。例如,你可以实现一个基于sessionStorageHistory模式,或者一个基于hashHistory模式。

自定义History模式需要实现History接口,并提供相应的listenpushreplace等方法。

九、总结的总结

今天我们深入浅出地探讨了Vue Router中createWebHistory的实现原理。从History APIcreateBaseHistory,再到createHref,我们一步步揭开了History模式的神秘面纱。希望这次旅程能让你对Vue Router有更深刻的理解。

记住,源码就像一本武功秘籍,只有不断研读,才能练成绝世神功!

好了,今天的讲座就到这里,下次再见! 记得点赞收藏哦!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注