教學
介紹
在這個實作教程中,我們將學習如何使用 Yew 建立 Web 應用程式。 Yew 是一個現代的 Rust 框架,用於使用 WebAssembly 建立前端 Web 應用程式。 Yew 透過利用 Rust 強大的類型系統,鼓勵可重複使用、可維護和良好結構化的架構。 一個龐大的社群所創造的函式庫生態系統,稱為Rust 中的crates,為常用模式(如狀態管理)提供了元件。 Rust 的套件管理器 Cargo 允許我們利用 crates.io 上提供的大量 crate,例如 Yew。
我們將要建構的內容
Rustconf 是 Rust 社群每年舉辦的星際派對。 Rustconf 2020 有大量的演講,提供了大量的資訊。 在這個實作教程中,我們將建立一個 Web 應用程序,幫助其他 Rustaceans 了解這些演講並從 一個頁面觀看它們。
設定
先決條件
這個教程假設您已經熟悉 Rust。如果您是Rust 的新手,免費的Rust 書 為初學者提供了一個很好的起點,並且即使對於有經驗的Rust 開發人員來說,它仍然是一個很好的資源。
確保安裝了最新版本的 Rust,方法是執行 rustup update
或安裝 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 web 應用程序,需要進行一些更改。
[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
中啟用它。
use yew::prelude::*;
#[function_component(App)]
fn app() -> Html {
html! {
<h1>{ "Hello World" }</h1>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
現在,讓我們在專案的根目錄中建立一個 index.html
。
<!doctype html>
<html lang="en">
<head></head>
<body></body>
</html>
啟動開發伺服器
運行以下命令建置並在本地提供應用程式。
trunk serve --open
刪除選項 '--open' 以在執行 trunk serve
後不開啟預設瀏覽器。
Trunk 將在您修改任何原始程式碼檔案時即時重新建立您的應用程式。 預設情況下,伺服器將在位址 '127.0.0.1' 的連接埠 '8080' 上監聽 => http://localhost:8080。 若要變更這部分配置,請建立以下檔案並根據需要進行編輯:
[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 有一些不同之處:
- 表達式必須用大括號(
{ }
)括起來 - 只能有一個根節點。如果您想要在不將它們包裝在容器中的情況下擁有多個元素,可以使用空標籤/片段(
<> ... </>
) - 元素必須正確關閉。
我們想要建立一個佈局,原始 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>
</>
}
刷新瀏覽器頁面,您應該看到以下輸出:
在標記中使用 Rust 語言結構
在 Rust 中編寫標記的一個很大的優勢是,我們在標記中獲得了 Rust 的所有優點。
現在,我們不再在 HTML 中硬編碼影片列表,而是將它們定義為 Vec
的 Video
結構體。
我們建立一個簡單的 struct
(在 main.rs
或我們選擇的任何檔案中)來保存我們的 資料。
struct Video {
id: usize,
title: String,
speaker: String,
url: String,
}
接下來,我們將在 app
函數中建立這個結構體的實例,並使用它們來取代硬編碼的資料:
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
來實現:
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
。它是一個「函數式元件」。
- 結構體組件
- 函數式組件
在本教程中,我們將使用函數式元件。
現在,讓我們將 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"("properties" 的縮寫)。 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
完成的。這個概念稱為「傳遞處理程序」。我們修改其 props 以接受一個 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
,所以我們可以使用特殊的{ for ... }
語法來逐個顯示Iterator
返回的唯一元素,而這