今天來介紹 yew
。
cargo new yew-app
cd yew-app
cargo build
npm install
npm run build:css
trunk serve
應用程式將在 http://localhost:8080
啟動。
首頁導航
影片播放器使用
圖片載入器使用
# 建置應用程式
trunk build
# 啟動開發伺服器(自動重新載入)
trunk serve
# 建置生產版本
trunk build --release
# 監視 CSS 變更
npm run watch:css
# 建置 CSS(一次性)
npm run build:css
yew-app/
├── src/
│ ├── main.rs # 主應用程式入口和主要組件
│ ├── routes.rs # 路由定義和切換邏輯
│ └── components/ # UI 組件目錄
│ ├── mod.rs # 組件模組聲明
│ ├── navbar.rs # 導航列組件
│ ├── home.rs # 首頁組件
│ ├── video_player.rs # 影片播放器組件
│ └── image_loader.rs # 圖片載入器組件
├── index.html # HTML 模板
├── style.css # 編譯後的 CSS 樣式
├── tailwind.css # Tailwind CSS 來源
├── tailwind.config.js # Tailwind 配置
├── postcss.config.js # PostCSS 配置
├── Trunk.toml # Trunk 建置配置
├── Cargo.toml # Rust 依賴配置
└── package.json # Node.js 依賴配置
use yew::prelude::*;
use yew_router::prelude::*;
mod components;
mod routes;
/// Main application component that integrates routing and navigation
#[function_component(App)]
fn app() -> Html {
html! {
<BrowserRouter>
<components::navbar::Navbar />
<main class="w-full container mx-auto px-6 py-8">
<Switch<routes::Route> render={routes::switch} />
</main>
</BrowserRouter>
}
}
/// Application entry point
fn main() {
// Initialize logging for WASM environment
wasm_logger::init(wasm_logger::Config::default());
// Mount the main App component to the DOM
yew::Renderer::<App>::new().render();
}
use yew::prelude::*;
use yew_router::prelude::*;
/// Application routes
#[derive(Clone, Routable, PartialEq)]
pub enum Route {
#[at("/")]
Home,
#[at("/video")]
Video,
#[at("/image")]
Image,
}
/// Route switcher function that maps routes to components
pub fn switch(routes: Route) -> Html {
match routes {
Route::Home => html! { <super::components::home::HomePage /> },
Route::Video => html! { <super::components::video_player::VideoPlayer /> },
Route::Image => html! { <super::components::image_loader::ImageLoader /> },
}
}
pub mod home;
pub mod navbar;
pub mod video_player;
pub mod image_loader;
use yew::prelude::*;
use yew_router::prelude::*;
use crate::routes::Route;
/// Home page component
#[function_component(HomePage)]
pub fn home_page() -> Html {
html! {
<section class="w-full container mx-auto px-6 py-10">
<div class="bg-white rounded-2xl shadow-sm ring-1 ring-black/5 p-10 text-center">
<h1 class="text-3xl font-semibold tracking-tight">{ "歡迎使用 Yew 示範" }</h1>
<p class="mt-4 text-gray-600 leading-relaxed">
{ "工具展示:影片播放器和圖片載入器。" }
</p>
<div class="mt-8 flex flex-col sm:flex-row gap-4 justify-center">
<Link<Route>
to={Route::Video}
classes={classes!(
"inline-flex",
"items-center",
"justify-center",
"gap-2",
"text-white",
"px-5",
"py-2.5",
"rounded-lg",
"cursor-pointer",
"text-base",
"transition-colors",
"bg-blue-600",
"hover:bg-blue-700",
"no-underline",
"shadow-sm"
)}
>
{ "前往影片播放器" }
</Link<Route>>
<Link<Route>
to={Route::Image}
classes={classes!(
"inline-flex",
"items-center",
"justify-center",
"gap-2",
"text-gray-700",
"px-5",
"py-2.5",
"rounded-lg",
"cursor-pointer",
"text-base",
"transition-colors",
"border",
"border-black/10",
"hover:bg-black/5",
"no-underline"
)}
>
{ "前往圖片載入器" }
</Link<Route>>
</div>
</div>
</section>
}
}
use yew::prelude::*;
use yew_router::prelude::*;
use crate::routes::Route;
/// Navigation bar component
#[function_component(Navbar)]
pub fn navbar() -> Html {
let current_route = use_route::<Route>();
html! {
<nav class="w-full sticky top-0 z-10 backdrop-blur bg-white/70 border-b border-black/5">
<div class="container mx-auto px-4 py-3 text-center">
<Link<Route>
to={Route::Home}
classes={classes!(
"text-gray-700",
"no-underline",
"px-4",
"py-2",
"rounded-full",
"transition-colors",
"mx-1.5",
"hover:bg-black/5",
if current_route == Some(Route::Home) { "bg-gray-900 text-white hover:bg-gray-900" } else { "" }
)}
>
{ "首頁" }
</Link<Route>>
<Link<Route>
to={Route::Video}
classes={classes!(
"text-gray-700",
"no-underline",
"px-4",
"py-2",
"rounded-full",
"transition-colors",
"mx-1.5",
"hover:bg-black/5",
if current_route == Some(Route::Video) { "bg-gray-900 text-white hover:bg-gray-900" } else { "" }
)}
>
{ "影片播放器" }
</Link<Route>>
<Link<Route>
to={Route::Image}
classes={classes!(
"text-gray-700",
"no-underline",
"px-4",
"py-2",
"rounded-full",
"transition-colors",
"mx-1.5",
"hover:bg-black/5",
if current_route == Some(Route::Image) { "bg-gray-900 text-white hover:bg-gray-900" } else { "" }
)}
>
{ "圖片載入器" }
</Link<Route>>
</div>
</nav>
}
}
use yew::prelude::*;
use web_sys::{HtmlInputElement, Url};
/// Image loader component with file upload functionality
#[function_component(ImageLoader)]
pub fn image_loader() -> Html {
let image_url = use_state(|| None::<String>);
let file_input_ref = use_node_ref();
let on_file_change = {
let image_url = image_url.clone();
Callback::from(move |e: Event| {
let input: HtmlInputElement = e.target_unchecked_into();
if let Some(files) = input.files() {
if let Some(file) = files.get(0) {
if let Some(old_url) = (*image_url).as_ref() {
Url::revoke_object_url(old_url)
.unwrap_or_else(|_| log::warn!("Failed to revoke image URL"));
}
match Url::create_object_url_with_blob(&file) {
Ok(url) => image_url.set(Some(url)),
Err(err) => log::error!("Error creating image object URL: {:?}", err),
}
}
}
})
};
let trigger_file_input = {
let file_input_ref = file_input_ref.clone();
Callback::from(move |_| {
if let Some(input) = file_input_ref.cast::<HtmlInputElement>() {
input.click();
}
})
};
{
let image_url_handle = image_url.clone();
use_effect_with(image_url_handle, move |url_state_handle| {
let url_to_revoke = (**url_state_handle).clone();
move || {
if let Some(url) = url_to_revoke {
Url::revoke_object_url(&url).unwrap_or_else(|_| {
log::warn!("Failed to revoke image URL on unmount/change")
});
}
}
});
}
html! {
<div class="bg-white p-8 rounded-2xl shadow-sm ring-1 ring-black/5 w-full max-w-[720px] text-center mx-auto">
<h1 class="text-xl font-medium tracking-tight text-gray-900">{ "簡單圖片載入器" }</h1>
<input
type="file"
accept="image/*"
ref={file_input_ref}
class="hidden"
onchange={on_file_change}
/>
<button
onclick={trigger_file_input}
class="inline-flex items-center justify-center gap-2 text-white px-5 py-2.5 rounded-lg cursor-pointer text-base transition-colors bg-green-600 hover:bg-green-700 shadow-sm"
>
{ "載入圖片" }
</button>
{
if let Some(url) = (*image_url).as_ref() {
html! {
<img
src={url.clone()}
alt="已載入的圖片"
class="w-full mt-4 rounded-xl object-contain max-h-[70vh] ring-1 ring-black/10"
/>
}
} else {
html! { <p class="mt-4 text-gray-500">{ "請選擇要顯示的圖片檔案。" }</p> }
}
}
</div>
}
}
use yew::prelude::*;
use web_sys::{HtmlInputElement, Url};
/// Video player component with file upload functionality
#[function_component(VideoPlayer)]
pub fn video_player() -> Html {
let video_url = use_state(|| None::<String>);
let file_input_ref = use_node_ref();
let on_file_change = {
let video_url = video_url.clone();
Callback::from(move |e: Event| {
let input: HtmlInputElement = e.target_unchecked_into();
if let Some(files) = input.files() {
if let Some(file) = files.get(0) {
if let Some(old_url) = (*video_url).as_ref() {
Url::revoke_object_url(old_url)
.unwrap_or_else(|_| log::warn!("Failed to revoke video URL"));
}
match Url::create_object_url_with_blob(&file) {
Ok(url) => video_url.set(Some(url)),
Err(err) => log::error!("Error creating video object URL: {:?}", err),
}
}
}
})
};
let trigger_file_input = {
let file_input_ref = file_input_ref.clone();
Callback::from(move |_| {
if let Some(input) = file_input_ref.cast::<HtmlInputElement>() {
input.click();
}
})
};
{
let video_url_handle = video_url.clone();
use_effect_with(video_url_handle, move |url_state_handle| {
let url_to_revoke = (**url_state_handle).clone();
move || {
if let Some(url) = url_to_revoke {
Url::revoke_object_url(&url).unwrap_or_else(|_| {
log::warn!("Failed to revoke video URL on unmount/change")
});
}
}
});
}
html! {
<div class="bg-white p-8 rounded-2xl shadow-sm ring-1 ring-black/5 w-full max-w-[720px] text-center mx-auto">
<h1 class="text-xl font-medium tracking-tight text-gray-900">{ "簡單影片播放器" }</h1>
<input
type="file"
accept="video/*"
ref={file_input_ref}
class="hidden"
onchange={on_file_change}
/>
<button
onclick={trigger_file_input}
class="inline-flex items-center justify-center gap-2 text-white px-5 py-2.5 rounded-lg cursor-pointer text-base transition-colors bg-blue-600 hover:bg-blue-700 shadow-sm"
>
{ "載入影片" }
</button>
{
if let Some(url) = (*video_url).as_ref() {
html! {
<video
controls=true
src={url.clone()}
class="w-full mt-4 rounded-xl bg-black ring-1 ring-black/10"
>
{ "您的瀏覽器不支援影片標籤。" }
</video>
}
} else {
html! { <p class="mt-4 text-gray-500">{ "請選擇要播放的影片檔案。" }</p> }
}
}
</div>
}
}