页面加载的生命周期
在深入讨论之前,了解一下更高层次的概览可能会有所帮助。从你在浏览器中输入一个服务器渲染的 Leptos 应用的 URL 到点击按钮使计数器增加之间,究竟发生了什么?
这里假设你对互联网的工作原理有一定的了解,因此不会详细讨论 HTTP 或其他相关内容。我们将着重展示 Leptos API 的不同部分如何对应于整个过程的各个阶段。
本文基于以下前提:你的应用是为两个独立的目标进行编译的:
- 服务器版本:通常运行在 Actix 或 Axum 上,使用 Leptos 的
ssr功能进行编译; - 浏览器版本:编译为 WebAssembly (WASM),使用 Leptos 的
hydrate功能进行编译。
cargo-leptos 构建工具的作用就是协调为这两个不同目标编译你的应用程序的过程。
在服务器端
- 浏览器向服务器发出一个针对该 URL 的
GET请求。这时,浏览器对将要渲染的页面几乎一无所知。(至于“浏览器如何知道要向哪里请求页面?”是一个有趣的问题,但超出了本教程的范围!) - 服务器接收到请求,并检查是否有处理该路径的
GET请求的方法。这正是leptos_axum和leptos_actix中.leptos_routes()方法的用途。当服务器启动时,这些方法会根据<Routes/>提供的路由结构生成应用程序可以处理的所有路由列表,并告诉服务器的路由器:“对于每一个这些路由,如果接收到请求……将其交给 Leptos 处理。” - 服务器发现该路由可以由 Leptos 处理。因此,它渲染你的根组件(通常称为
<App/>),并向其提供正在请求的 URL 以及其他一些数据(例如 HTTP 头信息和请求元数据)。 - 应用程序在服务器端运行一次,生成该路由下组件树的 HTML 版本。(有关资源和
<Suspense/>的更多内容将在下一章讨论。) - 服务器返回这个 HTML 页面,同时注入信息,用于加载已编译为 WASM 的客户端版本以及初始化它所需的 JavaScript。
返回的 HTML 页面本质上是你的应用程序的“脱水版”或“冻干版”:它是没有任何反应式功能或事件监听器的 HTML。浏览器会通过为服务器渲染的 HTML 添加反应式系统并附加事件监听器来“重水化”该页面。因此,这个过程的两个阶段分别使用了两种功能标志:服务器端渲染的
ssr和浏览器端重水化的hydrate。
在浏览器端
- 浏览器从服务器接收到这个 HTML 页面。然后,它会立即向服务器请求加载运行交互式客户端版本所需的 JS 和 WASM。
- 在此期间,浏览器渲染 HTML 版本。
- 当 WASM 版本加载完成后,它会执行与服务器相同的路由匹配过程。因为
<Routes/>组件在服务器端和客户端是完全一致的,浏览器版本会读取 URL 并渲染与服务器返回的页面相同的内容。 - 在初始“重水化”阶段,WASM 版本不会重新创建组成应用的 DOM 节点。相反,它会遍历现有的 HTML 树,“拾取”已有的元素,并添加必要的交互功能。
需要注意的是,这里有一些权衡。在完成重水化之前,页面看起来是交互式的,但实际上不会响应交互。例如,如果你有一个计数按钮,并在 WASM 加载完成之前点击它,计数不会增加,因为必要的事件监听器和反应式功能尚未添加。我们将在后续章节中讨论如何实现“优雅降级”。
客户端导航
接下来的步骤非常重要。假设用户现在点击了一个链接,导航到应用程序中的另一个页面。
此时,浏览器不会再次向服务器发送请求,也不会像普通 HTML 页面或仅使用服务端渲染(如 PHP)应用那样重新加载整个页面。
相反,WASM 版本的应用程序会在浏览器中直接加载新页面,而无需从服务器请求其他页面。基本上,应用程序会从一个服务器加载的“多页面应用程序”升级为一个浏览器渲染的“单页面应用程序”。这结合了两种模式的最佳优点:服务器渲染的 HTML 提供了快速的初始加载时间,而客户端路由提供了快速的二次导航。
在后续章节中描述的一些内容(例如服务器函数、资源与 <Suspense/> 的交互)可能看起来过于复杂。你可能会问:“如果我的页面已经在服务器上渲染为 HTML,为什么我不能直接在服务器上 .await?如果我可以在服务器函数中调用库 X,为什么不能在组件中调用它?”原因很简单:为了实现从服务器渲染到客户端渲染的平滑过渡,应用中的所有内容都必须能够在服务器或浏览器中运行。
当然,这并不是创建网站或 Web 框架的唯一方式。但我们认为,这是为用户创造尽可能平滑体验的一种非常好的方法。