了解单页应用程序和客户端路由

2025-06-04

了解单页应用程序和客户端路由

这篇文章来自我的“Web Wizardry”简报,我会在其中探索常见 Web 开发问题的常用解决方案(无论您喜欢哪个框架)。如果您喜欢,请免费注册🪄


自从像 React 这样的“现代” JS 框架出现以来,“SPA” 就一直是一个热门话题。它们承诺提供各种各样的好处,比如动态用户交互、 闪电般的加载速度、 解决世界饥饿问题等等(好吧,最后一个有点夸张……)。

但你有没有想过,幕后究竟发生了什么?如果你使用 SPA,你的体验是否还停留在过去?让我们来探索一下:

  • 🤔 非 SPA 的工作原理 + 它们在现代网络上的表现(剧透一下,它们一如既往地是一个不错的选择!)
  • ⚙️ SPA 的主要优势和内部运作
  • 🍔 并排视觉比较,多汁的汉堡

前进!

🤔 首先,SPA 如何运作?

最近,Web 开发一直沉浸在各种缩写词的漩涡中,所以我认为有必要先澄清一下什么不是SPA 🙃

如果你的网站不是单页应用 (SPA),那么你很可能正在使用所谓的“基于服务器的路由”。这里需要打个比方。假设你身处一家正式的餐厅,有一位服务员(你也可以叫他服务员😉)。如果你想点餐,你必须:

  1. 向服务员询问你想要的食物
  2. 等待菜做好
  3. 完成后即可收到成品盘子

所有网站首次访问时都是这样的。你请求你想要的内容(访问类似这样的 URL https://breakfast.club),等待服务器返回(加载旋转器),然后在“大餐”准备好后享用(页面加载完成!🎉)

但是如果你想点甜点怎么办?回到我们的餐厅场景,你会经历和上次相同的流程:

  1. 向服务员要最美味的甜点
  2. 把脏盘子递给他们
  3. 等待菜做好
  4. 收到一个闪亮的新碗,里面装着你的圣代冰淇淋🍨

有一点我想重申一下:你把脏盘子交出去,换回一个全新的。这就是为什么每次跳转到新页面时,你都会看到一个小的加载条重新出现。以下是 11ty 使用基于服务器的路由的文档示例:

点击 11ty 文档网站的页面

每当我们点击链接时,都会看到加载栏

首先,无论何时单击这些导航链接,您似乎仍在同一页面上,并且浏览器仅刷新发生变化的部分(文档)。

🚨 但事实并非如此!每当你点击一个链接时,你的浏览器都会“清理你的盘子”(从页面上移除所有内容),并加载新的内容,包括导航和所有内容。所以,每次访问新页面时,你实际上都在重新加载所有这些导航内容,即使它们本来在你的盘子里。

等等,这太浪费了吗?

听起来浏览器好像在这里做了很多额外的工作!虽然 SPA 仍然可以解决这个问题,但它已经不像以前那么麻烦了。以下是一些需要考虑的效率点:

1. HTML 的加载和渲染非常便宜

尽管浏览器每次都要从头开始“重新绘制”页面,但加载 HTML 框架也只需要几毫秒。真正耗费资源的是浏览器需要获取的样式、脚本和图片,这就引出了……

2. 共享资源无需重新加载

例如,假设每个文档页面都加载相同的 CSS 文件: 。当您点击另一个<link rel="stylesheet" href="documentation.css">加载此 CSS 的链接时,浏览器会智能地告诉您:“哦,我已经加载了它!我只需使用它并将其应用到页面即可。” 图像和字体也是如此。

💡注意:这一切都归功于缓存。如果你想深入了解,Remix 团队提供了一篇关于缓存文档、资源等内容的精彩教程,请点击此处😄

所以,实际加载这些共享资源并没有问题。但是,如果一遍又一遍地将这些资源绘制到页面上,该怎么办呢?这就引出了……

3. 下一页仅在准备就绪时显示

以前,在“清空盘子”和“接收新盘子”之间,你可能会看到一闪而过的白色虚空。但现代浏览器已经基本解决了这个问题!简而言之,网络浏览器会等待下一个页面不仅“加载完成”的信号,而且也已经准备好供你查看和交互。这就像在盘子准备好吃饭时把它端上来,而不是给你端来一盘你需要自己动手准备的食材。

厨师在火盆烤架周围挥动刀具

Hibachi 渲染:在页面准备好食用之前向您展示页面的绘制过程(如果浏览器这样做就没那么酷了) 😉

这尤其有助于那些依赖 JavaScript 等阻塞资源来渲染所有内容的页面。以下是 Chrome 团队推出此概念时的前后对比图 🚀

Google Chrome 更新前后加载 JS 密集型页面闪白现象对比

来源文章作者:Addy Osmani

免责声明:浏览器并不总是会等待下一个页面可交互后才显示;如果您的网速较慢,浏览器认为等待时间过长,它仍然会显示未完成的页面。不过,对于像之前提到的11ty 文档中提到的速度更快的网站来说,这应该不成问题!

💁现在,我们来谈谈单页应用

那么 SPA 相比如何呢?好吧,让我们回顾一下之前的餐厅示例。当你第一次访问使用 SPA 方法的网站时,一切运作方式几乎相同:

  1. 向服务员询问您想要的食物(访问https://spa-breakfast.club
  2. 稍等片刻,让菜肴准备好(浏览器加载旋转器)
  3. 完成后即可收到成品盘子(页面已加载完成!🎉)

现在,有趣的部分就来了,当你回过头来看的时候。当你点击 SPA 上的链接时,它会用客户端路由取代典型的基于服务器的路由😮 换句话说,我们使用自己编写的 JavaScript 来处理所有链接请求,而不是立即将这些请求发送到服务器。

以下是让你的大脑动起来的代码片段🧠

<a href="/desert">Go eat desert</a>
<script>
    document.addEventListener('click', (event) => {
      if (
        // if you clicked on an A-nchor tag (link)
        event.target.tagName === 'A' &&
        // and you're going to a page on this domain (like /desert)
        event.target.origin === location.origin
      ) {
        // don't ask the server for that resource!
        event.preventDefault()
        // instead, we'll go fetch the resource ourselves
        const response = fetch('https://buffet.table/desert')
        // ...convert that response to something we can work with
        const htmlString = await response.text()
        const desert = new DOMParser()
          .parseFromString(htmlString, 'text/html')

        // ...and do something with that desert element
        // ex. append desert to our "plate" in the DOM
        document.querySelector('.my-plate').appendChild(desert)
      }
    })
</script>
Enter fullscreen mode Exit fullscreen mode

很奇怪吧?这其实就是 SPA 的精髓所在:你永远不会真正“离开”你开始的页面。相反,你会拦截所有后续的请求(链接点击),并自行处理数据获取。你通常会使用所有现代浏览器的原生APIfetch来实现这一点,就像上面的 demo-d 一样。

这就是为什么我会把 SPA 和客户端路由想象成自助餐。你不用点餐然后等着食物准备好,而是可以自己起身去拿预先准备好的食物!

这种方法的主要优点

客户端路由为单页应用带来了两大好处 🔓

首先,加载时间效率可以提升🚀我说“可以”,是因为我之前提到的所有服务器端路由优化(这可能会抵消任何性能提升)。但对于像 React、Vue 和 Svelte 这样资源密集型的框架来说,差异还是很明显的。所有这些框架都使用某种形式的客户端路由来将 JavaScript 加载时间保持在最低限度。

例如,如果我使用服务器端路由从 React 应用的一个页面转到另一个页面,浏览器就会使用 JavaScript重新解析并重新渲染整个页面!这会严重影响你的“可交互时间”,这是一个可访问性问题,你可以在这里了解更多关于它的信息

其次,您现在可以构建动态的、类似应用程序的交互 ⚙️动画页面转换是这里最容易指出的。由于您可以完全控制新内容的加载并将其应用到页面上,因此您可以实现各种 CSS 技巧,例如交叉淡入淡出、弹出式模态框、标签滑块等等。以下是我的个人网站中使用客户端路由的示例:

点击 bholmes.dev 上的链接预览页面转换

🍔 精彩的视觉对比

现在我们已经介绍了 SPA + 客户端路由的概念,让我们来看一下与服务器端路由的并排比较。

假设您收到一份订单,要求一份五分熟、多汁的汉堡,但是您更想要一份全熟的肉饼。

我们将这一刻想象为“点击链接”,请求从/medium-rare-burger/well-done-burger

使用基于服务器的方法,“点击”可以实现以下效果:

服务器端路由流程如下:1. 返回五分熟的汉堡包;2. 清空 HTML 文档,加载新的文档;3. 将全熟的汉堡包应用到我们的页面

动画展示了 3 步渲染过程

然后,客户端路由可能会这样处理请求:

客户端路由流程如下:1. 返回五分熟的汉堡;2. 使用 fetch API 请求全熟的汉堡;3. 处理响应;4. 取出

动画展示了我们新的 4 步渲染流程

请注意,在 SPA 方法中,我们永远不会清除页面!我们只需请求资源(一个烤好的汉堡),挑选出想要添加到页面的部分(肉饼),然后使用 JavaScript 执行 DOM 操作即可。

对于 HTML 文件来说,这并没有太大的性能提升。但如果 HTML 文件附带了一些我们可以获取的 JavaScript 和样式比如 React 组件),那么性能提升的空间就很大。

总结:那么我应该选择哪一个?

尽管 SPA 看起来像是任何网站的“灵丹妙药”,但两种方法都有其适用之处。

  • 服务器端路由最明显的优势在于,它更简单。无需编写和维护所有点击监听器;只需让浏览器为您服务即可。没错,您通常会使用特定于框架的库(例如React Router )来开发 SPA ,但学习和更新 JS 库总是会带来更大的开销。
  • 服务器端的第二个优势是无需担心可访问性。当你在客户端处理所有路由时,你可能会损害屏幕阅读器和键盘的体验。例如,你需要在屏幕阅读器用户点击链接时提醒他们页面上出现了新内容。而对于键盘用户,你需要确保当新的内容滑动到视图中时,他们能够聚焦到正确的元素上

☝️但是,如果您有信心解决这些问题(或使用强大的库来为您解决),那么 SPA 是一个很好的选择!

学到一点东西吗?

很高兴听到这个消息!如果你想要更多像这样的通用解决方案,可以订阅“网络魔法”简报,每两周更新一次网络魔法🔮

文章来源:https://dev.to/bholmesdev/understanding-single-page-apps-client-side-routing-52gb
PREV
为什么我使用 Surge 而不是 GitHub Pages
NEXT
4 场会议演讲改变了我作为 Web 开发者的视角