---
title: '教程'
slug: /tutorial
---

## 介绍

在这个实践教程中，我们将学习如何使用 Yew 构建 Web 应用程序。
**Yew** 是一个现代的 [Rust](https://www.rust-lang.org/) 框架，用于使用 [WebAssembly](https://webassembly.org/) 构建前端 Web 应用程序。
Yew 通过利用 Rust 强大的类型系统，鼓励可重用、可维护和良好结构化的架构。
一个庞大的社区创建的库生态系统，称为 Rust 中的 [crates](https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html)，为常用模式（如状态管理）提供了组件。
Rust 的包管理器 [Cargo](https://doc.rust-lang.org/cargo/) 允许我们利用 [crates.io](https://crates.io) 上提供的大量 crate，例如 Yew。

### 我们将要构建的内容

Rustconf 是 Rust 社区每年举办的星际聚会。
Rustconf 2020 有大量的演讲，提供了大量的信息。
在这个实践教程中，我们将构建一个 Web 应用程序，帮助其他 Rustaceans 了解这些演讲并从一个页面观看它们。

## 设置

### 先决条件

这个教程假设您已经熟悉 Rust。如果您是 Rust 的新手，免费的 [Rust 书](https://doc.rust-lang.org/book/ch00-00-introduction.html) 为初学者提供了一个很好的起点，并且即使对于有经验的 Rust 开发人员来说，它仍然是一个很好的资源。

确保安装了最新版本的 Rust，方法是运行 `rustup update` 或者[安装 Rust](https://www.rust-lang.org/tools/install)。

安装 Rust 后，您可以使用 Cargo 运行以下命令安装 `trunk`：

```bash
cargo install trunk
```

我们还需要添加 WASM 构建目标，运行以下命令：

```bash
rustup target add wasm32-unknown-unknown
```

### 设置项目

首先，创建一个新的 cargo 项目：

```bash
cargo new yew-app
cd yew-app
```

为了验证 Rust 环境是否设置正确，使用 cargo 构建工具运行初始项目。
在关于构建过程的输出之后，您应该看到预期的 "Hello, world!" 消息。

```bash
cargo run
```

## 我们的第一个静态页面

为了将这个简单的命令行应用程序转换为一个基本的 Yew web 应用程序，需要进行一些更改。

```toml title="Cargo.toml" {7}
[package]
name = "yew-app"
version = "0.1.0"
edition = "2021"

[dependencies]
yew = { version = "0.22", features = ["csr"] }
```

:::info

如果你只是正在构建一个应用程序，你只需要 `csr` 特性。它将启用 `Renderer` 和所有与客户端渲染相关的代码。

如果你正在制作一个库，请不要启用此特性，因为它会将客户端渲染逻辑拉入服务器端渲染包中。

如果你需要 Renderer 进行测试或示例，你应该在 `dev-dependencies` 中启用它。

:::

```rust ,no_run title="src/main.rs"
use yew::prelude::*;

#[component(App)]
fn app() -> Html {
    html! {
        <h1>{ "Hello World" }</h1>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}
```

现在，让我们在项目的根目录创建一个 `index.html`。

```html title="index.html"
<!doctype html>
<html lang="en">
    <head></head>
    <body></body>
</html>
```

### 启动开发服务器

运行以下命令构建并在本地提供应用程序。

```bash
trunk serve --open
```

:::info
删除选项 '--open' 以在运行 `trunk serve` 后不打开默认浏览器。
:::

Trunk 将在您修改任何源代码文件时实时重新构建您的应用程序。
默认情况下，服务器将在地址 '127.0.0.1' 的端口 '8080' 上监听 => [http://localhost:8080](http://127.0.0.1:8080)。
要更改这部分配置，请创建以下文件并根据需要进行编辑：

```toml title="Trunk.toml"
[serve]
# 局域网上的监听地址
address = "127.0.0.1"
# 广域网上的监听地址
# address = "0.0.0.0"
# 监听的端口
port = 8000
```

如果您感兴趣，您可以运行 `trunk help` 和 `trunk help <subcommand>` 以获取更多关于正在进行的流程的详细信息。

### 恭喜

您现在已经成功设置了 Yew 开发环境，并构建了您的第一个 Yew Web 应用程序。

## 构建 HTML

Yew 利用了 Rust 的过程宏，并为我们提供了一种类似于 JSX（JavaScript 的扩展，允许您在 JavaScript 中编写类似 HTML 的代码）的语法来创建标记。

### 转换为经典 HTML

由于我们已经对我们的网站长什么样有了一个很好的想法，我们可以简单地将我们的草稿转换为与 `html!` 兼容的表示。如果您习惯于编写简单的 HTML，那么您在 `html!` 中编写标记时应该没有问题。需要注意的是，这个宏与 HTML 有一些不同之处：

1. 表达式必须用大括号（`{ }`）括起来
2. 只能有一个根节点。如果您想要在不将它们包装在容器中的情况下拥有多个元素，可以使用空标签/片段（`<> ... </>`）
3. 元素必须正确关闭。

我们想要构建一个布局，原始 HTML 如下：

```html
<h1>RustConf Explorer</h1>
<div>
    <h3>Videos to watch</h3>
    <p>John Doe: Building and breaking things</p>
    <p>Jane Smith: The development process</p>
    <p>Matt Miller: The Web 7.0</p>
    <p>Tom Jerry: Mouseless development</p>
</div>
<div>
    <h3>John Doe: Building and breaking things</h3>
    <img
        src="https://placehold.co/640x360.png?text=Video+Player+Placeholder"
        alt="video thumbnail"
    />
</div>
```

现在，让我们将这个 HTML 转换为 `html!`。将以下代码片段输入（或复制/粘贴）到 `app` 函数的主体中，以便函数返回 `html!` 的值

```rust ,ignore
html! {
    <>
        <h1>{ "RustConf Explorer" }</h1>
        <div>
            <h3>{"Videos to watch"}</h3>
            <p>{ "John Doe: Building and breaking things" }</p>
            <p>{ "Jane Smith: The development process" }</p>
            <p>{ "Matt Miller: The Web 7.0" }</p>
            <p>{ "Tom Jerry: Mouseless development" }</p>
        </div>
        <div>
            <h3>{ "John Doe: Building and breaking things" }</h3>
            <img src="https://placehold.co/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
        </div>
    </>
}
```

刷新浏览器页面，您应该看到以下输出：

![Running WASM application screenshot](/img/tutorial_application_screenshot.png)

### 在标记中使用 Rust 语言结构

在 Rust 中编写标记的一个很大的优势是，我们在标记中获得了 Rust 的所有优点。
现在，我们不再在 HTML 中硬编码视频列表，而是将它们定义为 `Vec` 的 `Video` 结构体。
我们创建一个简单的 `struct`（在 `main.rs` 或我们选择的任何文件中）来保存我们的数据。

```rust
struct Video {
    id: usize,
    title: String,
    speaker: String,
    url: String,
}
```

接下来，我们将在 `app` 函数中创建这个结构体的实例，并使用它们来替代硬编码的数据：

```rust
use website_test::tutorial::Video; // 换成你自己的路径

let videos = vec![
    Video {
        id: 1,
        title: "Building and breaking things".to_string(),
        speaker: "John Doe".to_string(),
        url: "https://youtu.be/PsaFVLr8t4E".to_string(),
    },
    Video {
        id: 2,
        title: "The development process".to_string(),
        speaker: "Jane Smith".to_string(),
        url: "https://youtu.be/PsaFVLr8t4E".to_string(),
    },
    Video {
        id: 3,
        title: "The Web 7.0".to_string(),
        speaker: "Matt Miller".to_string(),
        url: "https://youtu.be/PsaFVLr8t4E".to_string(),
    },
    Video {
        id: 4,
        title: "Mouseless development".to_string(),
        speaker: "Tom Jerry".to_string(),
        url: "https://youtu.be/PsaFVLr8t4E".to_string(),
    },
];
```

为了显示它们，我们需要将 `Vec` 转换为 `Html`。我们可以通过创建一个迭代器，将其映射到 `html!` 并将其收集为 `Html` 来实现：

```rust ,ignore
let videos = videos.iter().map(|video| html! {
    <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
}).collect::<Html>();
```

:::tip
在列表项上使用键有助于 Yew 跟踪列表中哪些项发生了变化，从而实现更快的重新渲染。[始终建议在列表中使用键](/concepts/html/lists.mdx#keyed-lists)。
:::

最后，我们需要用从数据创建的 `Html` 替换硬编码的视频列表：

```rust ,ignore {6-10}
html! {
    <>
        <h1>{ "RustConf Explorer" }</h1>
        <div>
            <h3>{ "Videos to watch" }</h3>
-           <p>{ "John Doe: Building and breaking things" }</p>
-           <p>{ "Jane Smith: The development process" }</p>
-           <p>{ "Matt Miller: The Web 7.0" }</p>
-           <p>{ "Tom Jerry: Mouseless development" }</p>
+           { videos }
        </div>
        // ...
    </>
}
```

## 组件

组件是 Yew 应用程序的构建块。通过组合组件（可以由其他组件组成），我们构建我们的应用程序。通过为可重用性构建组件并保持它们的通用性，我们将能够在应用程序的多个部分中使用它们，而无需重复代码或逻辑。

到目前为止我们一直在使用的 `app` 函数是一个组件，称为 `App`。它是一个“函数式组件”。

1. 结构体组件
2. 函数式组件

在本教程中，我们将使用函数式组件。

现在，让我们将 `App` 组件拆分为更小的组件。我们首先将视频列表提取到自己的组件中。

```rust ,compile_fail
use yew::prelude::*;

struct Video {
    id: usize,
    title: String,
    speaker: String,
    url: String,
}

#[derive(Properties, PartialEq)]
struct VideosListProps {
    videos: Vec<Video>,
}

#[component(VideosList)]
fn videos_list(VideosListProps { videos }: &VideosListProps) -> Html {
    videos.iter().map(|video| html! {
        <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
    }).collect()
}
```

注意我们的 `VideosList` 函数组件的参数。函数组件只接受一个参数，该参数定义了它的 "props"（"properties" 的缩写）。Props 用于从父组件传递数据到子组件。在这种情况下，`VideosListProps` 是一个定义 props 的结构体。

:::important
用于 props 的结构体必须通过派生实现 `Properties`。
:::

为了使上面的代码编译通过，我们需要修改 `Video` 结构体如下：

```rust {1}
#[derive(Clone, PartialEq)]
struct Video {
    id: usize,
    title: String,
    speaker: String,
    url: String,
}
```

现在，我们可以更新我们的 `App` 组件以使用 `VideosList` 组件。

```rust ,ignore {4-7,13-14}
#[component(App)]
fn app() -> Html {
    // ...
-    let videos = videos.iter().map(|video| html! {
-        <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
-    }).collect::<Html>();
-
    html! {
        <>
            <h1>{ "RustConf Explorer" }</h1>
            <div>
                <h3>{"Videos to watch"}</h3>
-               { videos }
+               <VideosList videos={videos} />
            </div>
            // ...
        </>
    }
}
```

通过查看浏览器窗口，我们可以验证列表是否按预期呈现。我们已经将列表的渲染逻辑移动到了它的组件中。这缩短了 `App` 组件的源代码，使我们更容易阅读和理解。

### 使应用可以交互

这里的最终目标是显示所选视频。为了做到这一点，`VideosList` 组件需要在选择视频时“通知”其父组件，这是通过 `Callback` 完成的。这个概念称为“传递处理程序”。我们修改其 props 以接受一个 `on_click` 回调：

```rust ,ignore {4}
#[derive(Properties, PartialEq)]
struct VideosListProps {
    videos: Vec<Video>,
+    on_click: Callback<Video>
}
```

然后我们修改 `VideosList` 组件以将所选视频传递给回调。

```rust ,ignore {2-4,6-12,15-16}
#[component(VideosList)]
-fn videos_list(VideosListProps { videos }: &VideosListProps) -> Html {
+fn videos_list(VideosListProps { videos, on_click }: &VideosListProps) -> Html {
+    let on_click = on_click.clone();
    videos.iter().map(|video| {
+        let on_video_select = {
+            let on_click = on_click.clone();
+            let video = video.clone();
+            Callback::from(move |_| {
+                on_click.emit(video.clone())
+            })
+        };

        html! {
-            <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
+            <p key={video.id} onclick={on_video_select}>{format!("{}: {}", video.speaker, video.title)}</p>
        }
    }).collect()
}
```

接下来，我们需要修改 `VideosList` 的使用以传递该回调。但在这样做之前，我们应该创建一个新的组件 `VideoDetails`，当点击视频时才会显示。

```rust
use website_test::tutorial::Video;
use yew::prelude::*;

#[derive(Properties, PartialEq)]
struct VideosDetailsProps {
    video: Video,
}

#[component(VideoDetails)]
fn video_details(VideosDetailsProps { video }: &VideosDetailsProps) -> Html {
    html! {
        <div>
            <h3>{ video.title.clone() }</h3>
            <img src="https://placehold.co/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
        </div>
    }
}
```

现在，修改 `App` 组件以在选择视频时显示 `VideoDetails` 组件。

```rust ,ignore {4,6-11,13-15,22-23,25-29}
#[component(App)]
fn app() -> Html {
    // ...
+    let selected_video = use_state(|| None);

+    let on_video_select = {
+        let selected_video = selected_video.clone();
+        Callback::from(move |video: Video| {
+            selected_video.set(Some(video))
+        })
+    };

+    let details = selected_video.as_ref().map(|video| html! {
+        <VideoDetails video={video.clone()} />
+    });

    html! {
        <>
            <h1>{ "RustConf Explorer" }</h1>
            <div>
                <h3>{"Videos to watch"}</h3>
-               <VideosList videos={videos} />
+               <VideosList videos={videos} on_click={on_video_select.clone()} />
            </div>
+            { for details }
-            <div>
-                <h3>{ "John Doe: Building and breaking things" }</h3>
-                <img src="https://placehold.co/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
-            </div>
        </>
    }
}
```

现在不用担心 `use_state`，我们稍后会回到这个问题。注意我们用 `{ for details }` 提取列表数据的技巧。
`Option<_>` 实现了 `Iterator`，所以我们可以使用特殊的 `{ for ... }` 语法来逐个显示 `Iterator` 返回的唯一元素，而这[由 `html!` 宏支持](concepts/html/lists)。

### 处理状态

还记得之前使用的 `use_state` 吗？那是一个特殊的函数，称为 "hook"。Hooks 用于 "hook" 到函数组件的生命周期中并执行操作。您可以在[这里](concepts/function-components/hooks/introduction.mdx#pre-defined-hooks)了解更多关于这个 hook 和其他 hook 的信息。

:::note
结构体组件的行为不同。请查看[文档](advanced-topics/struct-components/introduction.mdx)了解有关这些的信息。
:::

## 获取数据（使用外部 REST API）

在真实世界的应用程序中，数据通常会来自 API，而不是直接硬编码在程序里。现在让我们从外部来源获取影片列表。为此，我们需要添加以下 crates：

- [`gloo-net`](https://crates.io/crates/gloo-net)  
  用于发送 fetch 请求。
- [`serde`](https://serde.rs) 并启用 `derive` 功能  
  用于将 JSON 响应反序列化。

让我们更新 `Cargo.toml` 文件中的依赖项：

```toml title="Cargo.toml" {2-5}
[dependencies]
-yew = { version = "0.22", features = ["csr"] }
+yew = { version = "0.22", features = ["csr", "serde"] }
+gloo-net = "0.7"
+serde = { version = "1.0", features = ["derive"] }
```

Yew 的 `serde` 功能会启用与 `serde` crate 的集成。对我们来说最重要的一点是，
它会为 `AttrValue` 添加 `serde::Deserialize` 的实现。

:::note
选择依赖项时，请务必确认它们与 `wasm32` 兼容！  
否则你的应用程序将无法运行。
:::

更新导入：

```rust {2}
use yew::prelude::*;
+use serde::Deserialize;
```

更新 `Video` 结构体，让它派生 `Deserialize` trait：

```rust {1-2}
-#[derive(Clone, PartialEq)]
+#[derive(Clone, PartialEq, Deserialize)]
struct Video {
    id: usize,
    title: AttrValue,
    speaker: AttrValue,
    url: AttrValue,
}
```

接下来我们需要更新 `App` 组件，让它能够获取数据。现代 Yew 的做法是使用 [`use_future`](https://docs.rs/yew/0.22.0/yew/suspense/fn.use_future.html) 和 [`<Suspense>`](https://yew.rs/docs/concepts/suspense)。

另一种方式是使用 [`yew::platform::spawn_local`](https://docs.rs/yew/0.22.0/yew/platform/fn.spawn_local.html)，
如果 hooks 无法使用，例如在 struct component 或普通函数中时，可以采用这种方式。

`use_future` 会在异步操作完成前挂起该组件，而 `<Suspense>` 则会在这段期间显示
后备 UI（例如加载中的提示）。

更新导入：

```rust {3-4}
use yew::prelude::*;
use serde::Deserialize;
+use gloo_net::http::Request;
+use yew::suspense::use_future;
```

我们将数据获取逻辑拆分到一个子组件 `VideosFetcher` 中，该组件返回 `HtmlResult`，
而父组件 `App` 则使用 `<Suspense>` 将它包裹起来：

```rust {2-6,8-39}
use yew::suspense::use_future;
+#[derive(Properties, PartialEq)]
+struct VideosFetchProps {
+    on_click: Callback<Video>,
+    selected_video: Option<Video>,
+}

+#[component]
+fn VideosFetcher(
+    VideosFetchProps {
+        on_click,
+        selected_video,
+    }: &VideosFetchProps,
+) -> HtmlResult {
+    let videos = use_future(|| async {
+        Request::get("https://yew.rs/tutorial/data.json")
+            .send()
+            .await?
+            .json::<Vec<Video>>()
+            .await
+    })?;
+
+    match &*videos {
+        Ok(videos) => Ok(html! {
+            <>
+                <div>
+                    <h3>{ "Videos to watch" }</h3>
+                    <VideosList videos={videos.clone()} on_click={on_click.clone()} />
+                </div>
+                if let Some(video) = selected_video {
+                    <VideoDetails video={video.clone()} />
+                }
+            </>
+        }),
+        Err(err) => Ok(html! {
+            <p>{format!("Error fetching videos: {err}")}</p>
+        }),
+    }
+}
```

现在我们会在 `App` 组件中使用这个新组件。

```rust {3-28,33-47}
#[component]
fn App() -> Html {
-   let videos = vec![
-       Video {
-           id: 1,
-           title: "Building and breaking things".into(),
-           speaker: "John Doe".into(),
-           url: "https://youtu.be/PsaFVLr8t4E".into(),
-       },
-       Video {
-           id: 2,
-           title: "The development process".into(),
-           speaker: "Jane Smith".into(),
-           url: "https://youtu.be/PsaFVLr8t4E".into(),
-       },
-       Video {
-           id: 3,
-           title: "The Web 7.0".into(),
-           speaker: "Matt Miller".into(),
-           url: "https://youtu.be/PsaFVLr8t4E".into(),
-       },
-       Video {
-           id: 4,
-           title: "Mouseless development".into(),
-           speaker: "Tom Jerry".into(),
-           url: "https://youtu.be/PsaFVLr8t4E".into(),
-       },
-   ];

    // ...

    html! {
-       <h1>{ "RustConf Explorer" }</h1>
-       <div>
-           <h3>{ "Videos to watch" }</h3>
-           <VideosList {videos} on_click={on_video_select} />
-       </div>
-       if let Some(video) = &*selected_video {
-           <VideoDetails video={video.clone()} />
-       }
+       <h1>{ "RustConf Explorer" }</h1>
+       <Suspense fallback={html! {<p> {"Loading..."} </p>}} >
+           <VideosFetcher
+                   on_click={on_video_select}
+                   selected_video={(*selected_video).clone()}
+               />
+       </Suspense>
    }
}
```

:::note
我们在 `use_future` 调用上使用 `?` 来传递 suspension。  
使用 suspense hooks 时，组件返回的是 `HtmlResult`，而不是 `Html`。父组件则通过 `<Suspense fallback=...>` 在数据获取期间显示加载提示。
:::

现在，回到浏览器查看，一切本来应该会如预期运行……如果不是因为 CORS 的话。  
要解决这个问题，我们需要一个代理服务器。幸运的是，trunk 已经提供了这个功能。

更新以下这一行：

```rust {1-2}
-       Request::get("https://yew.rs/tutorial/data.json")
+       Request::get("/tutorial/data.json")
```

现在，使用以下命令重新启动服务器：

```bash
trunk serve --proxy-backend=https://yew.rs/tutorial
```

刷新页面后，一切就应该能如预期运行。

由于数据加载速度很快，你可能看不到加载中的文字。如果你想看到它，可以在 `use_future` hook 中使用 `TimeoutFuture` 添加一段人为延迟。首先，将 `gloo-timers` 添加到 `Cargo.toml`：

```toml
gloo-timers = { version = "0.3", features = ["futures"] }
```

然后在 hook 中加入延迟：

```rust
let videos = use_future(|| async {
+   gloo_timers::future::TimeoutFuture::new(3_000).await; // 延迟 3 秒
    Request::get("/tutorial/data.json")
        .send()
        .await?
        .json::<Vec<Video>>()
        .await
})?;
```

## 总结

恭喜！您已经创建了一个从外部 API 获取数据并显示视频列表的 Web 应用程序。

## 接下来

这个应用程序离完美或有用还有很长的路要走。在完成本教程后，您可以将其作为探索更高级主题的起点。

### 样式

我们的应用程序看起来非常丑陋。没有 CSS 或任何样式。不幸的是，Yew 没有提供内置的样式组件。请查看 [Trunk 的 assets](https://trunk-rs.github.io/trunk/guide/assets/index.html)，了解如何添加样式表。

### 更多依赖库

我们的应用程序只使用了很少的外部依赖。有很多 crate 可以使用。请查看[外部库](/community/external-libs)以获取更多详细信息。

### 了解更多关于 Yew

阅读我们的[官方文档](../getting-started/introduction.mdx)。它更详细地解释了许多概念。要了解有关 Yew API 的更多信息，请查看我们的[API 文档](https://docs.rs/yew)。
