Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

服务器函数

Server Functions

如果你正在创建任何超出玩具应用(toy app)范畴的项目,你就需要经常在服务器上运行代码:从仅在服务器运行的数据库读取或向其写入数据,使用你不希望发送到客户端的库运行昂贵的计算,访问由于 CORS 原因或因为需要存储在服务器上且绝对不应发送到用户浏览器的秘密 API 密钥而必须从服务器调用的 API。

If you’re creating anything beyond a toy app, you’ll need to run code on the server all the time: reading from or writing to a database that only runs on the server, running expensive computations using libraries you don’t want to ship down to the client, accessing APIs that need to be called from the server rather than the client for CORS reasons or because you need a secret API key that’s stored on the server and definitely shouldn’t be shipped down to a user’s browser.

传统上,这是通过分离服务器和客户端代码,并设置像 REST API 或 GraphQL API 之类的东西来允许你的客户端获取和变动服务器上的数据。这没问题,但它要求你在多个不同的地方编写和维护代码(客户端代码用于获取,服务器端函数用于运行),同时还需要创建一个额外的东西来管理,即两者之间的 API 协约。

Traditionally, this is done by separating your server and client code, and by setting up something like a REST API or GraphQL API to allow your client to fetch and mutate data on the server. This is fine, but it requires you to write and maintain your code in multiple separate places (client-side code for fetching, server-side functions to run), as well as creating a third thing to manage, which is the API contract between the two.

Leptos 是引入 服务器函数 (server functions) 概念的众多现代框架之一。服务器函数有两个关键特征:

Leptos is one of a number of modern frameworks that introduce the concept of server functions. Server functions have two key characteristics:

  1. 服务器函数与你的组件代码 同地协作 (co-located),这样你就可以按功能而不是按技术来组织工作。例如,你可能有一个“深色模式”功能,它应该跨会话持久化用户的深色/浅色模式偏好,并在服务器渲染期间应用,从而避免闪烁。这需要一个在客户端上具有交互性的组件,以及一些在服务器上完成的工作(设置 cookie,甚至在数据库中存储用户信息)。传统上,这个功能最终可能会分散在代码中的两个不同位置,一个在“前端”,一个在“后端”。有了服务器函数,你可能只需将它们都写在一个 dark_mode.rs 中,然后就可以不用管它了。

  2. Server functions are co-located with your component code, so that you can organize your work by feature, not by technology. For example, you might have a “dark mode” feature that should persist a user’s dark/light mode preference across sessions, and be applied during server rendering so there’s no flicker. This requires a component that needs to be interactive on the client, and some work to be done on the server (setting a cookie, maybe even storing a user in a database.) Traditionally, this feature might end up being split between two different locations in your code, one in your “frontend” and one in your “backend.” With server functions, you’ll probably just write them both in one dark_mode.rs and forget about it.

  3. 服务器函数是 同构的 (isomorphic),即它们既可以从服务器调用,也可以从浏览器调用。这是通过为两个平台生成不同的代码来实现的。在服务器上,服务器函数只需运行。在浏览器中,服务器函数的主体被替换为一个存根(stub),该存根实际上向服务器发起 fetch 请求,将参数序列化到请求中,并从响应中反序列化返回值。但在任何一端,该函数都可以简单地被调用:你可以创建一个写入数据库的 add_todo 函数,并只需从浏览器中按钮的点击处理器调用它即可!

  4. Server functions are isomorphic, i.e., they can be called either from the server or the browser. This is done by generating code differently for the two platforms. On the server, a server function simply runs. In the browser, the server function’s body is replaced with a stub that actually makes a fetch request to the server, serializing the arguments into the request and deserializing the return value from the response. But on either end, the function can simply be called: you can create an add_todo function that writes to your database, and simply call it from a click handler on a button in the browser!

使用服务器函数

Using Server Functions

事实上,我很喜欢那个例子。它看起来会是什么样子?其实非常简单。

Actually, I kind of like that example. What would it look like? It’s pretty simple, actually.

// todo.rs
// todo.rs

#[server]
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
    let mut conn = db().await?;

    match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
        .bind(title)
        .execute(&mut conn)
        .await
    {
        Ok(_row) => Ok(()),
        Err(e) => Err(ServerFnError::ServerError(e.to_string())),
    }
}

#[component]
pub fn BusyButton() -> impl IntoView {
	view! {
        <button on:click=move |_| {
            spawn_local(async {
                add_todo("So much to do!".to_string()).await;
            });
        }>
            "Add Todo"
        </button>
	}
}

你会立即注意到这里有几件事:

You’ll notice a couple things here right away:

  • 服务器函数可以使用仅限服务器的依赖项,如 sqlx,并且可以访问仅限服务器的资源,如我们的数据库。

  • Server functions can use server-only dependencies, like sqlx, and can access server-only resources, like our database.

  • 服务器函数是 async 的。即使它们在服务器上只执行同步工作,函数签名仍然需要是 async 的,因为从浏览器调用它们 必须 是异步的。

  • Server functions are async. Even if they only did synchronous work on the server, the function signature would still need to be async, because calling them from the browser must be asynchronous.

  • 服务器函数返回 Result<T, ServerFnError>。同样,即使它们在服务器上只执行绝不会出错(infallible)的工作,这也是成立的,因为 ServerFnError 的变体包含了在发起网络请求过程中可能出现的各种错误。

  • Server functions return Result<T, ServerFnError>. Again, even if they only do infallible work on the server, this is true, because ServerFnError’s variants include the various things that can be wrong during the process of making a network request.

  • 服务器函数可以从客户端调用。看看我们的点击处理器。这是 永远只会 在客户端运行的代码。但它可以像调用普通的异步函数一样调用函数 add_todo(使用 spawn_local 来运行 Future):

  • Server functions can be called from the client. Take a look at our click handler. This is code that will only ever run on the client. But it can call the function add_todo (using spawn_local to run the Future) as if it were an ordinary async function:

move |_| {
	spawn_local(async {
		add_todo("So much to do!".to_string()).await;
	});
}
  • 服务器函数是用 fn 定义的顶级函数。与事件监听器、派生信号以及 Leptos 中的绝大多数其他事物不同,它们不是闭包!作为 fn 调用,它们无法访问应用的响应式状态或任何未作为参数传入的内容。同样,这非常合理:当你向服务器发起请求时,服务器无法访问客户端状态,除非你显式发送它。(否则我们将不得不序列化整个响应式系统,并在每次请求时通过线路发送。这可不是个好主意。)

  • Server functions are top-level functions defined with fn. Unlike event listeners, derived signals, and most everything else in Leptos, they are not closures! As fn calls, they have no access to the reactive state of your app or anything else that is not passed in as an argument. And again, this makes perfect sense: When you make a request to the server, the server doesn’t have access to client state unless you send it explicitly. (Otherwise we’d have to serialize the whole reactive system and send it across the wire with every request. This would not be a great idea.)

  • 服务器函数的参数和返回值都需要是可序列化的。同样,希望这也能理解:虽然通常函数参数不需要被序列化,但从浏览器调用服务器函数意味着序列化参数并通过 HTTP 发送它们。

  • Server function arguments and return values both need to be serializable. Again, hopefully this makes sense: while function arguments in general don’t need to be serialized, calling a server function from the browser means serializing the arguments and sending them over HTTP.

关于定义服务器函数的方式,也有几点需要注意。

There are a few things to note about the way you define a server function, too.

  • 服务器函数是通过使用 #[server] 来标注定义的,它是一个可以定义在任何地方的顶级函数。

  • Server functions are created by using the #[server] macro to annotate a top-level function, which can be defined anywhere.

服务器函数通过使用条件编译来工作。在服务器上,服务器函数创建一个 HTTP 端点,该端点接收其参数作为 HTTP 请求,并将其结果作为 HTTP 响应返回。对于客户端/浏览器构建,服务器函数的主体被替换为一个 HTTP 请求存根。

Server functions work by using conditional compilation. On the server, the server function creates an HTTP endpoint that receives its arguments as an HTTP request, and returns its result as an HTTP response. For the client-side/browser build, the body of the server function is stubbed out with an HTTP request.

:::admonish warning title="关于安全性的重要提示"

关于安全性的重要提示

An Important Note about Security

服务器函数是一项很酷的技术,但请务必记住:服务器函数不是魔法;它们只是定义公共 API 的语法糖。 服务器函数的 主体 (body) 永远不会公开;它只是你服务器二进制文件的一部分。但服务器函数是一个可以公开访问的 API 端点,其返回值只是一个 JSON 或类似的二进制大对象(blob)。除非信息是公开的,或者你已经实施了适当的安全程序,否则不要从服务器函数返回信息。这些程序可能包括验证传入请求、确保适当的加密、速率限制访问等。

Server functions are a cool technology, but it’s very important to remember. Server functions are not magic; they’re syntax sugar for defining a public API. The body of a server function is never made public; it’s just part of your server binary. But the server function is a publicly accessible API endpoint, and its return value is just a JSON or similar blob. Do not return information from a server function unless it is public, or you've implemented proper security procedures. These procedures might include authenticating incoming requests, ensuring proper encryption, rate limiting access, and more. :::

自定义服务器函数

Customizing Server Functions

默认情况下,服务器函数将其参数编码为 HTTP POST 请求(使用 serde_qs),将其返回值编码为 JSON(使用 serde_json)。这种默认设置旨在促进与 <form> 元素的兼容性,即使在 WASM 被禁用、不受支持或尚未加载的情况下,<form> 元素也原生支持发起 POST 请求。它们将端点挂载在一个经过哈希处理的 URL 上,旨在防止命名冲突。

By default, server functions encode their arguments as an HTTP POST request (using serde_qs) and their return values as JSON (using serde_json). This default is intended to promote compatibility with the <form> element, which has native support for making POST requests, even when WASM is disabled, unsupported, or has not yet loaded. They mount their endpoints at a hashed URL intended to prevent name collisions.

然而,自定义服务器函数的方法有很多,支持各种输入和输出编码、设置特定端点的能力等等。

However, there are many ways to customize server functions, with a variety of supported input and output encodings, the ability to set specific endpoints, and so on.

查看 #[server]server_fn crate 的文档,以及仓库中广泛的 server_fns_axum 示例,以获取更多信息和示例。

Take a look at the docs for the #[server] macro and server_fn crate, and the extensive server_fns_axum example in the repo for more information and examples.

使用自定义错误

Using Custom Errors

服务器函数可以返回任何实现了 FromServerFnError trait 的错误类型。 这使得错误处理更加符合人体工程学,并允许你向客户端提供特定领域的错误信息:

Server functions can return any kind of errors that implement the FromServerFnError trait. This makes error handling much more ergonomic and allows you to provide domain-specific error information to your clients:

use leptos::prelude::*;
use server_fn::codec::JsonEncoding;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum AppError {
    ServerFnError(ServerFnErrorErr),
    DbError(String),
}

impl FromServerFnError for AppError {
    type Encoder = JsonEncoding;

    fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
        AppError::ServerFnError(value)
    }
}

#[server]
pub async fn create_user(name: String, email: String) -> Result<User, AppError> {
    // 尝试在数据库中创建用户
    // Try to create user in database
    match insert_user_into_db(&name, &email).await {
        Ok(user) => Ok(user),
        Err(e) => Err(AppError::DbError(e.to_string())),
    }
}

注意事项

Quirks to Note

服务器函数有一些值得注意的特性(quirks):

Server functions come with a few quirks that are worth noting:

  • 使用指针大小的整数类型,如 isizeusize,在 32 位 WASM 架构和 64 位服务器架构之间进行调用时可能会导致错误;如果服务器返回的值超过了 32 位所能容纳的范围,这将导致反序列化错误。使用固定大小的类型(如 i32i64)可以缓解这个问题。

  • Using pointer-sized integer types such as isize and usize can lead to errors when making calls between the 32-bit WASM architecture and a 64-bit server architecture; if the server responds with a value that doesn't fit in 32 bits, this will lead to a deserialization error. Use fixed size types such as i32 or i64 to mitigate this problem.

  • 发送到服务器的参数默认使用 serde_qs 进行 URL 编码。这使它们能很好地与 <form> 元素配合工作,但也可能有一些特性:例如,当前版本的 serde_qs 在处理可选类型(见 此处此处)或带有元组变体的枚举(见 此处)时并不总是能很好地工作。你可以使用这些 issue 中描述的变通方法,或者 切换到备选输入编码

  • Arguments sent to the server are URL-encoded using serde_qs by default. This allows them to work well with <form> elements, but can have some quirks: for example, the current version of serde_qs does not always work well with optional types (see here or here) or with enums that have tuple variants (see here). You can use the workarounds described in those issues, or switch to an alternate input encoding.

将服务器函数与 Leptos 集成

Integrating Server Functions with Leptos

到目前为止,我所说的一切实际上都是与框架无关的。(事实上,Leptos 的服务器函数 crate 也已经被集成到了 Dioxus 中!)服务器函数只是一种定义类似于函数的 RPC 调用的方式,它依赖于 HTTP 请求和 URL 编码等 Web 标准。

So far, everything I’ve said is actually framework agnostic. (And in fact, the Leptos server function crate has been integrated into Dioxus as well!) Server functions are simply a way of defining a function-like RPC call that leans on Web standards like HTTP requests and URL encoding.

但在某种程度上,它们也为我们到目前为止的故事提供了最后缺失的原语。因为服务器函数只是一个普通的 Rust 异步函数,它能与我们 之前 讨论过的异步 Leptos 原语完美集成。因此,你可以轻松地将服务器函数与应用程序的其他部分集成:

But in a way, they also provide the last missing primitive in our story so far. Because a server function is just a plain Rust async function, it integrates perfectly with the async Leptos primitives we discussed earlier. So you can easily integrate your server functions with the rest of your applications:

  • 创建 资源 (resources),通过调用服务器函数从服务器加载数据。

  • Create resources that call the server function to load data from the server

  • <Suspense/><Transition/> 下读取这些资源,以便在数据加载时启用流式 SSR 和回退状态。

  • Read these resources under <Suspense/> or <Transition/> to enable streaming SSR and fallback states while data loads.

  • 创建 操作 (actions),通过调用服务器函数在服务器上变动数据。

  • Create actions that call the server function to mutate data on the server

本书的最后一部分将通过介绍使用渐进增强(progressively-enhanced)的 HTML 表单来运行这些服务器操作的模式,使这一点变得更加具体。

The final section of this book will make this a little more concrete by introducing patterns that use progressively-enhanced HTML forms to run these server actions.

但在接下来的几章中,我们将实际研究一下你可能想用服务器函数做的一些细节,包括与 Actix 和 Axum 服务器框架提供的强大提取器(extractors)集成的最佳方法。

But in the next few chapters, we’ll actually take a look at some of the details of what you might want to do with your server functions, including the best ways to integrate with the powerful extractors provided by the Actix and Axum server frameworks.