跳至主要內容
版本:0.21

教學

簡介

在這份實作教學中,我們將了解如何能使用 Yew 來建立網頁應用程式。**Yew** 是個現代化的 Rust 架構,能以 WebAssembly 來建立前端網頁應用程式。透過善用 Rust 強大的型別系統,Yew 鼓勵再利用、可維護性和良好的架構。在 Rust 中稱為 箱子 的社群建立函式庫組成龐大的生態系,它們提供元件來處理常見的型態,像是狀態管理。Rust 的套件管理員 Cargo,讓我們能利用 crates.io 上眾多的箱子,像是 Yew。

我們將要建立的事情

Rustconf 是一款每年舉辦一次的 Rust 社群星際大會。Rustconf 2020 有許許多多的場次提供充足的資訊。在這個動手做的教學課程中,我們將建立一款網路應用程式,幫助其他 Rustaceans 統覽這些場次並從一個頁面觀看所有場次。

設定

先備條件

本教學課程假設您已熟悉 Rust。如果您是 Rust 新手,可以閱讀免費的 Rust 手冊,它是初學者的絕佳起點,並且仍是經驗豐富的 Rust 開發人員的絕佳資源。

執行 rustup update 安裝最新版本的 Rust,或者如果您尚未安裝,可以從 這裡安裝 Rust

安裝 Rust 後,您可以使用 Cargo 執行以下指令來安裝 trunk

cargo install trunk

我們還需要執行以下指令來新增 WASM 建置目標:

rustup target add wasm32-unknown-unknown

設定專案

首先,建立一個新的 cargo 專案

cargo new yew-app
cd yew-app

為了驗證 Rust 環境設定是否正確,請使用 cargo 建置工具執行基本的專案。在建置程序的輸出結果中,您應該會看到「Hello, world!」這則訊息。

cargo run

我們的首個靜態網頁

若要將這個簡單的命令列應用程式轉換成基本的 Yew 網頁應用程式,需要進行一些變更。請針對下列檔案進行更新:

Cargo.toml
[package]
name = "yew-app"
version = "0.1.0"
edition = "2021"

[dependencies]
yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] }
資訊

如果您要建立應用程式,則只需要具有 csr 功能。它將啟用 Renderer 和所有和用戶端呈現相關的程式碼。

如果您要建立程式庫,請勿啟用此功能,因為它會將用戶端呈現邏輯載入伺服器端呈現套件中。

如果您需要 Renderer 來進行測試或範例,應該在 dev-dependencies 中啟用。

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.

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

啟動開發伺服器

執行以下指令,來在本地端建置並提供應用程式服務。

trunk serve --open
資訊

移除 '--open' 選項以避免開啟您的預設瀏覽器 trunk serve

Trunk 將在您的預設瀏覽器中開啟您的應用程式、監督專案目錄,並在您修改任何原始檔時,提供協助並重建您的應用程式。如果連接埠正被其他應用程式使用,則將失敗。預設值,伺服器會在地址「127.0.0.1」和連接埠「8080」上執行 => https://127.0.0.1:8080。如欲變更,請建立下列檔案並視需要修改

Trunk.toml
[serve]
# The address to serve on LAN.
address = "127.0.0.1"
# The address to serve on WAN.
# address = "0.0.0.0"
# The port to serve on.
port = 8000

如果你有興趣,可以執行「trunk help」和「trunk help <子命令>」來了解更多詳細資訊。

恭喜

您已順利設定 Yew 開發環境,並建立了您的第一個 Yew 網路應用程式。

建立 HTML

Yew 使用 Rust 的程序巨集,並提供類似的 JSX 語法 (這是 JavaScript 的擴充,讓您可以在 JavaScript 中撰寫類似 HTML 的程式碼) 來建立標記。

轉換傳統 HTML

由於我們已經大致了解網站外觀,因此可以將我們的草稿轉譯為與 html! 相容的表示方式。如果您習慣撰寫簡單的 HTML 程式碼,那麼在 html! 中撰寫標記應該沒有問題。請務必注意,此巨集在幾個方面與 HTML 並不相同

  1. 運算式必須包含在大括弧中 ({ })
  2. 只能有一個根節點。如果您想要擁有多個元素,而不想將它們包裝在容器中,那就使用一個空標籤/片段 (<> ... </>)
  3. 元素必須適當地關閉。

我們想要建立一個佈局,其在原始 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! 的值由函式傳回

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

在標記中使用 Rust 語言建構體

在 Rust 中撰寫標記的一大優點,是可在我們的標記中充分運用 Rust 所有優點。現在,我們使用一個 VecVideo 結構來定義影片清單,而不是在 HTML 中以硬編碼的方式實作。我們在 main.rs (或我們選擇的任何檔案) 中建立一個簡單的 struct 來存放我們的資料。

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

接下來,我們將在我們的 app 函式中建立這個結構的實例,並使用它們來取代硬編碼的資料

use website_test::tutorial::Video; // replace with your own path

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 來達到此目的

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

列表項目上的鍵有助於 Yew 追蹤列表中哪些項目已變更,從而加快重新渲染的速度。強烈建議在列表中使用鍵

最後,我們需要將硬編碼的影片清單替換為我們從資料建立的 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>
+ { videos }
</div>
// ...
</>
}

元件

元件是 Yew 應用程式的建構區塊。透過組合可以由其他元件構成的元件,我們建立了自己的應用程式。透過將我們的元件結構化,以符合再利用性並保持它們的通用性,我們將能夠在應用程式的多個部分使用它們,而不需要重複程式碼或邏輯。

我們到目前為止所使用的 app 函式是一個叫做 App 的元件。它是一個「函式元件」。Yew 中有兩種不同的元件類型。

  1. 結構元件
  2. 函式元件

在本教學課程中,我們將使用函式元件。

現在,我們將我們的 App 元件拆分為較小的元件。我們從將影片清單擷取到其自己的元件開始。

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」(「屬性」的簡稱)。Props 用於將資料從父元件傳遞到子元件。在本例中,VideosListProps 是定義 props 的結構。

重要

用於 props 的結構必須透過繼承來實作 Properties

上述程式碼要能編譯起來,我們需要這樣修改 Video 結構

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

現在,我們可以更新我們的 App 元件以使用 VideosList 元件。

#[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 來達成。這個程式概念被稱為「傳遞處理常式」。我們修改其道具以接受一個 on_click 回呼函式。

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

然後,我們修改 VideosList 元件為選取的影片「發射」呼叫回傳

#[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

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 元件。

#[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,因此我們可以將它用於顯示由 Iterator 回傳的單一元素,並使用 html! 巨集支援的特殊 { for ... } 語法。

處理狀態

還記得前面用過的 use_state 嗎?它是一個特殊函式,稱為「勾子」。勾子用於「勾」住函式元件的生命週期並執行動作。你可以 在此 了解更多關於這個勾子和其他勾子的資訊。

注意事項

結構元件的動作方式有所不同。請參閱 文件 以進一步了解。

擷取資料 (使用外部 REST API)

在實際應用的中,資料通常來自 API,而不是硬編碼。我們將從外部來源擷取我們的影片清單。為此,我們需要新增以下箱子

讓我們在 Cargo.toml 檔案中更新依賴項

Cargo.toml
[dependencies]
gloo-net = "0.2"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen-futures = "0.4"
注意事項

選擇依賴項時,請確保它們與 wasm32 相容!否則您將無法執行應用程式。

更新 Video 結構以衍生 Deserialize 特徵

+ use serde::Deserialize;

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

現在,作為最後一個步驟,我們需要更新 App 組件,讓它發出提取請求,而不是使用硬式編碼資料

+ 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.dev.org.tw/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 }
</>
}
}
注意事項

我們在此使用 unwrap,因為這是一個範例應用程式。在現實世界的應用程式中,您可能希望有 正確的異常處理

現在,查看瀏覽器以查看所有內容是否如預期般運作……如果沒有 CORS,就會發生這種情況。為了解決此問題,我們需要一個代理伺服器。很幸運地,trunk 提供了代理伺服器。

更新下列程式碼行:

// ...
- let fetched_videos: Vec<Video> = Request::get("https://yew.dev.org.tw/tutorial/data.json")
+ let fetched_videos: Vec<Video> = Request::get("/tutorial/data.json")
// ...

現在,使用下列指令重新執行伺服器:

trunk serve --proxy-backend=https://yew.dev.org.tw/tutorial

重新整理標籤,所有內容都應如預期般運作。

收尾

恭喜您!您已經建立了一個網頁應用程式,可以從外部 API 中提取資料並顯示影片清單。

接下來是什麼

這個應用程式遠未達到完美或有用的地步。完成本教學課程後,您可以使用它作為起點,來探索更多進階的主題。

樣式

我們的應用程式看起來非常醜陋。沒有 CSS 或任何形式的樣式。很不幸的是,Yew 沒有提供內建的方式來調整組件樣式。請參閱 Trunk 的資產,瞭解如何新增樣式表。

更多函式庫

我們的應用程式只使用了幾個外部依賴項。市面上有很多可以用到的板條箱。請参閱 外部函式庫 以瞭解更多詳細資訊。

進一步瞭解 Yew

閱讀我們的 官方文件。它會以更詳細的方式來說明很多概念。若要更了解 Yew API,請參閱我們的 API 文件