c5soft 发表于 2020-10-29 10:49
Tags:Yew,Seed
Rust前端是用Yew还是Seed, 两个都研究过的大佬出来说说,如何选择?
vue js
基于cargo-make构建yew-todomvc
使用方法: 1.安装make: cargo install cargo-make 2.将压缩包释放到某个文件夹下,并进入该文件夹 3.启动一个命令窗口,使用cargo make build构建项目 4.使用cargo make serve启动项目 5.启动另外一个命令窗口,使用cargo make watch跟踪文件变动,自动构建项目 6. 打开浏览器,地址栏输入:http://127.0.0.1:8000查看运行结果
压缩包地址:https://download.csdn.net/download/c5soft/13078373
这个项目需要一个前端,Rust 前端,有兴趣可以合作。
-- 👇 139629905: 这个和yew,seed有什么关系吗,没看到啊
-- 👇 老牛: TTstack https://github.com/ktmlm/TTstack.git
这个和yew,seed有什么关系吗,没看到啊
TTstack https://github.com/ktmlm/TTstack.git
从语法看,Yew的写法好像比Seed看起来整洁很多,同时比较todoMVC Yew这样写:
#![recursion_limit = "512"] use serde_derive::{Deserialize, Serialize}; use strum::IntoEnumIterator; use strum_macros::{EnumIter, ToString}; use yew::events::IKeyboardEvent; use yew::format::Json; use yew::services::storage::{Area, StorageService}; use yew::{html, Component, ComponentLink, Href, Html, ShouldRender}; const KEY: &'static str = "yew.todomvc.self"; pub struct Model { storage: StorageService, state: State, } #[derive(Serialize, Deserialize)] pub struct State { entries: Vec<Entry>, filter: Filter, value: String, edit_value: String, } #[derive(Serialize, Deserialize)] struct Entry { description: String, completed: bool, editing: bool, } pub enum Msg { Add, Edit(usize), Update(String), UpdateEdit(String), Remove(usize), SetFilter(Filter), ToggleAll, ToggleEdit(usize), Toggle(usize), ClearCompleted, Nope, } impl Component for Model { type Message = Msg; type Properties = (); fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self { let storage = StorageService::new(Area::Local); let entries = { if let Json(Ok(restored_model)) = storage.restore(KEY) { restored_model } else { Vec::new() } }; let state = State { entries, filter: Filter::All, value: "".into(), edit_value: "".into(), }; Model { storage, state } } fn update(&mut self, msg: Self::Message) -> ShouldRender { match msg { Msg::Add => { let entry = Entry { description: self.state.value.clone(), completed: false, editing: false, }; self.state.entries.push(entry); self.state.value = "".to_string(); } Msg::Edit(idx) => { let edit_value = self.state.edit_value.clone(); self.state.complete_edit(idx, edit_value); self.state.edit_value = "".to_string(); } Msg::Update(val) => { println!("Input: {}", val); self.state.value = val; } Msg::UpdateEdit(val) => { println!("Input: {}", val); self.state.edit_value = val; } Msg::Remove(idx) => { self.state.remove(idx); } Msg::SetFilter(filter) => { self.state.filter = filter; } Msg::ToggleEdit(idx) => { self.state.edit_value = self.state.entries[idx].description.clone(); self.state.toggle_edit(idx); } Msg::ToggleAll => { let status = !self.state.is_all_completed(); self.state.toggle_all(status); } Msg::Toggle(idx) => { self.state.toggle(idx); } Msg::ClearCompleted => { self.state.clear_completed(); } Msg::Nope => {} } self.storage.store(KEY, Json(&self.state.entries)); true } fn view(&self) -> Html<Self> { html! { <div class="todomvc-wrapper"> <section class="todoapp"> <header class="header"> <h1>{ "todos" }</h1> { self.view_input() } </header> <section class="main"> <input class="toggle-all" type="checkbox" checked=self.state.is_all_completed() onclick=|_| Msg::ToggleAll /> <ul class="todo-list"> { for self.state.entries.iter().filter(|e| self.state.filter.fit(e)).enumerate().map(view_entry) } </ul> </section> <footer class="footer"> <span class="todo-count"> <strong>{ self.state.total() }</strong> { " item(s) left" } </span> <ul class="filters"> { for Filter::iter().map(|flt| self.view_filter(flt)) } </ul> <button class="clear-completed" onclick=|_| Msg::ClearCompleted> { format!("Clear completed ({})", self.state.total_completed()) } </button> </footer> </section> <footer class="info"> <p>{ "Double-click to edit a todo" }</p> <p>{ "Written by " }<a href="https://github.com/DenisKolodin/" target="_blank">{ "Denis Kolodin" }</a></p> <p>{ "Part of " }<a href="http://todomvc.com/" target="_blank">{ "TodoMVC" }</a></p> </footer> </div> } } } impl Model { fn view_filter(&self, filter: Filter) -> Html<Model> { let flt = filter.clone(); html! { <li> <a class=if self.state.filter == flt { "selected" } else { "not-selected" } href=&flt onclick=|_| Msg::SetFilter(flt.clone())> { filter } </a> </li> } } fn view_input(&self) -> Html<Model> { html! { // You can use standard Rust comments. One line: // <li></li> <input class="new-todo" placeholder="What needs to be done?" value=&self.state.value oninput=|e| Msg::Update(e.value) onkeypress=|e| { if e.key() == "Enter" { Msg::Add } else { Msg::Nope } } /> /* Or multiline: <ul> <li></li> </ul> */ } } } fn view_entry((idx, entry): (usize, &Entry)) -> Html<Model> { let mut class = "todo".to_string(); if entry.editing { class.push_str(" editing"); } if entry.completed { class.push_str(" completed"); } html! { <li class=class> <div class="view"> <input class="toggle" type="checkbox" checked=entry.completed onclick=|_| Msg::Toggle(idx) /> <label ondoubleclick=|_| Msg::ToggleEdit(idx)>{ &entry.description }</label> <button class="destroy" onclick=|_| Msg::Remove(idx) /> </div> { view_entry_edit_input((idx, &entry)) } </li> } } fn view_entry_edit_input((idx, entry): (usize, &Entry)) -> Html<Model> { if entry.editing == true { html! { <input class="edit" type="text" value=&entry.description oninput=|e| Msg::UpdateEdit(e.value) onblur=|_| Msg::Edit(idx) onkeypress=|e| { if e.key() == "Enter" { Msg::Edit(idx) } else { Msg::Nope } } /> } } else { html! { <input type="hidden" /> } } } #[derive(EnumIter, ToString, Clone, PartialEq, Serialize, Deserialize)] pub enum Filter { All, Active, Completed, } impl<'a> Into<Href> for &'a Filter { fn into(self) -> Href { match *self { Filter::All => "#/".into(), Filter::Active => "#/active".into(), Filter::Completed => "#/completed".into(), } } } impl Filter { fn fit(&self, entry: &Entry) -> bool { match *self { Filter::All => true, Filter::Active => !entry.completed, Filter::Completed => entry.completed, } } } impl State { fn total(&self) -> usize { self.entries.len() } fn total_completed(&self) -> usize { self.entries .iter() .filter(|e| Filter::Completed.fit(e)) .count() } fn is_all_completed(&self) -> bool { let mut filtered_iter = self .entries .iter() .filter(|e| self.filter.fit(e)) .peekable(); if filtered_iter.peek().is_none() { return false; } filtered_iter.all(|e| e.completed) } fn toggle_all(&mut self, value: bool) { for entry in self.entries.iter_mut() { if self.filter.fit(entry) { entry.completed = value; } } } fn clear_completed(&mut self) { let entries = self .entries .drain(..) .filter(|e| Filter::Active.fit(e)) .collect(); self.entries = entries; } fn toggle(&mut self, idx: usize) { let filter = self.filter.clone(); let mut entries = self .entries .iter_mut() .filter(|e| filter.fit(e)) .collect::<Vec<_>>(); let entry = entries.get_mut(idx).unwrap(); entry.completed = !entry.completed; } fn toggle_edit(&mut self, idx: usize) { let filter = self.filter.clone(); let mut entries = self .entries .iter_mut() .filter(|e| filter.fit(e)) .collect::<Vec<_>>(); let entry = entries.get_mut(idx).unwrap(); entry.editing = !entry.editing; } fn complete_edit(&mut self, idx: usize, val: String) { let filter = self.filter.clone(); let mut entries = self .entries .iter_mut() .filter(|e| filter.fit(e)) .collect::<Vec<_>>(); let entry = entries.get_mut(idx).unwrap(); entry.description = val; entry.editing = !entry.editing; } fn remove(&mut self, idx: usize) { let idx = { let filter = self.filter.clone(); let entries = self .entries .iter() .enumerate() .filter(|&(_, e)| filter.fit(e)) .collect::<Vec<_>>(); let &(idx, _) = entries.get(idx).unwrap(); idx }; self.entries.remove(idx); } }
Seed这样写:
#![allow(clippy::wildcard_imports)] use seed::{prelude::*, *}; use std::collections::BTreeMap; use std::convert::TryFrom; use std::mem; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use strum_macros::EnumIter; use ulid::Ulid; const ENTER_KEY: &str = "Enter"; const ESCAPE_KEY: &str = "Escape"; const STORAGE_KEY: &str = "todos-seed"; // ------ Url path parts ------ const ACTIVE: &str = "active"; const COMPLETED: &str = "completed"; // ------ ------ // Init // ------ ------ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model { orders.subscribe(Msg::UrlChanged); Model { base_url: url.to_hash_base_url(), todos: LocalStorage::get(STORAGE_KEY).unwrap_or_default(), new_todo_title: String::new(), selected_todo: None, filter: Filter::from(url), } } // ------ ------ // Model // ------ ------ struct Model { base_url: Url, todos: BTreeMap<Ulid, Todo>, new_todo_title: String, selected_todo: Option<SelectedTodo>, filter: Filter, } #[derive(Deserialize, Serialize)] struct Todo { id: Ulid, title: String, completed: bool, } struct SelectedTodo { id: Ulid, title: String, input_element: ElRef<web_sys::HtmlInputElement>, } // ------ Filter ------ #[derive(Copy, Clone, Eq, PartialEq, EnumIter)] enum Filter { All, Active, Completed, } impl From<Url> for Filter { fn from(mut url: Url) -> Self { match url.remaining_hash_path_parts().as_slice() { [ACTIVE] => Self::Active, [COMPLETED] => Self::Completed, _ => Self::All, } } } // ------ ------ // Urls // ------ ------ struct_urls!(); impl<'a> Urls<'a> { pub fn home(self) -> Url { self.base_url() } pub fn active(self) -> Url { self.base_url().add_hash_path_part(ACTIVE) } pub fn completed(self) -> Url { self.base_url().add_hash_path_part(COMPLETED) } } // ------ ------ // Update // ------ ------ enum Msg { UrlChanged(subs::UrlChanged), NewTodoTitleChanged(String), // ------ Basic Todo operations ------ CreateTodo, ToggleTodo(Ulid), RemoveTodo(Ulid), // ------ Bulk operations ------ CheckOrUncheckAll, ClearCompleted, // ------ Selection ------ SelectTodo(Option<Ulid>), SelectedTodoTitleChanged(String), SaveSelectedTodo, } fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) { match msg { Msg::UrlChanged(subs::UrlChanged(url)) => { model.filter = Filter::from(url); } Msg::NewTodoTitleChanged(title) => { model.new_todo_title = title; } // ------ Basic Todo operations ------ Msg::CreateTodo => { let title = model.new_todo_title.trim(); if not(title.is_empty()) { let id = Ulid::new(); model.todos.insert( id, Todo { id, title: title.to_owned(), completed: false, }, ); model.new_todo_title.clear(); } } Msg::ToggleTodo(id) => { if let Some(todo) = model.todos.get_mut(&id) { todo.completed = not(todo.completed); } } Msg::RemoveTodo(id) => { model.todos.remove(&id); } // ------ Bulk operations ------ Msg::CheckOrUncheckAll => { let all_checked = model.todos.values().all(|todo| todo.completed); for todo in model.todos.values_mut() { todo.completed = not(all_checked); } } Msg::ClearCompleted => { // TODO: Refactor with `BTreeMap::drain_filter` once stable. model.todos = mem::take(&mut model.todos) .into_iter() .filter(|(_, todo)| not(todo.completed)) .collect(); } // ------ Selection ------ Msg::SelectTodo(Some(id)) => { if let Some(todo) = model.todos.get(&id) { let input_element = ElRef::new(); model.selected_todo = Some(SelectedTodo { id, title: todo.title.clone(), input_element: input_element.clone(), }); let title_length = u32::try_from(todo.title.len()).expect("title length as u32"); orders.after_next_render(move |_| { let input_element = input_element.get().expect("input_element"); input_element.focus().expect("focus input_element"); input_element .set_selection_range(title_length, title_length) .expect("move cursor to the end of input_element"); }); } } Msg::SelectTodo(None) => { model.selected_todo = None; } Msg::SelectedTodoTitleChanged(title) => { if let Some(selected_todo) = &mut model.selected_todo { selected_todo.title = title; } } Msg::SaveSelectedTodo => { if let Some(selected_todo) = model.selected_todo.take() { if let Some(todo) = model.todos.get_mut(&selected_todo.id) { todo.title = selected_todo.title; } } } } LocalStorage::insert(STORAGE_KEY, &model.todos).expect("save todos to LocalStorage"); } // ------ ------ // View // ------ ------ fn view(model: &Model) -> Vec<Node<Msg>> { nodes![ view_header(&model.new_todo_title), IF!(not(model.todos.is_empty()) => vec![ view_main(&model.todos, model.selected_todo.as_ref(), model.filter), view_footer(&model.todos, model.filter, &model.base_url), ]), ] } // ------ header ------ fn view_header(new_todo_title: &str) -> Node<Msg> { header![ C!["header"], h1!["todos"], input![ C!["new-todo"], attrs! { At::Placeholder => "What needs to be done?", At::AutoFocus => AtValue::None, At::Value => new_todo_title, }, input_ev(Ev::Input, Msg::NewTodoTitleChanged), keyboard_ev(Ev::KeyDown, |keyboard_event| { IF!(keyboard_event.key() == ENTER_KEY => Msg::CreateTodo) }), ] ] } // ------ main ------ fn view_main( todos: &BTreeMap<Ulid, Todo>, selected_todo: Option<&SelectedTodo>, filter: Filter, ) -> Node<Msg> { section![ C!["main"], view_toggle_all(todos), view_todo_list(todos, selected_todo, filter), ] } fn view_toggle_all(todos: &BTreeMap<Ulid, Todo>) -> Vec<Node<Msg>> { let all_completed = todos.values().all(|todo| todo.completed); vec![ input![ C!["toggle-all"], attrs! { At::Id => "toggle-all", At::Type => "checkbox", At::Checked => all_completed.as_at_value() }, ev(Ev::Change, |_| Msg::CheckOrUncheckAll), ], label![attrs! {At::For => "toggle-all"}, "Mark all as complete"], ] } // TODO: Remove once rustfmt is updated. #[rustfmt::skip] fn view_todo_list( todos: &BTreeMap<Ulid, Todo>, selected_todo: Option<&SelectedTodo>, filter: Filter, ) -> Node<Msg> { let todos = todos.values().filter(|todo| match filter { Filter::All => true, Filter::Active => not(todo.completed), Filter::Completed => todo.completed, }); ul![C!["todo-list"], todos.map(|todo| { let id = todo.id; let is_selected = Some(id) == selected_todo.map(|selected_todo| selected_todo.id); li![C![IF!(todo.completed => "completed"), IF!(is_selected => "editing")], el_key(&todo.id), div![C!["view"], input![C!["toggle"], attrs!{At::Type => "checkbox", At::Checked => todo.completed.as_at_value()}, ev(Ev::Change, move |_| Msg::ToggleTodo(id)), ], label![ &todo.title, ev(Ev::DblClick, move |_| Msg::SelectTodo(Some(id))), ], button![C!["destroy"], ev(Ev::Click, move |_| Msg::RemoveTodo(id)) ], ], IF!(is_selected => { let selected_todo = selected_todo.unwrap(); input![C!["edit"], el_ref(&selected_todo.input_element), attrs!{At::Value => selected_todo.title}, input_ev(Ev::Input, Msg::SelectedTodoTitleChanged), keyboard_ev(Ev::KeyDown, |keyboard_event| { Some(match keyboard_event.key().as_str() { ESCAPE_KEY => Msg::SelectTodo(None), ENTER_KEY => Msg::SaveSelectedTodo, _ => return None }) }), ev(Ev::Blur, |_| Msg::SaveSelectedTodo), ] }), ] }) ] } // ------ footer ------ fn view_footer(todos: &BTreeMap<Ulid, Todo>, selected_filter: Filter, base_url: &Url) -> Node<Msg> { let completed_count = todos.values().filter(|todo| todo.completed).count(); let active_count = todos.len() - completed_count; footer![ C!["footer"], span![ C!["todo-count"], strong![active_count], format!(" item{} left", if active_count == 1 { "" } else { "s" }), ], view_filters(selected_filter, base_url), IF!(completed_count > 0 => button![C!["clear-completed"], "Clear completed", ev(Ev::Click, |_| Msg::ClearCompleted), ] ) ] } fn view_filters(selected_filter: Filter, base_url: &Url) -> Node<Msg> { ul![ C!["filters"], Filter::iter().map(|filter| { let urls = Urls::new(base_url); let (url, title) = match filter { Filter::All => (urls.home(), "All"), Filter::Active => (urls.active(), "Active"), Filter::Completed => (urls.completed(), "Completed"), }; li![a![ C![IF!(filter == selected_filter => "selected")], attrs! {At::Href => url}, title, ],] }) ] } // ------ ------ // Start // ------ ------ #[wasm_bindgen(start)] pub fn start() { console_error_panic_hook::set_once(); let root_element = document() .get_elements_by_class_name("todoapp") .item(0) .expect("element with the class `todoapp`"); App::start(root_element, init, update, view); }
从工具链来说,Seed使用Cargo make serve/watch很爽,rust代码一修改,编译发布自动进行,很爽。
评论区
写评论vue js
基于cargo-make构建yew-todomvc
使用方法: 1.安装make: cargo install cargo-make 2.将压缩包释放到某个文件夹下,并进入该文件夹 3.启动一个命令窗口,使用cargo make build构建项目 4.使用cargo make serve启动项目 5.启动另外一个命令窗口,使用cargo make watch跟踪文件变动,自动构建项目 6. 打开浏览器,地址栏输入:http://127.0.0.1:8000查看运行结果
压缩包地址:https://download.csdn.net/download/c5soft/13078373
这个项目需要一个前端,Rust 前端,有兴趣可以合作。
--
👇
139629905: 这个和yew,seed有什么关系吗,没看到啊
--
👇
老牛: TTstack https://github.com/ktmlm/TTstack.git
这个和yew,seed有什么关系吗,没看到啊
--
👇
老牛: TTstack https://github.com/ktmlm/TTstack.git
TTstack https://github.com/ktmlm/TTstack.git
从语法看,Yew的写法好像比Seed看起来整洁很多,同时比较todoMVC Yew这样写:
Seed这样写:
从工具链来说,Seed使用Cargo make serve/watch很爽,rust代码一修改,编译发布自动进行,很爽。