Rust写前端

尝试用Rust已经有点时间了,之前主要的问题是卡在IndexedDB的使用上,最后的代码是这样。

spawn_local(async move {
        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(String::from("todo").as_str(), 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 _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();
        let transaction = db.transaction_with_str_and_mode(&String::from("user"), IdbTransactionMode::Readwrite).expect("transaction_with_str error");
        let store = transaction.object_store(&String::from("user")).expect("store error");

        let name = JsValue::from_str(_content_element.value().as_str());
        let add_request = store.add_with_key(&name, &JsValue::from("name")).expect("add error");

        let on_add_error = Closure::once(move |event: &Event| {
            console::log_1(&String::from("写入数据失败").into());
            console::log_1(&event.into());
        });
        add_request.set_onerror(Some(on_add_error.as_ref().unchecked_ref()));
        on_add_error.forget();

        let on_add_success = Closure::once(move |event: &Event| {
            console::log_1(&String::from("写入数据成功").into());
        });
        add_request.set_onsuccess(Some(on_add_success.as_ref().unchecked_ref()));
        on_add_success.forget();

        console::log_1(&String::from("do").into());
 });

因为IndexedDB连接成功是个异步事件,db 只能在成功事件中才能拿到,最初因为对IndexedDB的使用不熟悉,这玩意在很早以前看HTML5的时候就看到过,不过一直没有用过。我在成功事件中调用create_object_store,结果给报错了,实际上创建数据库只能在onupgradeneeded事件中处理。

let myDatabase = MyDatabase::new();
myDatabase.add(String::from("jasper", String::from("name")));

早期的想法是把数据库的操作封装起来,但是连接是异步的,上面的add操作时数据库可能还没有连接成功,db等于为空。可能是之前做iOS的缘故,我一直想着数据库的操作应该是个单例,不过Rust的单例好像不那么简单,一直报错,所以这个想法就先放放了。

早期的时候甚至不知道如何用Rust设置回调,最后发现了这个库kvdb_web,代码也是参考了indexed_db.rs中的方法。这个库并没有演示的例子,所以只是参考了他打开数据库的方法。里面有用到futures::channel,但是实际使用老是报错。后来我忘了Rust写前端,实际上是运行在Wasm虚拟机的环境下,Rust的多线程则是一般的操作系统的环境下了。实际上要在Wasm使用多线程,得用另外一种方式。实际上是用Rust的代码调用JS了,JS是单线程运行的。所以整个思路虽然是用Rust在写,但实际思想的话得跟着JS来走。我想在后面的代码中拿到db,有点类似JS中使用await的方式。最后用了两个库,wasm-bindgen-futures主要使用了spawn_local这个东西,另外还有futures-channel这个库。

spawn_local(async {
	let (tx, rx) = oneshot::channel::<i32>();
	// 省略中间代码
	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();
    // 省略剩余代码
})

futures-channel这个库的用法跟Rust自带那个有点类似,不过只用这个是可以工作的。后面的问题基本上是IndexedDB的用法问题,最后终于可以把数据成功插入了,虽然再次插入数据时会有写入失败的问题,不过这个已经是后面的事了。

当我费了很多周折,成功写入时,也就是写这篇文章时候,突然间发现可以直接用下面的方式写,如果只是要写入数据的话。

let window = web_sys::window().unwrap();
let idb_factory = window.indexed_db().unwrap().unwrap();

let open_request = idb_factory
    .open_with_u32(String::from("todo").as_str(), 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 _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 transaction = db
        .transaction_with_str_and_mode(&String::from("user"), IdbTransactionMode::Readwrite)
        .expect("transaction_with_str error");
    let store = transaction
        .object_store(&String::from("user"))
        .expect("store error");

    let name = JsValue::from_str(_content_element.value().as_str());
    let add_request = store
        .add_with_key(&name, &JsValue::from("name"))
        .expect("add error");

    let on_add_error = Closure::once(move |event: &Event| {
        console::log_1(&String::from("写入数据失败").into());
        console::log_1(&event.into());
    });
    add_request.set_onerror(Some(on_add_error.as_ref().unchecked_ref()));
    on_add_error.forget();

    let on_add_success = Closure::once(move |event: &Event| {
        console::log_1(&String::from("写入数据成功").into());
    });
    add_request.set_onsuccess(Some(on_add_success.as_ref().unchecked_ref()));
    on_add_success.forget();
});
open_request.set_onsuccess(Some(on_success.as_ref().unchecked_ref()));
on_success.forget();

总结

至此算是把IndexedDB的问题解决了,如果想把操作封装起来,那么可以用前面的方式。只是简单的操作的话,后面的代码也是可以的。到目前为止,基本上还没有写网页的东西,Yew的东西,也就只是搭了个架子时有用到,并没有深入的使用。后面得需要好好的深入下,Yew这个框架跟传统的还不太一样,用trunk serve启动后,实际会有一个WebSocket在前后端之间通讯。

再谈谈Rust学习的东西,实际上《Rust权威指南》这本书我只是看了个大概,而目前的实验项目中并没有使用Rust去写很多逻辑的代码,更多的是调用。用Rust写前端这个方式,起步会比一般的难点,主要还是牵涉的东西不只是Rust的问题,还有Wasm以及JS相关的东西,最重要的还是要明白他的运行环境是浏览器中Wasm虚拟机。不过后续如果有大量成熟的库,可以解决这些基础问题的话,只写网页的话,难度会小些。

虽然项目整体进展不大,也走了一些弯路,不过却也让我思考到了更多的东西。现在感觉Rust是未来全栈程序员必备的语言,这门语言是非常考验你是不是一个合格的程序员,因为他既有很底层的东西,比如内存概念,也有很现代的语法,但前提是你需要都懂,对于那些不是科班出身又没有好好学习计算机相关的东西人来说,这个确实有难度。