0%

Rust写前端

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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事件中处理。

1
2
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这个库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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的用法问题,最后终于可以把数据成功插入了,虽然再次插入数据时会有写入失败的问题,不过这个已经是后面的事了。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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是未来全栈程序员必备的语言,这门语言是非常考验你是不是一个合格的程序员,因为他既有很底层的东西,比如内存概念,也有很现代的语法,但前提是你需要都懂,对于那些不是科班出身又没有好好学习计算机相关的东西人来说,这个确实有难度。