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