---
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 = { git = "https://github.com/yewstack/yew/", features = ["csr"] }
```

:::info

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

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

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

:::

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

#[function_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://via.placeholder.com/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://via.placeholder.com/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>,
}

#[function_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}
#[function_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}
#[function_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,
}

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

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

```rust ,ignore {4,6-11,13-15,22-23,25-29}
#[function_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://via.placeholder.com/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 而不是硬编码。让我们从外部源获取我们的视频列表。为此，我们需要添加以下 crate：

-   [`gloo-net`](https://crates.io/crates/gloo-net)
    用于进行 fetch 调用。
-   [`serde`](https://serde.rs) 和其派生特性
    用于反序列化 JSON 响应
-   [`wasm-bindgen-futures`](https://crates.io/crates/wasm-bindgen-futures)
    用于将 Rust 的 Future 作为 Promise 执行

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

```toml title="Cargo.toml"
[dependencies]
gloo-net = "0.6"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen-futures = "0.4"
```

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

更新 `Video` 结构体以派生 `Deserialize` 特性：

```rust ,ignore {1, 3-4}
+ use serde::Deserialize;

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

作为最后一步，我们需要更新我们的 `App` 组件，以便进行 fetch 请求，而不是使用硬编码的数据

```rust ,ignore {1,5-25,34-35}
+ use gloo_net::http::Request;

#[function_component(App)]
fn app() -> Html {
-    let videos = vec![
-        // ...
-    ]
+    let videos = use_state(|| vec![]);
+    {
+        let videos = videos.clone();
+        use_effect_with((), move |_| {
+            let videos = videos.clone();
+            wasm_bindgen_futures::spawn_local(async move {
+                let fetched_videos: Vec<Video> = Request::get("https://yew.rs/tutorial/data.json")
+                    .send()
+                    .await
+                    .unwrap()
+                    .json()
+                    .await
+                    .unwrap();
+                videos.set(fetched_videos);
+            });
+            || ()
+        });
+    }

    // ...

    html! {
        <>
            <h1>{ "RustConf Explorer" }</h1>
            <div>
                <h3>{"Videos to watch"}</h3>
-                <VideosList videos={videos} on_click={on_video_select.clone()} />
+                <VideosList videos={(*videos).clone()} on_click={on_video_select.clone()} />
            </div>
            { for details }
        </>
    }
}
```

:::note
我们在这里使用 `unwrap`，因为这是一个演示应用程序。在真实的应用程序中，您可能希望有[适当的错误处理](https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html)。
:::

现在，查看浏览器，看看一切是否按预期工作……如果不是因为 CORS 的话。为了解决这个问题，我们需要一个代理服务器。幸运的是 trunk 提供了这个功能。

更新这些行：

```rust ,ignore {2-3}
// ...
-                let fetched_videos: Vec<Video> = Request::get("https://yew.rs/tutorial/data.json")
+                let fetched_videos: Vec<Video> = Request::get("/tutorial/data.json")
// ...
```

现在，使用以下命令重新运行服务器：

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

刷新网页，一切应该按预期工作。

## 总结

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

## 接下来

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

### 样式

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

### 更多依赖库

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

### 了解更多关于 Yew

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