【Rust Yew web-sys】初学向 Yewでinputへの入力内容取得、及び処理結果の表示
Yew とweb -sys でのWeb 開発
先人が
WebAssembly とYew
私が挫折した原因の一は、手法の不定にあった。
wat2wasm simple.wat -o simple.wasm
一方、
wasm-pack build --target web
次の記事では、こうした手順を少しでも簡便にするべく、
そして
trunk serve
ここまで論いた事柄から、
Yew の問題
実行方法
先ずは
1
例
use wasm_bindgen::prelude::*;
use yew::prelude::*;
struct Model {
link: ComponentLink<Self>,
value: i64,
}
enum Msg {
AddOne,
}
impl Component for Model {
type Message = Msg;
type Properties = ();
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
link,
value: 0,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::AddOne => self.value += 1
}
true
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
// Should only return "true" if new properties are different to
// previously received properties.
// This component has no properties so we will always return "false".
false
}
fn view(&self) -> Html {
html! {
<div>
<button onclick={self.link.callback(|_| Msg::AddOne)}>{ "+1" }</button>
<p>{ self.value }</p>
</div>
}
}
}
#[wasm_bindgen(start)]
pub fn run_app() {
App::<Model>::new().mount_to_body();
}
wasm-pack build --target web --out-name wasm --out-dir ./static
miniserve ./static --index index.html
2
例
use yew::prelude::*;
#[function_component(App)]
fn app() -> Html {
html! {
<h1>{ "Hello World" }</h1>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
trunk serve --open
実行方法は言わずもがな、プログラムのfn main()
内を見ても、二者が全く異なる手法で実行しようとしていることが分かるだろう。Cargo.toml
をも引用する。
- 例
1 Cargo.toml[package] name = "yew-app" version = "0.1.0" authors = ["Yew App Developer <name@example.com>"] edition = "2018" [lib] crate-type = ["cdylib", "rlib"] [dependencies] yew = "0.17" wasm-bindgen = "0.2"
- 例
2 Cargo.toml[package] name = "yew-app" version = "0.1.0" edition = "2021" [dependencies] yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] }
上はyew = "0.17"
とあるから、
一方で下にはバージョンを示す数値がなく、直接
この違いは極めて重要である。にも関わらず、両者が混在しているため、私のような初学の混乱を招くのである。
爰に、本記事では例
開発手法
ここで一度、本記事の表題を確認しておく。
「
具体的且つ基本的な課題は、次の三点である。
-
で、画面上の要素や、内容を取得できること。Rust -
で、画面上に何らかの内容を表示できること。Rust -
で、Rust を定義できること。EventListner
このような
EventListnerとは
<input type = "number" id = "inputArea" />
<button id = "button" >button</button>
<p id = "outputArea" ></p>
<script>
/* buttonを押すとinputの内容を`<p id = "outputArea" ></p>`に表示する */
document.getElementById("button") // `<button id = "button" >button</button>`を取得
.addEventListner(
"click", // clickに対応させる
/* 以下の無名関数を連係する */
() => {
/* `<input type = "number" id = "inputArea" />`の値を取得する */
const inputValue = document.getElementById("inputArea").value;
/* `<p id = "outputArea" ></p>`の内容を上書きする */
document.getElementById("outputArea").textContent = inputValue;
}
);
</script>
参考:
#[derive(Properties, PartialEq)]
pub struct Props {
pub callback: Callback<String, String>,
}
#[function_component(MyComponent)]
fn my_component(props: &Props) -> Html {
let greeting = props.callback.emit("Yew".to_string());
html! {
<>{ &greeting }</>
}
}
#[function_component(UseCallback)]
fn callback() -> Html {
let counter = use_state(|| 0);
let onclick = {
let counter = counter.clone();
Callback::from(move |_| counter.set(*counter + 1))
};
// This callback depends on (), so it's created only once, then MyComponent
// will be rendered only once even when you click the button multiple times.
let callback = use_callback((), move |name, _| format!("Hello, {}!", name));
// It can also be used for events, this callback depends on `counter`.
let oncallback = use_callback(counter.clone(), move |_e, counter| {
let _ = **counter;
});
html! {
<div>
<button {onclick}>{ "Increment value" }</button>
<button onclick={oncallback}>{ "Callback" }</button>
<p>
<b>{ "Current value: " }</b>
{ *counter }
</p>
<MyComponent {callback} />
</div>
}
}
始めに私怨を込めて雑駁と指摘するのは、この例には関数の定義の仕方しか記述されておらず、「これをどのように実行するのか」には全く触れられていない点である。吾が私怨に反駁するとすれば、「逐一実行方法まで触れる必要はない」のだが、先述の通り
本旨に戻ると、この例の中には
Callback::from()
use_callback()
二者の差異はcounter.clone()
の位置であり、使い勝手が異なる。
また、counter
はuse_state()
によって定義されている。これをcounter.set()
で値を変じると、
上の例は、このような考えに基づいて組まれたプログラムである。これにより、「画面上の値の更新」をすることは出来る。しかしながら、
JavaScript とweb -sys
本記事では、
unwrap()
を付する必要がある点である。次を比較せよ。
document.getElementById("ID")
use web_sys::window;
window().unwrap().document().unwrap().get_element_by_id("ID").unwrap()
型の推移
Option<>
の被覆を除去して中を取り出すのが、ここでのunwrap()
の働きと言える。
camel caseとsnake case
unwrap()
とは、「失敗し得る処理」に付される関数であるが、ここでは、これが無いと実行できないから付しているに過ぎない。但し、処理が失敗していた場合には「プログラム全体が停止するため危険」である。停止されては困る場合、unwrap_or()
等で、「失敗したらどう対処するか」を定義する必要がある。
その他、
本題
今更だが、
今回作成するのは、
- 数値を入力する
-
を押すと、数値を取得し、計算するbutton - 計算結果を表示する
これらの簡単な例として、
BMIとは
体重を
肥瘠判定の指標として世界的に用いられており、日本肥満学会は、
Rust project を作り、Yew とweb -sys を追加する
cargo new bmi_calculator
加えてindex.html
を作成する。例として、内容は以下のようにする。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>BMI</title>
</head>
<body>
</body>
</html>
今、
web -sys の追加
cargo add web-sys
初回ならば、これにより、自動的にCargo.lock
が生成される。追加された
また、Cargo.toml
に次のような内容が追加される。今回のバージョンは
[dependencies]
web-sys = "0.3.66"
Yew の追加
Cargo.toml
に追記する。
[dependencies]
web-sys = "0.3.66"
yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] }
featuresについて
先述した通り、本記事では作成したプログラムを次のように実行する。
fn main() {
yew::Renderer::<App>::new().render();
}
このyew::Renderer
についての
Available on crate feature csr only.
この記述にある通り、yew::Renderer
を使用するには、csr
Rust を記述する
完成は以下に示す。
use yew::prelude::*;
use web_sys::{
window,
HtmlInputElement,
wasm_bindgen::JsCast
};
/* clone()を使うために`PartialEq`を付与する。 */
#[derive(PartialEq)]
struct BMI {
bmi_value: String,
} impl BMI {
/* constructor */
fn new_empty() -> Self {
BMI {
bmi_value: "".to_owned()
}
}
fn new_value(value: f32) -> Self {
let string_value = value.to_string();
BMI {
bmi_value: string_value
}
}
/* 計算 */
fn calculation(height: f32, weight: f32) -> f32 {
/* 体重[㎏] ÷ {(身長[㎝] ÷ 100)[m]}² */
weight / (height / 100.0).powi(2)
}
}
/* idで探してvalueを得る */
fn fn_get_value_by_id(id: &str) -> String {
window().unwrap()
.document().unwrap()
.get_element_by_id(id).unwrap()
.dyn_ref::<HtmlInputElement>().unwrap()
.value()
}
#[function_component(App)]
fn app() -> Html {
/* HTMLに埋め込む変数 */
let bmi_state = use_state(
|| {
BMI::new_empty()
}
);
/* Event Listner */
let calculate_bmi = use_callback(
bmi_state.clone(),
|_mouse_event, clone_state| {
let height = fn_get_value_by_id("height").parse::<f32>().unwrap();
let weight = fn_get_value_by_id("weight").parse::<f32>().unwrap();
let bmi = BMI::calculation(height, weight);
clone_state.set(
BMI::new_value(bmi)
);
}
);
html! {
<>
<h1>{"BMI calculation"}</h1>
<p>{"height (㎝)"}<input type = "number" step = "0.1" id = "height" /></p>
<p>{"weight (㎏)"}<input type = "number" step = "0.1" id = "weight" /></p>
<p>{format!("BMI:{}", (*bmi_state).bmi_value)}</p>
<p><button onclick = {calculate_bmi} >{"calculate"}</button></p>
</>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
概要図
概説
概説
プログラムについての説明は以下に述べる。
main()
fn main() {
yew::Renderer::<App>::new().render();
}
fn
で関数を定義する。main()
がプログラムの開始点となる。
use
use yew::prelude::*;
use web_sys::{
window,
HtmlInputElement,
wasm_bindgen::JsCast
};
使用するcrate
を明記している。既に機能が整理され、提供されているプログラム群を、
use yew::prelude::*;
は、web_sys
は、
struct BMI
/* clone()を使うために`PartialEq`を付与する。 */
#[derive(PartialEq)]
struct BMI {
bmi_value: String,
} impl BMI {
/* constructor */
fn new_empty() -> Self {
BMI {
bmi_value: "".to_string()
}
}
fn new_value(value: f32) -> Self {
let string_value = value.to_string();
BMI {
bmi_value: string_value
}
}
/* 計算 */
fn calculation(height: f32, weight: f32) -> f32 {
/* 体重[㎏] ÷ {(身長[㎝] ÷ 100)[m]}² */
weight / (height / 100.0).powi(2)
}
}
BMI
構造体 struct BMI {
bmi_value: String,
}
この構造体は、String
型の文字列をbmi_value
という名で保有する。bmi_value
は関数で扱うため、&str
型では成立しない。
文字列の型
&str
とString
の二種がある。
簡単には、次のように分別する。
型 | 概要 |
---|---|
&str |
そのまま使う場合 |
String |
内容が変わる場合 関数から返却する場合 |
&str
は、厳格で融通が利かない。String
は、&str
ではできないこと(関数処理など)ができる。この選択を誤ると、プログラムが実行できなくなる。
この分類や、str
でなく&str
であることは、一見して受け入れ難いものである。しかしここでは受容することとし、敢えてその理由は述べない。
BMI
は「型」として扱うことができる。
構造体に関数を実装する
impl BMI {
/* constructor */
fn new_empty() -> Self {
BMI {
bmi_value: "".to_string()
}
}
fn new_value(value: f32) -> Self {
let clone_value = value.clone();
let string_value = clone_value.to_string();
BMI {
bmi_value: string_value
}
}
/* 計算 */
fn calculation(height: f32, weight: f32) -> f32 {
/* 体重[㎏] ÷ {(身長[㎝] ÷ 100)[m]}² */
weight / (height / 100.0).powi(2)
}
}
new_empty()
fn new_empty() -> Self {
BMI {
bmi_value: "".to_string()
}
}
bmi_value
が空の文字列を保有する、BMI
型構造体を返却する。
関数が返却する値の型は->
で明記する。ここでは、Self
はBMI
を言う。
空の文字列""
は&str
型であるため、to_string()
でString
型に変換する。
new_value()
fn new_value(value: f32) -> Self {
let string_value = value.to_string();
BMI {
bmi_value: string_value
}
}
bmi_value
が、小数を表す文字列を保有する、BMI
型構造体を返却する。
value: f32
は、この関数がf32
型小数値を受け、value
という名で扱うことを示す。
value.to_string()
は、小数値をString
型文字列に変換している。(例:1.1
という小数値が"1.1"
という文字列となる)
式と戻り値との区別
関数の中で、処理を表す式は末尾に;
が付く。反対に;
が付かない場合は戻り値を表し、この値が返却される。
{
let string_value = value.to_string();
BMI { bmi_value: string_value }
}
let string_value = value.to_string();
は処理である。BMI { bmi_value: string_value }
は戻り値である。
別言語ではreturn
と書かなければ返却されないことがしばしばだが、それに比べて意図が分かりづらく、誤りやすい。
calculation()
fn calculation(height: f32, weight: f32) -> f32 {
/* 体重[㎏] ÷ {(身長[㎝] ÷ 100)[m]}² */
weight / (height / 100.0).powi(2)
}
身長と体重を受け、
powi()
は、整数指数の累乗である。
構造体に属性を付与する
#[derive(PartialEq)]
後のbmi_state.clone()
に対し、
fn_get_value_by_id
fn fn_get_value_by_id(id: &str) -> String {
window().unwrap()
.document().unwrap()
.get_element_by_id(id).unwrap()
.dyn_ref::<HtmlInputElement>().unwrap()
.value()
}
局所的な利便性を上げるため、
単にwindow().unwrap().document().unwrap().get_element_by_id(id).unwrap().value()
としたのでは
参考記事:
app()
#[function_component(App)]
fn app() -> Html {
/* HTMLに埋め込む変数 */
let bmi_state = use_state(
|| {
BMI::new_empty()
}
);
/* Event Listner */
let calculate_bmi = use_callback(
bmi_state.clone(),
|_mouse_event, clone_state| {
let height = fn_get_value_by_id("height").parse::<f32>().unwrap();
let weight = fn_get_value_by_id("weight").parse::<f32>().unwrap();
let bmi = BMI::calculation(height, weight);
clone_state.set(
BMI::new_value(bmi)
);
}
);
html! {
<>
<h1>{"BMI calculation"}</h1>
<p>{"height (㎝)"}<input type = "number" step = "0.1" id = "height" /></p>
<p>{"weight (㎏)"}<input type = "number" step = "0.1" id = "weight" /></p>
<p>{format!("BMI:{}", (*bmi_state).bmi_value)}</p>
<p><button onclick = {calculate_bmi} >{"calculate"}</button></p>
</>
}
}
main()
によって実行される。
bmi_state
let bmi_state = use_state(
|| {
BMI::new_empty()
}
);
BMI::new_empty()
が返却した、空の文字列""
を持つBMI
型構造体を保有する。
名前解決
::
は名前解決のための記号である。new_empty()
が何処から来たのかを、BMI::
と付することで明記する。
無名関数
|| BMI::new_empty()
は、無名関数を表す。||
は引数を表す。ここでは、引数はないことを示す。そのあとに続く記述は、関数としての処理を示す。次に同じ。
fn no_name() -> BMI {
BMI::new_empty()
}
calculate_bmi
let calculate_bmi = use_callback(
bmi_state.clone(),
|_mouse_event, clone_state| {
let height = fn_get_value_by_id("height").parse::<f32>().unwrap();
let weight = fn_get_value_by_id("weight").parse::<f32>().unwrap();
let bmi = BMI::calculation(height, weight);
clone_state.set(
BMI::new_value(bmi)
);
}
);
use_callback()
は、
第一引数には、bmi_state
のclone
を指定する。
第二引数には、関数を指定する。
無名関数
|_mouse_event, clone_state| {
let height = fn_get_value_by_id("height").parse::<f32>().unwrap();
let weight = fn_get_value_by_id("weight").parse::<f32>().unwrap();
let bmi = BMI::calculation(height, weight);
clone_state.set(
BMI::new_value(bmi)
);
}
引数にはMouseEvent
型の_mouse_event
と、&UseStateHandle<BMI>
型のclone_state
を定める。
使用しない変数
変数名を_
から始めることで、その変数を使用していなくとも
身長と体重は次のように取得し、変換する。
let bmi = BMI::calculation(height, weight);
では、bmi
で受け取る。
clone_state.set(BMI::new_value(bmi));
では、1. BMI::new_value()
にbmi
を渡し、bmi
の値を持つBMI
型構造体を受け取る。2. 受け取った構造体で、clone_state
を更新する。
html!
html! {
<>
<h1>{"BMI calculation"}</h1>
<p>{"height (㎝)"}<input type = "number" step = "0.1" id = "height" /></p>
<p>{"weight (㎏)"}<input type = "number" step = "0.1" id = "weight" /></p>
<p>{format!("BMI:{}", (*bmi_state).bmi_value)}</p>
<p><button onclick = {calculate_bmi} >{"calculate"}</button></p>
</>
}
app()
の戻り値として返却する。
参照外し
(*bmi_state).bmi_value
の型推移は次の通りである。
*
は参照外し演算子である。UseStateHandle<BMI>
からBMI
を抜き出すために使用している。この他、次のような対応関係もある。
&
は参照、*
が参照外しである。本記事では敢えてこれ以上述べない。
format!
ここでは、"BMI:{}"
の{}
に、(*bmi_state).bmi_value
を当て嵌めた文字列を生成している。
html!
<h1>{"BMI calculation"}</h1>
のように、{}
で<button onclick = {calculate_bmi} >
のように、onclick = {calculate_bmi}
を直接見ることはできない)
また、html!
html!{<p></p><div></div>}
のような指定はできないため、空の<></>
で一括している。
!とmacro
!
の付くものを#[]
も
#[function_component(App)]
この属性を付与された関数は、戻り値としてHtml
型の値を返却する必要がある。また先述の通り、擬似的に
実行する
諸説ある実行方法だが、本記事では次の通り行う。
準備
実行にあたり、trunk
を使用する。
cargo install --locked trunk
また私自身記憶が曖昧だが、wasm32-unknown-unknown
を追加する必要があると思われる。
rustup target add wasm32-unknown-unknown
その他若し不足があれば、指南に従う外ない。
実行
先にも示したが、以下のようにして実行する。
trunk serve --open
--open
を付すると、自動的に127.0.0.1:8080
を表示する。
計算例
小数点以下の処理こそしていないが、入力を基に計算できていることが確かめられた。
跋
私が
Discussion