教程
简介
在本实践教程中,我们将了解如何使用 Yew 构建 Web 应用程序。Yew 是一个现代 Rust 框架,用于使用 WebAssembly 构建前端 Web 应用程序。Yew 通过利用 Rust 强大的类型系统,鼓励可重用、可维护且结构良好的架构。一个由社区创建的庞大库生态系统(在 Rust 中称为 crates)为常用模式(例如状态管理)提供组件。Rust 的包管理器 Cargo 让我们能够利用 crates.io 上提供的众多 crates,例如 Yew。
我们将构建的内容
Rustconf 是 Rust 社区的年度星际聚会。Rustconf 2020 提供了大量信息丰富的演讲。在本动手教程中,我们将构建一个 Web 应用程序,帮助 Rustaceans 了解演讲并在一页上观看所有演讲。
设置
先决条件
本教程假设您已熟悉 Rust。如果您是 Rust 新手,免费的 Rust Book 为初学者提供了绝佳的起点,即使对于经验丰富的 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 Web 应用程序,需要进行一些更改。按如下方式更新文件
[package]
name = "yew-app"
version = "0.1.0"
edition = "2021"
[dependencies]
yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] }
如果您正在构建应用程序,则只需要 csr
功能。它将启用 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]
# 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 <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; // 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 中有两种不同类型的组件。
- 结构组件
- 函数组件
在本教程中,我们将使用函数组件。
现在,让我们将 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 的结构。
用于属性的结构必须通过派生实现 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,而不是硬编码。让我们从外部来源获取我们的视频列表。为此,我们需要添加以下板条箱
gloo-net
用于进行获取调用。serde
具有派生功能,用于反序列化 JSON 响应wasm-bindgen-futures
用于将 Rust Future 作为 Promise 执行
让我们更新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.rust-lang.net.cn/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.rust-lang.net.cn/tutorial/data.json")
+ let fetched_videos: Vec<Video> = Request::get("/tutorial/data.json")
// ...
现在,使用以下命令重新运行服务器
trunk serve --proxy-backend=https://yew.rust-lang.net.cn/tutorial
刷新选项卡,一切应按预期工作。
总结
恭喜!您已经创建了一个从外部 API 获取数据并显示视频列表的 Web 应用程序。
下一步
此应用程序远非完美或有用。完成本教程后,您可以将其用作探索更高级主题的起点。
样式
我们的应用程序看起来很丑陋。没有 CSS 或任何类型的样式。不幸的是,Yew 并未提供内置的方式来设置组件样式。请参阅 Trunk 的资产 以了解如何添加样式表。
更多库
我们的应用程序仅使用了一些外部依赖项。有很多可用的板条箱。请参阅 外部库 了解更多详情。