Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rust で型安全な SPA 開発

nawa
September 25, 2022

Rust で型安全な SPA 開発

Rust.Tokyo 2022 の発表資料です。

https://rust.tokyo/2022

Twitter とか質問を拾って回答したものを最後に追加しています。

以下は、スライド中のリンクです。

p5
- https://speakerdeck.com/koichik/isomorphic-survival-guide?slide=22

p47
- https://github.com/rust-unofficial/awesome-rust#gui
- https://github.com/flosse/rust-web-framework-comparison#frontend-frameworks-wasm

p69
- https://rustwasm.github.io/wasm-bindgen/examples/websockets.html

nawa

September 25, 2022
Tweet

More Decks by nawa

Other Decks in Programming

Transcript

  1. 目次 1. はじめに 2. 作ったものの紹介 3. The Elm Architecture (TEA)

    と Rust での実装 4. JS/TS と比べてどうか? 5. おわり 2
  2. 目次 1. はじめに 2. 作ったものの紹介 3. The Elm Architecture (TEA)

    と Rust での実装 4. JS/TS と比べてどうか? 5. おわり 3
  3. お前誰やねん 最近は Rust を 1 年半ほど書いています。好きな Web バックエンドの crate 構成は

    axum + sqlx。 また、Vue.js 2.x を 3 年ほど書いていました。C++ も 3 年ほど。 Elm も 3 ヶ月くらい触っています。 4 フロントエンド (外向け) フロントエンド (内部) バックエンド
  4. なぜ Rust でフロントエンドを作りたいのか • Rust が好き!!! • Rust は裏方だと思われているが、フロントエンドはどれくらい書けるのか試してみ たい

    • wasm にコンパイルできるので、ブラウザー上でも意外と動きそう • バックエンド (外部システム) とのデータを serde※ で Rust 内の表現に変換できる。ま た、バックエンドも Rust で書くなら、型定義が共有できそう 6 ※ serde の読み方をよくわかっていませんが発表中は「せるで」に統一します。
  5. 目次 1. はじめに 2. 作ったものの紹介 3. The Elm Architecture (TEA)

    と Rust での実装 4. JS/TS と比べてどうか? 5. おわり 7
  6. まずは顧客向けではない運用ツールを作る 顧客向けにいきなり Rust フロントエンドを作るのは厳しいので、社内の IT エンジニアで はない方に使ってもらう運用ツールを作成しました。 作成した 2 個を紹介します。

    • Web アプリ: サーバーインスタンス内の DB にアクセスするバックエンドも作りたい ため • デスクトップアプリ: バックエンドがすでに提供している API を呼びたいので、 PC 内で動作するデスクトップアプリ 9
  7. 採用した crate 今回 2 つの社内向けツールを作るにあたって、それぞれ異なる crate を採用しました。 • seed-rs: The

    Elm Architecture と似た形で Web フロントエンドを作成できる • iced: Elm にインスパイアされた Rust 用のマルチプラットフォーム GUI crate であ る 10 https://github.com/seed-rs/seed https://github.com/iced-rs/iced
  8. seed-rs のフロントエンド、actix-web と diesel のバックエンドを持つ Full Rust のツールを作成した。(Materialize CSS でデザイン)

    SQL の打ち間違いなどのリスクを減らしたい。 • フロントエンド・バックエンドで型定義を共有できた (そもそも seed-rs の examples にある) • デザイン部分を CSS におまかせできた seed-rs を使った場合の知見 12 作成した運用ツール PostgreSQL Java nginx やったこと:     目的:   良かったこと:  
  9. やったこと:   目的:   良かったこと:   • 認証情報の取得の自動化ができた • 取得した情報を見ながら操作できる

    iced を使った場合の知見 14 ① 認証情報の取得 ② 登録データの取得 ③ ②を見ながら操作 LINKLET (弊社サービス) 運用用デスクトップアプリを iced で作成した。 運用 API をコマンドラインから使うと、手数が多いので楽にしたい。
  10. seed-rs・iced の特徴 seed-rs も iced も Elm like に書くことができるため、大きな違いはありません。 強いて挙げるなら

    Web フロントエンドのみか、マルチプラットフォームかの違いがありま す。 • seed-rs: HTML タグで UI を構成して、wasm にビルドする • iced: UI 部品オブジェクトで UI を構成し、実行可能バイナリーや wasm にビルドす る 15 ※ UI 部品オブジェクト: Text や Row、Column など
  11. 目次 1. はじめに 2. 作ったものの紹介 3. The Elm Architecture (TEA)

    と Rust での実装 4. JS/TS と比べてどうか? 5. おわり 16
  12. Rust と The Elm Architecture (TEA) の相性が良くとても書きやすい Elm は AltJS

    の 1 種で、The Elm Architecture (TEA) の上でフロントエンドとして動き ます。ただ、Elm は関数型言語であるため慣れていない人はとっつきにくいかも知れま せん。 Rust も強い型であるため、TEA like にフロントエンドを書く crate があります。 17 https://guide.elm-lang.org/architecture/ The Elm Architecture (TEA) Elm TEA like Rust crate
  13. The Elm Architecture (TEA) seed-rs と iced の双方とも The Elm

    Architecture (TEA) を参考にしています。 Model で内部状態を定義し、内部状態をどう表示するかを View として実装し、UI など からの Message を受け取る Update で Model を更新します。 Model 内部状態 View UI 定義 Update 更新処理 Message 18
  14. seed-rs: カウンターの実装例 Model の初期状態を init() で定義する。 fn view(model: &Model) ->

    Node<Msg> { div![ "Counter: ", button![ model.counter, ev(Ev::Click, |_| Msg::Increment), ], ] } fn update( msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>, ) { match msg { Msg::Increment => model.counter += 1, } } enum Msg { Increment, } struct Model { counter: i32, } fn init( _: Url, _: &mut impl Orders<Msg>, ) -> Model { Model { counter: 0 } } 20 ※ これらを指定してアプリを開始する部分は省略しています
  15. seed-rs: カウンターの実装例 Model から view() で UI が作られる。 fn view(model:

    &Model) -> Node<Msg> { div![ "Counter: ", button![ model.counter, ev(Ev::Click, |_| Msg::Increment), ], ] } fn update( msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>, ) { match msg { Msg::Increment => model.counter += 1, } } enum Msg { Increment, } struct Model { counter: i32, } fn init( _: Url, _: &mut impl Orders<Msg>, ) -> Model { Model { counter: 0 } } 21 ※ これらを指定してアプリを開始する部分は省略しています
  16. seed-rs: カウンターの実装例 Model から view() で UI が作られる。 fn view(model:

    &Model) -> Node<Msg> { div![ "Counter: ", button![ model.counter, ev(Ev::Click, |_| Msg::Increment), ], ] } fn update( msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>, ) { match msg { Msg::Increment => model.counter += 1, } } enum Msg { Increment, } struct Model { counter: i32, } fn init( _: Url, _: &mut impl Orders<Msg>, ) -> Model { Model { counter: 0 } } 22 ※ これらを指定してアプリを開始する部分は省略しています
  17. seed-rs: カウンターの実装例 UI 上などでイベントが起こると Message が Update に渡される。 fn view(model:

    &Model) -> Node<Msg> { div![ "Counter: ", button![ model.counter, ev(Ev::Click, |_| Msg::Increment), ], ] } fn update( msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>, ) { match msg { Msg::Increment => model.counter += 1, } } enum Msg { Increment, } struct Model { counter: i32, } fn init( _: Url, _: &mut impl Orders<Msg>, ) -> Model { Model { counter: 0 } } 23 ※ これらを指定してアプリを開始する部分は省略しています
  18. seed-rs: カウンターの実装例 Message に基づいて Model を更新する。 fn view(model: &Model) ->

    Node<Msg> { div![ "Counter: ", button![ model.counter, ev(Ev::Click, |_| Msg::Increment), ], ] } fn update( msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>, ) { match msg { Msg::Increment => model.counter += 1, } } enum Msg { Increment, } struct Model { counter: i32, } fn init( _: Url, _: &mut impl Orders<Msg>, ) -> Model { Model { counter: 0 } } 24 ※ これらを指定してアプリを開始する部分は省略しています
  19. seed-rs: カウンターの実装例 ぐるぐる回る。 fn view(model: &Model) -> Node<Msg> { div![

    "Counter: ", button![ model.counter, ev(Ev::Click, |_| Msg::Increment), ], ] } fn update( msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>, ) { match msg { Msg::Increment => model.counter += 1, } } enum Msg { Increment, } struct Model { counter: i32, } fn init( _: Url, _: &mut impl Orders<Msg>, ) -> Model { Model { counter: 0 } } 25 ※ これらを指定してアプリを開始する部分は省略しています
  20. enum で列挙して match で網羅できるのが良い① Message・Update の実装で、enum と match が相性が良いです。 例えば「デクリメントボタンを追加」「入力フォーム※から値を指定する」などの機能が増え

    ることを考えると、 26 enum Msg { Increment, Decrement, // added! Set(i32), // added! } fn update(/* ... */) { match msg { Msg::Increment => model.counter += 1, Msg::Decrement => model.counter -= 1, Msg::Set(val) => model.counter = val, } } ※ 入力フォームがあると、フォームの変更を追うための Message も要りますが省略しています Message・Update の実装で、enum と match が相性が良いです。 例えば「デクリメントボタンを追加」「入力フォーム※から値を指定する」などの機能が増え ることを考えると、 • アクションの追加は Msg 列挙体にただ追加する • Update では match で網羅できていないとコンパイルエラーする
  21. enum で列挙して match で網羅できるのが良い② Model・View の実装でも、enum と match が相性が良いです。 例えば「タブビュー」の実装を考えると、Model

    に enum を持って、View で match して UI 定義を切り替えると実装できます。 27 struct Model { tab: Tabs, } enum Tabs { Home, Counter(i32), } ※ この例だと Counter の値が保存されないので微妙です。実装したツールではタブ切替時にデータ取得しています。 Welcome 0 Counter Tabs::Home Tabs::Counter(0)
  22. iced でのカウンターアプリの実装例 seed-rs と比べると、ボタンの状態が追加されたり、UI 定義が変わっています。 impl Counter { fn view(&mut

    self) -> Row<Msg> { Row::new() .push(Text::new("Counter: ".to_string())) .push(Button::new( &mut self.incr_btn, Text::new(self.value.to_string()) ).on_press(Msg::Increment)) } } impl Counter { fn update(&mut self, msg: Msg) { match msg { Msg::Increment => self.value += 1, } } } enum Msg { Increment, } struct Counter { value: i32, incr_btn: button::State, } impl Counter { fn new() -> Counter { Counter { value: 0, incr_btn: button::State::default(), } } } 28
  23. • フロントエンド・バックエンドを同じ Rust で書くことで、型定義を共有できて実装が楽 になる • serde で外部システムからのデータを Rust の表現にできる

    • enum と match があることで実装しやすい ◦ Rust でも Elm と同等の型安全性が得られる ◦ Rust も Elm 同様プログラム内では、そうそうエラーは起こらない • カスタムコンポーネントのデータを利用側の Model に保持する必要がある Rust・TEA のまとめ 36
  24. 目次 1. はじめに 2. 作ったものの紹介 3. The Elm Architecture (TEA)

    と Rust での実装 4. JS/TS と比べてどうか? 5. おわり 37
  25. JS/TS と比べたメリット・デメリット メリット 1. データ型定義を共有できて開発しやすい - フロントエンドとバックエンドの言語を統一したことによる 2. wasm-bindgen で

    JS 側と Rust 側のやり取りがやりやすい 3. デスクトップアプリだとファイル操作などをやり慣れた Rust で書ける デメリット 1. プラグインで対応できるものを自作する必要あり 2. ビルド時間が長い 3. Web フロントエンドだと、HTML あたりが冗長だったり CSS の選定が難しかったり する 38
  26. バックエンドも一緒に作ると型定義を共有できて便利 Node.js バックエンドで同じことができるが、Rust でもできる • TypeScript はフロントエンドが得意 vs Rust はバックエンドが得意

    異なる言語で書くと、どちらを書いているかわかりやすい (DSL) という観点もある • フロントエンド・バックエンドで書き方がだいぶ違うのであまり気にしなくて良いかも 39 JS/TS と比べてどうか? (1/6)
  27. wasm-bindgen で JS 側と Rust 側のやり取りが楽 JS の関数を Rust から呼ぶ

    40 JS/TS と比べてどうか? (2/6) Rust の関数を JS 側に公開 (呼ぶのは seed-rs)
  28. プラグインで対応できるものを自作する必要あり 以下のような機能は JS/TS では、パッケージをインストールすれば設定のみでほぼ実 装は不要です。カッコ内は Nuxt.js でのパッケージ名です。 • 認証機構 (@nuxtjs/auth-next)

    • i18n (@nuxtjs/i18n) • store (vuex) • localStorage へのデータのキャッシュ(vuex-persist) デスクトップアプリであれば、データのキャッシュはファイルに書き出せば※良いので、 Rust で書けることが嬉しいかも 41 ※ Isomorphic Survival Guide の定義ではバックエンドである JS/TS と比べてどうか? (3/6)
  29. HTML 出力の場合はデザインフレームワークに制限あり 以下の制限があります。 • なるべく CSS 単体で動く • JS で

    DOM の変更をしない Elm も同様ですが、むしろ JS/TS がよくできてる部分です。 次のスライドで詳しく説明します。 43 JS/TS と比べてどうか? (5/6)
  30. 生の HTML を意識するのがつらい JS/TS だと Vuetify や Ant Design のようなデザイン込みのフレームワークがあります。

    それらが提供するコンポーネントを使うと以下のようになります。 • それなりのデザインの UI が作成できる • 共通テーマなどで一括でデザインを指定できる 逆に Vue・React に依存しないことがメリットかも知れません。 • しかし React like とか Elm like とかが多いので JS/TS の後追いっぽさはある 44 JS/TS と比べてどうか? (6/6)
  31. 目次 1. はじめに 2. 作ったものの紹介 3. The Elm Architecture (TEA)

    と Rust での実装 4. JS/TS と比べてどうか? 5. おわり 45
  32. まとめ • Rust でも意外とフロントエンドが作成できる • TEA like で作成できるため、Elm のようにランタイム時に問題はそうそう起こらない •

    外部システムのデータのやり取りは serde でほぼ意識せずに行える • デザインがちょっとつらいけど、運用ツールなど機能が大事なものは十分作成でき そう 46
  33. おまけ: GUI 関連 crate の紹介 様々な crate があって触ってみたいと思っています。 これまでのスキルセットや目的にあった crate

    もあるかも知れません。 • React 風に書くことができる crate ◦ Yew: 一番有名?React and Elm like を謳っている。 ◦ Dioxus: rsx!() マクロで React のように書ける。 ◦ and more... • egui: 即時モードという描画方式でゲームエンジンと相性が良いらしい。 • Bevy: ゲームエンジン。もちろん GUI が必要。 • Tauri: Electron の Node.js 部を Rust で実装する。 https://github.com/rust-unofficial/awesome-rust#gui https://github.com/flosse/rust-web-framework-comparison#frontend-frameworks-wasm 47
  34. 49

  35. sli.do より 2 > seed-rs, iced 以外に選択肢として上がったcreateはありましたか? また、あった場合 この2つを選んだ理由が聞きたいです。 Rust

    のフロントエンド crate は群雄割拠すぎて、自分にとって書きやすいかなと思った らまず書くのが良いと思っています。 なので、seed-rs は Elm like で Elm 経験があり、チュートリアルがわかりやすかったの で採用して書きました。 @emergent が iced を採用したのは書籍で扱っていて、利用したことがあったからだそ うです。 52
  36. sli.do より 5 > 色々cratesあって何を選ぶのが良いのかかわかりません。どう選ぶと良いでしょうか p52 と同じですが、Rust のフロントエンド crate は群雄割拠すぎて、自分にとって書きや

    すいかなと思ったらまず書くのが良いと思っています。 探すのは p47 にも書いた以下のリンクより。 https://github.com/rust-unofficial/awesome-rust#gui https://github.com/flosse/rust-web-framework-comparison#frontend-frameworks-w asm 55
  37. sli.do より 6 > Tauriってのが流行ってるみたいですがどうでしょうか Tauri は Electron のデスクトップ側部分 (バックエンド※)

    を Rust で実装するようにした ものです。 フロントエンドは Web の技術で実装するのですよね。なので、Tauri の上で React を動 かしたり、たぶん seed-rs を動かしたりできると思います。 56 ※ ほとんどそのまま Isomorphic Survival Guide の p15 の構成ですね……すごい資料だ。 https://speakerdeck.com/koichik/isomorphic-survival-guide?slide=15
  38. sli.do より 8 > 他の言語でのGUI開発でよく使うツールが無くて不便だったことはありますか?JSの StoryBookとかReact Dev Toolとか。本番で使うならそういうのあると良いなあと思いま すがどうでしょう devtools

    は Elm・Rust では必要になったことがないんですよね。コンパイルが通ったら devtools が必要だったようなバグをほぼ見ないです。 StoryBook はビルドが遅いのでないのがつらい。 58
  39. sli.do より 10 > バックエンドとの型共有、素晴らしいと思います。protobufとかでやるのと違いはあり ますか?パフォーマンスや開発効率とか。 protobuf を知らなかったのですが、GraphQL や OpenAPI

    のようにスキーマファースト で書く形っぽいですかね。 型定義の共有自体は、もう Rust しか使わない、みたいな判断になってロックインに繋が るので、初期段階の開発スピードがほしいときだけだと思います。 パフォーマンスは、結局 serde で変換されると思うので変わらないと思います。 60
  40. sli.do より 11 > RustでのGUI開発を選択する基準はありますか?こんなプロジェクトならWeb技術の GUIよりRustのGUIが良いとか。 Rust でバックエンドを作成していて、cargo workspace を使っているなら、型定義だけ

    crate を分けると Cargo.toml で依存を書くと持ってこれて簡単に共有できるので、現在 でも Rust で GUI を作る余地はあるかなと思います。 61
  41. sli.do より 13 > Reactなど普段書いています。それに比べて記述量が多そう。Rustならではのメリッ トってあるのでしょうか 記述量は多くなりますね……※ 第 1 には、Rust

    だと match でもれなく実装できるというのが嬉しいです。 あとは好みの問題ですが、大域脱出がないことが良いと思っています。(質問と関係ない ですが Rust は GC がないことがとても好き) 63 ※ 個人的には記述量が多い (ボイラープレートが多い ) ことはデメリットだと思っていないです。 (標準的な意見ではないと思う。 ) 記述量が少ないことは、逆に暗黙のルールが多いことを表していて、読むときにその場にない知識が必要になるデメリットだと思います。
  42. sli.do より 14 > 作成したページのロード時間はどの程度でしょうか。社内ツールのようなので、あまり 問題にはならないのかもしれませんが、もし測定されていたのであれば教えていただけ ないでしょうか。 ロード時間は気にしていなかったです!参考までにファイルサイズですが、リリースビル ドの wasm

    は 668 KB でした。 Materialize CSS の JavaScript・CSS がそれぞれ 176 KB・138 KB なので、wasm は まあまあ大きい※ですね。 (iced はローカルで動くので N/A です。画像とかのアセットがないのでローカルまできた らどちらも爆速で動きます。) 64 ※ あまり考えずに crate に依存している部分があるので、ちゃんと整理して詰めれば減らせるとは思います。
  43. sli.do より 17 > Viewとそれ以外のロジックが密結合で、長期的にメンテするとき、Viewだけ別ライブラ リに乗り換えたりが大変そうな印象です。良い設計方法やアーキテクチャはあります か?どう調べたら良いでしょうか View だけ別ライブラリーに乗り換えるのは実際大変だと思いますが、JS/TS でも一緒

    だと思います。crate の数が多くて、乗り換えしたくなるというのはその通りですが。 ただ Elm like から別の Elm like に乗り換える場合、コンポーネント構成を使い回せる のでコンパイルエラーを地道に直していくと乗り換えできると思います。 67
  44. sli.do より 19 > Elm で解決できないことは Ports で結局 JavaScript の世界で解決しないといけない

    ということがありますが、Rust ではそのあたりが変わりますか? Elm で Ports を使う必要があるのは「Elm の外側にある API を使う」ときだと思います。 Ports のサンプルにもなっている WebSocket とか、外部ライブラリーの関数を使ったり とか。 後者は Rust でもどうしようもないです※が、前者の WebSocket などブラウザー API の 利用には web_sys という crate があって Rust で完結できます。※ https://rustwasm.github.io/wasm-bindgen/examples/websockets.html 69 ※ Rust だと Ports ではなく wasm-bindgen を使った FFI です ※ Elm でも Ports をラップしたパッケージを使えば良いみたいですね
  45. Twitter より 1 https://twitter.com/rabe1028/status/1573129864127062017 seed-rs も iced も両方 TEA なのでそこまで書き味に差はないです。

    実は Tauri とか Electron とか使えば Web フロントエンドのデスクトップアプリは書ける ので、seed-rs だからブラウザーでしか動かないなんてこともないんですけども。 70