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

使用资源加载数据

Loading Data with Resources

资源(Resource)是异步任务的响应式包装器,它们允许你将异步的 Future 集成到同步响应式系统中。

Resources are reactive wrappers for asynchronous tasks, which allow you to integrate an asynchronous Future into the synchronous reactive system.

它们使你能够有效地加载一些异步数据,然后以同步或异步的方式响应式地访问它。你可以像对待普通 Future 一样 .await 一个资源,这会跟踪它。但你也可以通过 .get() 和其他信号访问方法来访问资源,就好像资源是一个信号一样,如果它已解析(resolved),则返回 Some(T),如果仍在挂起(pending),则返回 None

They effectively allow you to load some async data, and then reactively access it either synchronously or asynchronously. You can .await a resource like an ordinary Future, and this will track it. But you can also access a resource with .get() and other signal access methods, as if a resource were a signal that returns Some(T) if it has resolved, and None if it’s still pending.

资源主要有两种形式:ResourceLocalResource。如果你正在使用服务端渲染(本书稍后会讨论),你应该默认使用 Resource。如果你正在使用带有 !Send API 的客户端渲染(例如许多浏览器 API),或者你正在使用 SSR 但有一些只能在浏览器上完成的异步任务(例如访问异步浏览器 API),那么你应该使用 LocalResource

Resources come in two primary flavors: Resource and LocalResource. If you’re using server-side rendering (which this book will discuss later), you should default to using Resource. If you’re using client-side rendering with a !Send API (like many of the browser APIs), or if you are using SSR but have some async task that can only be done on the browser (for example, accessing an async browser API) then you should use LocalResource.

本地资源

Local Resources

LocalResource::new() 接受一个参数:一个返回 Future 的“获取器(fetcher)”函数。

LocalResource::new() takes a single argument: a “fetcher” function that returns a Future.

这个 Future 可以是一个 async 块、一个 async fn 调用的结果,或任何其他 Rust Future。该函数的工作方式类似于派生信号或我们目前见过的其他响应式闭包:你可以在其内部读取信号,每当信号发生变化时,该函数将再次运行,创建一个新的 Future 来执行。

The Future can be an async block, the result of an async fn call, or any other Rust Future. The function will work like a derived signal or the other reactive closures that we’ve seen so far: you can read signals inside it, and whenever the signal changes, the function will run again, creating a new Future to run.

// 这个 count 是我们的同步本地状态
// this count is our synchronous, local state
let (count, set_count) = signal(0);

// 跟踪 `count`,并在其变化时通过调用 `load_data` 重新加载
// tracks `count`, and reloads by calling `load_data`
// whenever it changes
let async_data = LocalResource::new(move || load_data(count.get()));

创建资源会立即调用其获取器并开始轮询 Future。读取资源将返回 None,直到异步任务完成,此时它会通知其订阅者,并变为 Some(value)

Creating a resource immediately calls its fetcher and begins polling the Future. Reading from a resource will return None until the async task completes, at which point it will notify its subscribers, and now have Some(value).

你也可以 .await 一个资源。这似乎毫无意义——为什么要为一个 Future 创建包装器,然后又去 .await 它呢?我们将在下一章看到原因。

You can also .await a resource. This might seem pointless—Why would you create a wrapper around a Future, only to then .await it? We’ll see why in the next chapter.

资源

Resources

如果你正在使用 SSR,在大多数情况下你应该使用 Resource 而不是 LocalResource

If you’re using SSR, you should be using Resource instead of LocalResource in most cases.

这个 API 略有不同。Resource::new() 接受两个函数作为参数:

This API is slightly different. Resource::new() takes two functions as its arguments:

  1. 一个源函数(source function),包含“输入”。该输入是经过记忆化(memoize)处理的,每当其值发生变化时,都会调用获取器。
  2. a source function, which contains the “input.” This input is memoized, and whenever its value changes, the fetcher will be called.
  3. 一个获取器函数(fetcher function),它从源函数获取数据并返回一个 Future
  4. a fetcher function, which takes the data from the source function and returns a Future

LocalResource 不同,Resource 会将其值从服务器序列化到客户端。然后,在客户端首次加载页面时,初始值将被反序列化,而不是再次运行异步任务。这非常重要且非常有用:这意味着数据加载从服务器开始,而不是等待客户端 WASM 包加载并开始运行应用程序。(关于这一点,后续章节会有更多说明。)

Unlike a LocalResource, a Resource serializes its value from the server to the client. Then, on the client, when first loading the page, the initial value will be deserialized rather than the async task running again. This is extremely important and very useful: It means that rather than waiting for the client WASM bundle to load and begin running the application, data loading begins on the server. (There will be more to say about this in later chapters.)

这也是为什么 API 被拆分为两部分:函数中的信号是被跟踪的,但获取器中的信号是不被跟踪的,因为这允许资源保持响应式,而无需在客户端初始注水(hydration)期间再次运行获取器。

This is also why the API is split into two parts: signals in the source function are tracked, but signals in the fetcher are untracked, because this allows the resource to maintain reactivity without needing to run the fetcher again during initial hydration on the client.

这是一个相同的示例,使用 Resource 代替 LocalResource

Here’s the same example, using Resource instead of LocalResource

// 这个 count 是我们的同步本地状态
// this count is our synchronous, local state
let (count, set_count) = signal(0);

// 我们的资源
// our resource
let async_data = Resource::new(
    move || count.get(),
    // 每当 `count` 变化时,这里都会运行
    // every time `count` changes, this will run
    |count| load_data(count) 
);

资源还提供了一个 refetch() 方法,允许你手动重新加载数据(例如,响应按钮点击)。

Resources also provide a refetch() method that allows you to manually reload the data (for example, in response to a button click).

要创建一个仅运行一次的资源,可以使用 OnceResource,它仅接收一个 Future,并添加了一些由于已知其仅加载一次而产生的优化。

To create a resource that simply runs once, you can use OnceResource, which simply takes a Future, and adds some optimizations that come from knowing it will only load once.

let once = OnceResource::new(load_data(42));

访问资源

Accessing Resources

LocalResourceResource 都实现了各种信号访问方法(.read().with().get()),但返回的是 Option<T> 而不是 T;在异步数据加载完成之前,它们将为 None

Both LocalResource and Resource implement the various signal access methods (.read(), .with(), .get()), but return Option<T> instead of T; they will be None until the async data has loaded.

在线示例

点击打开 CodeSandbox。

Click to open CodeSandbox.

CodeSandbox 源码 (CodeSandbox Source)
use gloo_timers::future::TimeoutFuture;
use leptos::prelude::*;

// 这里我们定义一个异步函数。
// 这可以是任何东西:网络请求、数据库读取等。
// 在这里,我们只是将一个数字乘以 10。
// Here we define an async function
// This could be anything: a network request, database read, etc.
// Here, we just multiply a number by 10
async fn load_data(value: i32) -> i32 {
    // 模拟一秒延迟
    // fake a one-second delay
    TimeoutFuture::new(1_000).await;
    value * 10
}

#[component]
pub fn App() -> impl IntoView {
    // 这个 count 是我们的同步本地状态
    // this count is our synchronous, local state
    let (count, set_count) = signal(0);

    // 跟踪 `count`,并在其变化时通过调用 `load_data` 重新加载
    // tracks `count`, and reloads by calling `load_data`
    // whenever it changes
    let async_data = LocalResource::new(move || load_data(count.get()));

    // 如果资源不读取任何响应式数据,它将只加载一次
    // a resource will only load once if it doesn't read any reactive data
    let stable = LocalResource::new(|| load_data(1));

    // 我们可以使用 .get() 访问资源值。
    // 在 Future 解析之前,这将响应式地返回 None,
    // 并在解析后更新为 Some(T)。
    // we can access the resource values with .get()
    // this will reactively return None before the Future has resolved
    // and update to Some(T) when it has resolved
    let async_result = move || {
        async_data
            .get()
            .map(|value| format!("Server returned {value:?}"))
            // 此加载状态仅在第一次加载之前显示
            // This loading state will only show before the first load
            .unwrap_or_else(|| "Loading...".into())
    };

    view! {
        <button
            on:click=move |_| *set_count.write() += 1
        >
            "Click me"
        </button>
        <p>
            <code>"stable"</code>": " {move || stable.get()}
        </p>
        <p>
            <code>"count"</code>": " {count}
        </p>
        <p>
            <code>"async_value"</code>": "
            {async_result}
            <br/>
        </p>
    }
}

fn main() {
    leptos::mount::mount_to_body(App)
}