dioxus web やってみる
dioxus は Rust のGUIアプリケーションフレームワーク。
React を模した仮想DOM の APIで、 desktop アプリや wasm 吐き出しができる。
公式チュートリアルは desktop 版だったのでブラウザ版を探したら、たどり辛いところにあった。
Setup
# rust や cargo のセットアップは略
$ cargo install trunk
$ rustup target add wasm32-unknown-unknown
# 公式ドキュメントになにもないが、 cargo add コマンドは cargo-edit が必要
# rust ユーザーなら常識かもだが、久しぶりなので知らなかった…
$ cargo install cargo-edit
# bundler として trunk というビルダーをインストール
$ cargo install --locked trunk
trunk も知らなかった。
API としては vite や parcel みたいに index.html
を入力にとり、SPA を出力するバンドラ。
cargo でプロジェクトを作成
$ cargo new --bin demo && cd demo
$ cargo add dioxus --features web
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" name="viewport" content="width=device-width, initial-scale=1.0" charset="UTF-8">
</head>
<body>
<div id="main"> </div>
</body>
</html>
rust のコード
use dioxus::prelude::*;
fn main() {
dioxus::web::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx!{
div { "hello, wasm!" }
})
}
rsx! のマクロで JSX のようなDOMツリーを宣言する。
この状態で trunk serve
コマンドを叩くと http://localhost:8080 に開発用サーバーが立つ
ビルド
リリースの際は --release
ビルドを行う
$ trunk build --release
dist に出力される
$ ls -lh dist/* | awk '{print $9,$5}'
dist/index-11dc12392903d87.js 35K
dist/index-11dc12392903d87_bg.wasm 315K
dist/index.html 463B
Hello World 時点でビルドサイズは315K, gzip して 105K ほどだった。
巨大すぎるほどというわけではないが、ライブラリとして提供したり、普通のウェブサイトに埋め込むには厳しい。
Counter の実装
use dioxus::prelude::*;
fn main() {
dioxus::web::launch(app);
}
fn app(cx: Scope) -> Element {
let mut count = use_state(&cx, || 0);
cx.render(rsx!{
div {}, "count: { count }"
button { onclick: move |_| count += 1, "++" }
})
}
いろいろな rsx
use dioxus::prelude::*;
fn main() {
dioxus::web::launch(app);
}
#[derive(Props, PartialEq)]
struct MyProps<'a> {
name: &'a str
}
fn MyComponent<'a>(cx: Scope<'a, MyProps<'a>>) -> Element {
cx.render(rsx! {
div {
"sub"
}
})
}
fn app(cx: Scope) -> Element {
let mut count = use_state(&cx, || 0);
let user_name = Some("bob");
let names = ["jim", "bob", "jane", "doe"];
let name_list = names.iter().map(|name| rsx!(
li { "{name}" }
));
let el = rsx!(
div {
user_name.map(|name| rsx!{
h1 { "Hello, world! {name}" }
})
name_list
}
);
// let sub_app = sub(cx);
cx.render(rsx!{
// nested
div {
h1 { "Counter" }
// bind
"count: { count }"
button { onclick: move |_| count += 1, "+1" }
}
hr {}
div {
el
}
MyComponent {
name: "bob"
}
})
}
インライン要素
#![allow(unused, non_upper_case_globals, non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::web::launch(app);
}
fn app(cx: Scope) -> Element {
let el = rsx!(
div {
user_name.map(|name| rsx!{
h1 { "Hello, world! {name}" }
})
name_list
}
);
cx.render(rsx!{
el
})
}
外部コンポーネント定義
#![allow(unused, non_upper_case_globals, non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::web::launch(app);
}
// Component
#[derive(Props, PartialEq)]
struct MyProps<'a> {
name: &'a str
}
fn MyComponent<'a>(cx: Scope<'a, MyProps<'a>>) -> Element {
cx.render(rsx! {
div {
"my_component: {cx.props.name}"
}
})
}
fn app(cx: Scope) -> Element {
cx.render(rsx!{
MyComponent {
name: "bob"
}
})
}
Hooks
React の useState 相当のローカルステートがある。
fn Example(cx: Scope) -> Element {
let name = use_state(&cx, || "...".to_string());
cx.render(rsx!(
"YN: {name}"
div {
button { onclick: move |_| name.set("yes".to_string()), "yes"}
button { onclick: move |_| name.set("no".to_string()), "no"}
}
))
}
グローバルステート。中身は実質 Arc?
#![allow(unused, non_upper_case_globals, non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::web::launch(app);
}
struct GlobalState { name: String }
fn Leaf(cx: Scope) -> Element {
match use_context::<GlobalState>(&cx) {
Some(state) => {
let name = &state.read().name;
cx.render(rsx! {
div {
"Leaf: {name}"
}
})
},
None => {
cx.render(rsx! {
div {
"Leaf: no state"
}
})
}
}
}
fn app(cx: Scope) -> Element {
use_context_provider(&cx, || GlobalState { name: String::from("Toby") });
let count = use_state(&cx, || 0);
cx.render(rsx!{
Leaf {}
})
}
Rc や Arc ってスレッド周りの処理が入ってビルド時に処理が膨らむ気がしたが、ビルドしてみたところ .wasm が331k。さして増えるわけではなさそう。
他人のスクラップに書き込むのは恐縮なのですが、僕もdioxusやRust Frontendを調査したりしてる部分があるので共有させていただきます。(ノイズになるなら削除してください)
Rc や Arc ってスレッド周りの処理が入ってビルド時に処理が膨らむ気がしたが、ビルドしてみたところ .wasm が331k。さして増えるわけではなさそう。
Rcは実はマルチスレッドとは実はそんなに関係がなかったりします。
RcはC++で言うところのshared_pointerに相当して、複数の所有者を持つことが出来るHeap上のデータになっています。(Rcに対しての対はBoxです)
Arcはthreadの間をまたぐことが出来るように実装されたRcの実装になっています。(これも、マルチスレッドの実行系とはそこまで関係は無いです)
WIP
use_future
公式サンプルは reqwest を使ったものだが、 web_sys で fetch を使ったものにしてみる。
まずそのまま fetch を実装。
Cargo.toml に web_sys と serde の依存を追加
[package]
name = "demo"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
dioxus = { version = "0.1.6", features = ["web"] }
wasm-logger = "0.2.0"
wasm-bindgen = { version = "0.2.79", features = ["serde-serialize"] }
log = "0.4.14"
console_error_panic_hook = "*"
im-rc = "15.0.0"
wasm-bindgen-futures = "0.4.29"
serde = { version = "1.0.80", features = ["derive"] }
serde_derive = "^1.0.59"
[dependencies.web-sys]
version = "0.3.4"
features = [
'Headers',
'Request',
'RequestInit',
'RequestMode',
'Response',
'Window'
]
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};
/// A struct to hold some data from the github Branch API.
///
/// Note how we don't have to define every member -- serde will ignore extra
/// data when deserializing
#[derive(Debug, Serialize, Deserialize)]
pub struct Branch {
pub name: String,
pub commit: Commit,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Commit {
pub sha: String,
pub commit: CommitDetails,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CommitDetails {
pub author: Signature,
pub committer: Signature,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Signature {
pub name: String,
pub email: String,
}
// #[wasm_bindgen]
pub async fn fetch(repo: String) -> Result<JsValue, JsValue> {
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::Cors);
let url = format!("https://api.github.com/repos/{}/branches/master", repo);
let request = Request::new_with_str_and_init(&url, &opts)?;
request
.headers()
.set("Accept", "application/vnd.github.v3+json")?;
let window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
// `resp_value` is a `Response` object.
assert!(resp_value.is_instance_of::<Response>());
let resp: Response = resp_value.dyn_into().unwrap();
// Convert this other `Promise` into a rust `Future`.
let json = JsFuture::from(resp.json()?).await?;
// Use serde to parse the JSON into a struct.
let branch_info: Branch = json.into_serde().unwrap();
// Send the `Branch` struct back to JS as an `Object`.
Ok(JsValue::from_serde(&branch_info).unwrap())
}
これを main.rs から使う。
#![allow(unused, non_upper_case_globals, non_snake_case)]
use dioxus::prelude::*;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::spawn_local;
use web_sys::console::{log, log_1};
fn main() {
log_1(&JsValue::from_str("Hello, world!"));
dioxus::web::launch(app);
}
fn app(cx: Scope) -> Element {
let x = use_future(&cx, move || demo::fetch("mizchi/mints".to_string()));
cx.render(rsx!{
div {
// TODO: x を参照する。
}
})
}
430k ぐらいに増えた。おそらく serde シリアライザの分
他人のスクラップに書き込むのは恐縮なのですが、僕もdioxusやRust Frontendを調査したりしてる部分があるので共有させていただきます。(ノイズになるなら削除してください)
公式サンプルは reqwest を使ったものだが、 web_sys で fetch を使ったものにしてみる。
ビルド時にターゲットをみて自動的に切り替わる設定になっているのでreqwest
は基本的にはwasm + web-sys上でも動きます。(試してみるっていう話でやってたならすいません)
僕がyewでwebアプリを書いたときは普通にreqwestでリクエスト送れていました。
useFuture で useEffect 相当の処理をする
#![allow(non_snake_case)]
use dioxus::prelude::*;
use wasm_bindgen::prelude::*;
use web_sys::console;
fn main() {
dioxus::web::launch(App);
}
fn App(cx: Scope) -> Element {
// console.log("[render]");
console::log_1(&"[render]".into());
// const [count, setCount] = useState(0);
let count = use_state(&cx, || 0 as i32);
// const [resource, setResource] = useState(0);
let resource = use_state(&cx, || 0 as i32);
// const countHandler = () => { setCount(count + 1); };
let count_handler = move |_| {
let text = format!("count_handler {}", count.get());
web_sys::console::log_1(&JsValue::from_str(&text));
count.set(count.get() + 1);
};
// const resource_handler = () => { if(count > 0) { setCount(count - 1); setResource(resource + 1) } };
let resource_handler = move |_| {
if *count.get() > 0 {
count.set(count.get() - 1);
resource.set(resource.get() + 1);
}
};
// useEffect(() => { console.log("onmount") }, [])
use_future(&cx, (), |()| async move {
console::log_1(&"on mount".into());
});
// useEffect(() => { console.log("onmount") }, [count])
use_future(&cx, count, |count| async move {
console::log_1(&format!("count:changed {}", count.get()).into());
});
let resource_disabled = if *count.get() > 0 { "false" } else { "true" };
/*
return <div>
<h1>Counter</h1>
<div>
count: {count}
<button onClick={countHandler}>+1</button>
</div>
<div>
resource: {resource}
<button disabled={resource_disabled} onClick={resource_handler}>
consume counter
</button>
</div>
</div>
*/
cx.render(rsx!{
div {
h1 { "Counter" }
div {
"count: { count }"
button { onclick: count_handler, "+1" }
}
div {
"resource: { resource }"
button {
onclick: resource_handler,
disabled: "{ resource_disabled }",
"consume counter"
}
}
}
})
}
React エンジニアのための dioxus 入門みたいなの書くといい気がしてきた。
web-sys から setTimeout を呼ぼうとしてみたがだいぶだるい
/*
useEffect(() => {
setTimeout(() => {
console.log("timeout");
}, 100);
}, []);
*/
use_future(&cx, (), |()| async move {
let callback = Closure::wrap(
Box::new(
move || console::log_1(&"timeout".into()),
) as Box<dyn Fn()>
);
let _ = web_sys::window().unwrap().set_timeout_with_callback_and_timeout_and_arguments_0(
callback.as_ref().unchecked_ref(),
100
);
callback.forget();
});