项目界面比较简单,主要实现增删改查的核心功能为主。这个项目完全是为了学习和探索Rust的产物,Rust能写前端应该给归功于WebAssembly,之前有看过《WebAssembly 实战》这本书,不过这个是用C/C++的。
Yew这个框架可以让你完全使用Rust来写前端,不像有些例子中用其他语言写个wasm的库,然后再用JS来调用写一些东西。有点像React,这个框架似乎还不太稳定,目前还是0.20.0,之前用过他最新的Next版本,结果过了一段时间之前能跑通的项目报各种错误,所以最后锁定了0.20.0。目前关于这个框架的资料不是很多,所以如果有不懂,主要还是看官网的文档为主,另外官方的Github项目examples目录里面包含了很多例子,这个是最好的参考。
直接上项目Github地址: yew-notepad
项目目录截图:
代码主要在src目录,Cargo.toml是Rust的相关依赖的配置文件。style.css是样式文件。dist这个目录上运行后生成的,没有上传到Github。
以下主要对主要的文件作下说明。
home.rs, 主页。add.rs,添加页面。edit.rs,编辑页面。
note.rs, 模型。reppository.rs,主要处理IndexedDB的连接以及存储、更新、删除、获取等操作。
route.rs, 路由的设置。
使用Yew写界面,以home.rs为例。
use web_sys::console;
use yew::Callback;
use yew::{html, Component, Context, Html};
use yew_router::prelude::*;
use super::fetch_error::FetchError;
use super::note::Note;
use super::repository::Repository;
use super::route::Route;
pub enum FetchState<T> {
NotFetching,
Fetching,
Success(T),
Failed(FetchError),
}
async fn fetch_todo() -> Result<Vec<Note>, FetchError> {
let repository = Repository::new().await;
let todo_list = repository.list().await;
Ok(todo_list)
}
pub enum Msg {
SetDelete(u32),
SetEdit(u32),
SetTodoFetchState(FetchState<Vec<Note>>),
GetTodo,
GetError,
}
pub struct Home {
todo: FetchState<Vec<Note>>,
}
impl Home {
pub fn send_get_todo_msg(&self, ctx: &Context<Self>) {
ctx.link().send_future(async {
match fetch_todo().await {
Ok(md) => Msg::SetTodoFetchState(FetchState::Success(md)),
Err(err) => Msg::SetTodoFetchState(FetchState::Failed(err)),
}
});
ctx.link()
.send_message(Msg::SetTodoFetchState(FetchState::Fetching));
}
}
impl Component for Home {
type Message = Msg;
type Properties = ();
fn create(ctx: &Context<Self>) -> Self {
let home = Self {
todo: FetchState::NotFetching,
};
home.send_get_todo_msg(ctx);
return home;
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::SetTodoFetchState(fetch_state) => {
self.todo = fetch_state;
true
}
Msg::GetTodo => {
ctx.link().send_future(async {
match fetch_todo().await {
Ok(md) => Msg::SetTodoFetchState(FetchState::Success(md)),
Err(err) => Msg::SetTodoFetchState(FetchState::Failed(err)),
}
});
ctx.link()
.send_message(Msg::SetTodoFetchState(FetchState::Fetching));
false
}
Msg::GetError => {
ctx.link().send_future(async {
match fetch_todo().await {
Ok(md) => Msg::SetTodoFetchState(FetchState::Success(md)),
Err(err) => Msg::SetTodoFetchState(FetchState::Failed(err)),
}
});
ctx.link()
.send_message(Msg::SetTodoFetchState(FetchState::Fetching));
false
}
Msg::SetEdit(id) => {
console::log_1(&id.into());
let history1 = ctx.link().navigator().unwrap();
history1.push(&Route::Edit { id: id.clone() });
false
}
Msg::SetDelete(id) => {
console::log_1(&id.into());
ctx.link().send_future(async move {
let repository = Repository::new().await;
repository.delete_note(id.clone());
match fetch_todo().await {
Ok(md) => Msg::SetTodoFetchState(FetchState::Success(md)),
Err(err) => Msg::SetTodoFetchState(FetchState::Failed(err)),
}
});
ctx.link()
.send_message(Msg::SetTodoFetchState(FetchState::Fetching));
false
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let history = ctx.link().navigator().unwrap();
let onclick = Callback::from(move |_| history.push(&Route::Add));
match &self.todo {
FetchState::NotFetching => html! {
<>
<button onclick={ctx.link().callback(|_| Msg::GetTodo)}>
{ "Get Markdown" }
</button>
<button onclick={ctx.link().callback(|_| Msg::GetError)}>
{ "Get using incorrect URL" }
</button>
</>
},
FetchState::Fetching => html! { "Fetching" },
FetchState::Success(data) => || -> Html {
html! {
<div>
<h1>{ "记事本" }</h1>
<div style="margin: 10px 10px 0 0;"><button {onclick}>{ "添加" }</button></div>
<div>
{
data.into_iter().map(|note| {
let id = note.id.unwrap();
html!{
<div class="note-container">
<div>{note.content.clone()}</div>
<div class="bottom-bar">
<div>{note.create_time.clone()}</div>
<button class="my-button" onclick={ctx.link().callback(move|_|Msg::SetEdit(id))}>{"编辑"}</button>
<button class="my-button" onclick={ctx.link().callback(move|_|Msg::SetDelete(id))}>{"删除"}</button>
</div>
</div>
}
}).collect::<Html>()
}
</div>
</div>
}
}(),
FetchState::Failed(err) => html! { err },
}
}
}
可以发现界面的主要代码在view这个里面,可以直接写html标签,这点有点类似于Vue。因为我们获取笔记是异步的,所以响应了几个事件。
存储部分我没有使用常用的localStorage,而是用了IndexedDB,因为这个是异步的,所以在一开始时候,这块卡了半天,在JS看来写一个这样的东西问题不大,但是用Rust写完全是另一种思路。
我们可以看下连接IndexedDB的代码,全部代码见reppository.rs。
pub async fn new() -> Repository {
let (tx, rx) = oneshot::channel::<IdbDatabase>();
let window = web_sys::window().unwrap();
let idb_factory = window.indexed_db().unwrap().unwrap();
let open_request = idb_factory.open_with_u32(DB_NAME, 1).unwrap();
let on_upgradeneeded = Closure::once(move |event: &Event| {
let target = event.target().expect("Event should have a target; qed");
let req = target
.dyn_ref::<IdbRequest>()
.expect("Event target is IdbRequest; qed");
let result = req
.result()
.expect("IndexedDB.onsuccess should have a valid result; qed");
assert!(result.is_instance_of::<IdbDatabase>());
let db = IdbDatabase::from(result);
// let _store: IdbObjectStore = db.create_object_store(&String::from("user")).unwrap();
let mut parameters: IdbObjectStoreParameters = IdbObjectStoreParameters::new();
parameters.auto_increment(true);
parameters.key_path(Some(&JsValue::from_str(String::from("id").as_str())));
let _store =
db.create_object_store_with_optional_parameters(&String::from("note"), ¶meters);
// let _index = store
// .create_index_with_str(&String::from("name"), &String::from("name"))
// .expect("create_index_with_str error");
});
open_request.set_onupgradeneeded(Some(on_upgradeneeded.as_ref().unchecked_ref()));
on_upgradeneeded.forget();
let on_success = Closure::once(move |event: &Event| {
// Extract database handle from the event
let target = event.target().expect("Event should have a target; qed");
let req = target
.dyn_ref::<IdbRequest>()
.expect("Event target is IdbRequest; qed");
let result = req
.result()
.expect("IndexedDB.onsuccess should have a valid result; qed");
assert!(result.is_instance_of::<IdbDatabase>());
let db = IdbDatabase::from(result);
let _ = tx.send(db);
});
open_request.set_onsuccess(Some(on_success.as_ref().unchecked_ref()));
on_success.forget();
let db = rx.await.unwrap();
Repository { db }
}
总结
之前一直看《Rust权威指南》这本书,但是自从写了这个后,发现还是《Rust程序设计》这本书更给力,尤其是Rust的内存管理那块。
虽然是个小东西,但是写的很卡。其实Rust的语法并不是很难,难点在于他的内存管理那块,尤其是习惯了垃圾回收这类语言后,顿不顿编译报错。另外用Rust写前端,运行环境编程浏览器,所以很多东西跟你写一般的操作系统应用是 不一样的。最后希望跟大家一块交流下,代码其实也比较乱,大家一块探索。