はじめに
今回RustでWebアプリケーションを制作するにあたり、色々と戸惑ったり遠回りをしたことがあったので記事にしました。
この記事ではひとつのプロジェクトを通して、
- yewってなんぞや?wasmってなんぞや?
- RustでWebアプリケーションを開発するのってメリットあるの?
- yewでHTMLの要素の状態を得るにはどうするん?
ということなどについて言及していきます。
また事前知識として、以下のようなものがあると理解がしやすいかなと思います。
もし本記事で分からないところがあれば、参考文献として見てみてください。
-
- Rust
-
The Rust Programming Language
10. ジェネリック型、トレイト、ライフタイム くらいまで
-
- HTML
-
HTML入門 - CreatorQuest
Chapter17 ラベルと部品を関連付けよう くらいまで
-
- CSS
-
CSS入門 - CreatorQuest
Chapter02 基本的なセレクタ くらいまで
yewとは
yewとは以下のような特徴をもつRustのWebフレームワークです。
- Elmアーキテクチャを採用
- マクロにより、HTMLタグを(ほとんどそのまま)埋め込める
- 仮想DOMを搭載
- 最終的にWebAssemblyにコンパイルされるため、めちゃんこ爆速🚀(になる予定)
まとめると、yewというのはRust版Reactのようなものなんだなと思って間違いないと思います。
ちなみに爆速🚀(になる予定)としたのは、以下のyew公式からの引用が理由です。
WebAssembly(Wasm)はRustがコンパイル可能な軽量で低レベルな言語です。WebAssemblyはブラウザでネイティブ並の速度で動き、JavaScriptと互換性があり、そして全ての主要なブラウザでサポートされています。(省略)
気をつけなければいけないこととして、Wasmは(まだ)Webアプリのパフォーマンスを改善する銀の弾丸ではありません。現時点では、DOMのAPIをWebAssemblyから使うのはまだJavaScriptから直接呼ぶよりも遅いのです。これは WebAssembly Interface Types が解決しようとしている今のところの課題です。詳しい情報についてはMozillaの提案が書いてある excellent article をご確認ください。
とはいえ、簡単な計算ならば本当にネイティブと同様のスピードが提供されるので、速度面での心配はいらないでしょう。
2022/01/15現在、yewは開発が活発に進められており、今後のアップデートで一部の仕様が変更される場合があります。
yew以外にも言えることですが、参考にする情報がいつのものなのかについては、十分に注意する必要があると思います。二敗
ちなみに、今回使用したバージョンは現在最新の0.19.3です。
導入
yewもといwasmを動作させるためには、Trunkというバンドラーをインストールする必要があります。
コマンドラインで以下のコマンドを実行しましょう。
# wasmをRustのコンパイルターゲットに追加する.
rustup target add wasm32-unknown-unknown
# cargoによるTrunkのインストール.
cargo install trunk
これで準備は完了です。
プロジェクトのビルドには以下のコマンドを、
trunk build
プロジェクトのビルドと実行には以下のコマンドをプロジェクトのルートフォルダで実行しましょう。
trunk serve --open
trunk serve --open
はホットリロードに対応しているので、プロジェクトフォルダ内で変更があれば即座にビルドし、ブラウザのページをリロードしてくれます。
めちゃ便利ですね。
アプリケーションの設計
開発環境が整ったので、アプリケーションの設計を考えていきます。
とはいっても、フレームワークはもう決まっているので、内部でどんなことをしたいのかを考えましょう。
今回はyewがどんなものか試してみるだけなので、第n項目の素数を求めるくらいがちょうどいいと思いました。
Rustには素数算出用の外部クレートがあるので、エラトステネスの篩だとかミラー・ラビン素数判定法だとかのアルゴリズムを理解する必要はありません。
実装
設計が定まったので、実際にコーディングしていきます。
そしてできたものがこちら。
ソースコード
src/main.rs
use yew::{Component,Context,html,Html,InputEvent,TargetCast};
use web_sys::HtmlInputElement;
use js_sys::Date;
use primal::StreamingSieve;
#[derive(Default)]
struct Prime{
n: usize,
prime: usize,
run_time: f64,
}
enum Message{
Inputted(String),
Run,
}
impl Component for Prime{
type Message=Message;
type Properties=();
fn create(_ctx: &Context<Self>)->Self{
Self::default()
}
fn update(&mut self,_ctx: &Context<Self>,msg: Self::Message)->bool{
match msg{
Message::Inputted(n)=>{
self.n=match n.trim().parse(){
Ok(ok)=>ok,
Err(_)=>usize::default(),
};
false
}
Message::Run=>{
if self.n>0{
let start=Date::now();
self.prime=StreamingSieve::nth_prime(self.n);
self.run_time=Date::now()-start;
}
true
}
}
}
fn view(&self,ctx: &Context<Self>)->Html{
html!{
<div>
<div>
<h1>{"Let's Witness the Nth Prime Number!"}</h1>
</div>
<div class="text_input_container">
<input type="text" id="nth_input" oninput={ctx.link().callback(|e: InputEvent| Message::Inputted(e.target_unchecked_into::<HtmlInputElement>().value()))} />
<label for="nth_input">{"write the N here!"}</label>
</div>
<div id="button_container">
<button class="Run" onclick={ctx.link().callback(|_| Message::Run)}>
<span class="circle" >
<span class="icon arrow"></span>
</span>
<span class="button_text">{"Run"}</span>
</button>
</div>
<div>
<div class="prime">
if (self.prime)!=usize::default(){
{self.prime}
}
</div>
</div>
<div>
<p class="run_time">{format!("run time: {} msec",self.run_time)}</p>
</div>
</div>
}
}
}
fn main(){
yew::start_app::<Prime>();
}
Cargo.toml
[package]
name = "prime"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
yew = "0.19.3"
web-sys = "0.3.55"
js-sys = "0.3.55"
primal = "0.3.0"
index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>Prime-Lime - 第n番目の素数を求める</title>
<link data-trunk rel="icon" type="image/png" href="icon.png">
<link data-trunk rel="sass" href="style.scss" />
</head>
<body>
</body>
</html>
style.scss
@import url(https://fonts.googleapis.com/css?family=Montserrat);
$background: #313E50;
$white: #fff;
$black: #282936;
body{
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: Montserrat;
background: $background;
}
h1{
color: $black;
}
.text_input_container{
position: relative;
margin-top: 50px;
margin-left: 50px;
input[type="text"]{
display: inline-block;
width: 500px;
height: 40px;
box-sizing: border-box;
outline: none;
border: 1px solid lightgray;
border-radius: 3px;
padding: 10px 10px 10px 180px;
transition: all 0.1s ease-out;
}
input[type="text"]+label{
position: absolute;
top: 0;
left: 0;
bottom: 0;
height: 40px;
line-height: 40px;
color: white;
border-radius: 3px 0 0 3px;
padding: 0 20px;
background: #E03616;
transform: translateZ(0) translateX(0);
transition: all 0.3s ease-in;
transition-delay: 0.2s;
}
input[type="text"]:focus+label{
transform: translateY(-120%) translateX(0%);
border-radius: 3px;
transition: all 0.1s ease-out;
}
input[type="text"]:focus{
padding: 10px;
transition: all 0.3s ease-out;
transition-delay: 0.2s;
}
}
@mixin transition($property: all, $duration: 0.45s, $ease: cubic-bezier(0.65,0,.076,1)){
transition: $property $duration $ease;
}
#button_container{
display: block;
text-align: center;
margin-top: 50px;
}
button{
position: relative;
display: inline-block;
cursor: pointer;
outline: none;
border: 0;
vertical-align: middle;
text-decoration: none;
background: transparent;
padding: 0;
font-size: inherit;
font-family: inherit;
&.Run{
width: 12rem;
height: auto;
.circle{
@include transition(all, 0.45s, cubic-bezier(0.65,0,.076,1));
position: relative;
display: block;
margin: 0;
width: 3rem;
height: 3rem;
background: $black;
border-radius: 1.625rem;
.icon{
@include transition(all, 0.45s, cubic-bezier(0.65,0,.076,1));
position: absolute;
top: 0;
bottom: 0;
margin: auto;
background: $white;
&.arrow{
@include transition(all, 0.45s, cubic-bezier(0.65,0,.076,1));
left: 0.625rem;
width: 1.125rem;
height: 0.125rem;
background: none;
&::before{
position: absolute;
content: '';
top: -0.25rem;
right: 0.0625rem;
width: 0.625rem;
height: 0.625rem;
border-top: 0.125rem solid #fff;
border-right: 0.125rem solid #fff;
transform: rotate(45deg);
}
}
}
}
.button_text{
@include transition(all, 0.45s, cubic-bezier(0.65,0,.076,1));
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 0.75rem 0;
margin-left: auto; margin-right: auto;
color: $black;
font-weight: 700;
line-height: 1.6;
text-align: center;
text-transform: uppercase;
}
}
&:hover{
.circle{
width: 100%;
.icon{
&.arrow{
background: $white;
transform: translate(1rem, 0);
}
}
}
.button_text{
color: $white;
}
}
}
.prime{
margin-top: 100px;
text-align: center;
font-size: 100px;
}
.run_time{
margin-top: 100px;
text-align: center;
}
ぱっと見かなり短いし、何よりトレイトによって実行が保証されていることが安心感を持たせてくれますね。
Elmアーキテクチャってやっぱいいなぁ...(しみじみ
気を取り直して、コードの説明をしていきます。
解説
Primeオブジェクトの定義
#[derive(Default)]
struct Prime{
n: usize,
prime: usize,
run_time: f64,
}
アプリに関係する変数をまとめます。
usize
型とf64
型はDefault
トレイトを実装しているので、ついでにPrime
オブジェクトにもderive
で実装しておくと便利です。
Message列挙体の定義
enum Message{
Inputted(String),
Run,
}
<button>
や<input type="text">
などにユーザーが変更を加えたときに送受信されるメッセージをまとめます。
Componentトレイト実装
impl Component for Prime{/*省略*/}
後述のstart_app()
を実行するために、Prime
オブジェクトにComponent
トレイトを実装します。
トレイトというのはJavaやC#でいうインターフェースのようなものです。
トレイトには定義されなければならないオブジェクトが存在し、オブジェクトがトレイトを得るにはそのオブジェクトを絶対に定義しなければなりません。
言い換えれば、オブジェクトに外部からインポートしたあるトレイトが実装されたとき、そのオブジェクトには外部クレート開発者がユーザーに定義しておいてほしいと思ったオブジェクトが定義されていることが保証されているということになります。
これにより、ユーザーは後述するupdate()
やview()
がいつ実行されるものなのかを正確に理解する必要はなく、どんな振る舞いをするのかにのみ重点を置けます。
なんて素晴らしい言語機能なんだぁ...。
create()実装
fn create(_ctx: &Context<Self>)->Self{
Self::default()
}
Prime
の初期化を定義します。
Self::default()
は先述の#[derive(Default)]
をしていないと使用できないので注意してください。
ちなみにSelf::default()
の中身は以下のようになっています。
fn default()->Prime{
Prime{
n: 0,
prime: 0,
run_time: 0.0,
}
}
update()実装
fn update(&mut self,_ctx: &Context<Self>,msg: Self::Message)->bool{/*省略*/}
メッセージを受け取った際の、Prime
オブジェクトのメンバ変数や画面の更新処理を定義します。
このとき受信したメッセージが引数となっているので、メッセージごとの処理をパターンマッチングで網羅的に定義していきます。
返り値は画面を更新するか否かを示し、これにより値は更新するが画面は更新しないということも可能になります。
第n項目の素数算出・実行時間計測
if self.n>0{
let start=Date::now();
self.prime=StreamingSieve::nth_prime(self.n);
self.run_time=Date::now()-start;
}
第n項目の素数を算出し、その実行時間を計測します。
ここで、Date
はjs-sysという外部クレートのオブジェクトです。
jsという名前から分かる通り、js-sysはJavaScript の標準的な組み込みオブジェクトをRustで扱えるようにしたものです。
Rustにもstd::time::Instant
というオブジェクトで実行時間の計測は可能なのですが、残念ながら多くのブラウザはstd::time::Instant
に対応していませんでした。
よって、js-sysよりDate
オブジェクトで代用する必要があります。
ちなみに、nth_prime()
はアトキンの篩という前述のエラトステネスの篩の進化版みたいなアルゴリズムを基に算出しているそうです。
興味のある方は是非速度対決してみて下さい。
自分は大敗しました。
view()実装
fn view(&self,ctx: &Context<Self>)->Html{/*省略*/}
画面への描画処理を定義します。
ここで、Html
オブジェクトはhtml!
マクロによって得られ、html!
マクロ内部ではJSXライクな記述ができます。
つまるところ、HTMLを書きつつもそこでRustのあらゆる言語機能を使用できるということです。
優秀だなぁ...。
<input type="text">からの入力情報取得
<div class="text_input_container">
<input type="text" id="nth_input" oninput={ctx.link().callback(|e: InputEvent| Message::Inputted(e.target_unchecked_into::<HtmlInputElement>().value()))} />
<label for="nth_input">{"write the N here!"}</label>
</div>
ユーザーがself.n
を入力するテキストボックスを作成します。
ここで、<input>
の'oninput'属性がテキストボックスに入力された際に実行される処理となります。
が、この記法は今後変更される可能性が高いと思われます。
というのも、e.target_unchecked_into::<HtmlInputElement>().value()
のHtmlInputElement
はweb-sys
という外部クレートからインポートしたオブジェクトだからです。
なぜHtmlInputElement
である必要があるのかは分かりませんが、少し前のバージョンではまた記法が異なっており、さらに現状unchecked
であることからも、今後のアップデートで記法が変わるであろうことが予想されます。
.icoの読み込み
<link data-trunk rel="icon" type="image/png" href="icon.png">
ブラウザでページタイトルの左側に表示されるアイコンを設定します。
何かそういう衝動に駆られたので、急遽手書きしました。
なんかアイコンがあると特別感が倍増しますね。
また、今回はTrunkでビルドを行うので、属性にdata-trunk
を追加する必要があります。
これをすることでdistフォルダへ手動でコピーせずに済みます。
.scssの読み込み
<link data-trunk rel="sass" href="style.scss" />
style.scss
を読み込みます。
先程と同様にdata-trunk
属性を追加するとscssからcssへのコンパイルだけでなくdistフォルダへのコピーまで行ってくれるので、めちゃんこ便利ですね。
解説はこんなところです。
もし不足があれば、コメントで是非その旨をお伝え下さい。
お答えできる範囲でお答えします。
完成!!!
ようやっと完成しました!
余談ですが、はじめてのWebアプリケーション制作が無事終了したのでテンションが上がり、流れでFirebaseによるデプロイ(サーバーにファイルを置いてインターネット上で公開すること)もしました。
デプロイにはこちらの記事がとても参考になりました。
おわりに
今回はお試し感覚でyewを使用してみました。
yewは未だ発展途上であるとはいえ、とても使いやすいフレームワークだという印象を受けました。
実行速度という観点だけではなく言語機能の充実さという観点からも、今後はRustによるWebアプリケーション制作が流行ってもおかしくはない気がします。
というか流行ってくれ...そしたらその分ドキュメントもexampleも増えるから...。
またね。