页面加载的生命周期
The Life of a Page Load
在深入细节之前,先进行一次高层级的概览可能会有所帮助。从你输入服务器渲染的 Leptos 应用 URL 的那一刻,到你点击按钮且计数器增加的那一刻,到底发生了什么?
Before we get into the weeds it might be helpful to have a higher-level overview. What exactly happens between the moment you type in the URL of a server-rendered Leptos app, and the moment you click a button and a counter increases?
我假设你具备一些关于互联网如何运作的基础知识,并且不会深入探讨 HTTP 之类的细节。相反,我将尝试展示 Leptos API 的不同部分如何映射到该过程的每个环节。
I’m assuming some basic knowledge of how the Internet works here, and won’t get into the weeds about HTTP or whatever. Instead, I’ll try to show how different parts of the Leptos APIs map onto each part of the process.
此描述还基于一个前提:你的应用正在为两个独立的目标进行编译:
This description also starts from the premise that your app is being compiled for two separate targets:
-
一个服务器版本,通常运行在 Actix 或 Axum 上,使用 Leptos 的
ssr特性进行编译。 -
A server version, often running on Actix or Axum, compiled with the Leptos
ssrfeature -
一个浏览器版本,使用 Leptos 的
hydrate特性编译为 WebAssembly (WASM)。 -
A browser version, compiled to WebAssembly (WASM) with the Leptos
hydratefeature
cargo-leptos 构建工具的存在是为了协调为这两个不同目标编译应用的过程。
The cargo-leptos build tool exists to coordinate the process of compiling your app for these two different targets.
在服务器上
On the Server
-
你的浏览器向服务器发起针对该 URL 的
GET请求。此时,浏览器对将要渲染的页面几乎一无所知。(“浏览器如何知道去哪里请求页面?”是一个有趣的问题,但超出了本教程的范围!) -
Your browser makes a
GETrequest for that URL to your server. At this point, the browser knows almost nothing about the page that’s going to be rendered. (The question “How does the browser know where to ask for the page?” is an interesting one, but out of the scope of this tutorial!) -
服务器接收到该请求,并检查它是否有办法处理该路径下的
GET请求。这就是leptos_axum和leptos_actix中的.leptos_routes()方法的作用。当服务器启动时,这些方法会遍历你在<Routes/>中提供的路由结构,生成应用可以处理的所有可能路由的列表,并告诉服务器的路由管理器:“对于这些路由中的每一个,如果你收到请求……就把它交给 Leptos。” -
The server receives that request, and checks whether it has a way to handle a
GETrequest at that path. This is what the.leptos_routes()methods inleptos_axumandleptos_actixare for. When the server starts up, these methods walk over the routing structure you provide in<Routes/>, generating a list of all possible routes your app can handle and telling the server’s router “for each of these routes, if you get a request... hand it off to Leptos.” -
服务器看到该路由可以由 Leptos 处理。因此它渲染你的根组件(通常称为
<App/>之类的名称),并为其提供请求的 URL 以及 HTTP 标头和请求元数据等其他数据。 -
The server sees that this route can be handled by Leptos. So it renders your root component (often called something like
<App/>), providing it with the URL that’s being requested and some other data like the HTTP headers and request metadata. -
你的应用程序在服务器上运行一次,构建将在该路由下渲染的组件树的 HTML 版本。(下一章中关于资源和
<Suspense/>还有更多内容。) -
Your application runs once on the server, building up an HTML version of the component tree that will be rendered at that route. (There’s more to be said here about resources and
<Suspense/>in the next chapter.) -
服务器返回此 HTML 页面,并注入关于如何加载已编译为 WASM 的应用版本的信息,以便它可以在浏览器中运行。
-
The server returns this HTML page, also injecting information on how to load the version of your app that has been compiled to WASM so that it can run in the browser.
返回的 HTML 页面本质上是你的应用,“脱水(dehydrated)”或“冻干(freeze-dried)”后的状态:它是没有任何响应式系统或你添加的事件监听器的 HTML。浏览器将通过添加响应式系统并将事件监听器附加到服务器渲染的 HTML 来“注水(rehydrate)”此 HTML 页面。因此,有两个特性标志(feature flags)适用于此过程的两半:服务器上的
ssr用于“服务器端渲染(server-side rendering)”,浏览器中的hydrate用于“注水”过程。
The HTML page that’s returned is essentially your app, “dehydrated” or “freeze-dried”: it is HTML without any of the reactivity or event listeners you’ve added. The browser will “rehydrate” this HTML page by adding the reactive system and attaching event listeners to that server-rendered HTML. Hence the two feature flags that apply to the two halves of this process:
ssron the server for “server-side rendering”, andhydratein the browser for that process of rehydration.
在浏览器中
In the Browser
-
浏览器从服务器接收此 HTML 页面。它立即回到服务器开始加载运行交互式、客户端版本应用所需的 JS 和 WASM。
-
The browser receives this HTML page from the server. It immediately goes back to the server to begin loading the JS and WASM necessary to run the interactive, client side version of the app.
-
与此同时,它渲染 HTML 版本。
-
In the meantime, it renders the HTML version.
-
当 WASM 版本加载完成后,它执行与服务器相同的路由匹配过程。因为
<Routes/>组件在服务器和客户端上是完全相同的,所以浏览器版本将读取 URL 并渲染与服务器已经返回的相同的页面。 -
When the WASM version has reloaded, it does the same route-matching process that the server did. Because the
<Routes/>component is identical on the server and in the client, the browser version will read the URL and render the same page that was already returned by the server. -
在这个初始的“注水(hydration)”阶段,WASM 版本的应用不会重新创建组成应用的 DOM 节点。相反,它遍历现有的 HTML 树,“拾取”现有的元素并添加必要的交互性。
-
During this initial “hydration” phase, the WASM version of your app doesn’t re-create the DOM nodes that make up your application. Instead, it walks over the existing HTML tree, “picking up” existing elements and adding the necessary interactivity.
请注意,这里存在一些权衡。在注水过程完成之前,页面将 看起来 是可交互的,但实际上不会响应交互。例如,如果你有一个计数器按钮并在 WASM 加载完成之前点击它,计数将不会增加,因为必要的事件监听器和响应式系统尚未添加。我们将在未来的章节中研究一些构建“平稳退化(graceful degradation)”的方法。
Note that there are some trade-offs here. Before this hydration process is complete, the page will appear interactive but won’t actually respond to interactions. For example, if you have a counter button and click it before WASM has loaded, the count will not increment, because the necessary event listeners and reactivity have not been added yet. We’ll look at some ways to build in “graceful degradation” in future chapters.
客户端导航
Client-Side Navigation
下一步非常重要。想象一下,用户现在点击一个链接以导航到应用程序中的另一个页面。
The next step is very important. Imagine that the user now clicks a link to navigate to another page in your application.
浏览器 不会 再次往返服务器,像在普通 HTML 页面之间导航或在仅使用服务器渲染(例如使用 PHP)但没有客户端部分的应用中导航那样重新加载整个页面。
The browser will not make another round trip to the server, reloading the full page as it would for navigating between plain HTML pages or an application that uses server rendering (for example with PHP) but without a client-side half.
相反,WASM 版本的应用将在浏览器中直接加载新页面,而无需向服务器请求另一个页面。本质上,你的应用从服务器加载的“多页应用”升级为浏览器渲染的“单页应用”。这产生了鱼与熊掌兼得的效果:由于服务器渲染的 HTML 具有快速的初始加载时间,而由于客户端路由具有快速的二次导航。
Instead, the WASM version of your app will load the new page, right there in the browser, without requesting another page from the server. Essentially, your app upgrades itself from a server-loaded “multi-page app” into a browser-rendered “single-page app.” This yields the best of both worlds: a fast initial load time due to the server-rendered HTML, and fast secondary navigations because of the client-side routing.
接下来的章节中描述的一些内容——比如服务器函数、资源和 <Suspense/> 之间的交互——可能看起来过于复杂。你可能会问自己:“如果我的页面在服务器上被渲染为 HTML,为什么我不能直接在服务器上 .await 它?如果我可以直接在服务器函数中调用库 X,为什么我不能在我的组件中调用它?”原因很简单:为了实现从服务器渲染到客户端渲染的升级,应用程序中的所有内容都必须能够在客户端和服务器上运行。
Some of what will be described in the following chapters—like the interactions between server functions, resources, and <Suspense/>—may seem overly complicated. You might find yourself asking, “If my page is being rendered to HTML on the server, why can’t I just .await this on the server? If I can just call library X in a server function, why can’t I call it in my component?” The reason is pretty simple: to enable the upgrade from server rendering to client rendering, everything in your application must be able to run on both the client and the server.
当然,这并不是创建网站或 Web 框架的唯一方式。但这是最常见的方式,而且我们恰好认为这是一种非常好的方式,可以为你的用户创造尽可能流畅的体验。
This is not the only way to create a website or web framework, of course. But it’s the most common way, and we happen to think it’s quite a good way, to create the smoothess possible experience for your users.