用Rust写前端记事本应用

395 阅读4分钟

项目界面比较简单,主要实现增删改查的核心功能为主。这个项目完全是为了学习和探索Rust的产物,Rust能写前端应该给归功于WebAssembly,之前有看过《WebAssembly 实战》这本书,不过这个是用C/C++的。

Yew这个框架可以让你完全使用Rust来写前端,不像有些例子中用其他语言写个wasm的库,然后再用JS来调用写一些东西。有点像React,这个框架似乎还不太稳定,目前还是0.20.0,之前用过他最新的Next版本,结果过了一段时间之前能跑通的项目报各种错误,所以最后锁定了0.20.0。目前关于这个框架的资料不是很多,所以如果有不懂,主要还是看官网的文档为主,另外官方的Github项目examples目录里面包含了很多例子,这个是最好的参考。

直接上项目Github地址: yew-notepad

项目目录截图:

WX20230618-173236@2x.png

代码主要在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"), &parameters);
            // 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写前端,运行环境编程浏览器,所以很多东西跟你写一般的操作系统应用是 不一样的。最后希望跟大家一块交流下,代码其实也比较乱,大家一块探索。