RubyでSPAを構築してみました

# はじめに
[RubyのWebAssemblyビルド](https://github.com/ruby/ruby.wasm)を使ってSPA(single page application)を構築するためのパッケージ"[Bormaŝino](https://github.com/keyasuda/bormashino)"を開発し、[TodoMVC](https://todomvc.com/)を[移植してみました](https://github.com/keyasuda/bormashino-todomvc)。
本記事ではパッケージの紹介及び移植されたコードを解説します。

# デモ
- TodoMVC 移植版 https://bormashino-todomvc.vercel.app/
- テンプレートアプリケーション https://bormashino-app-template.vercel.app/


# パッケージ"Bormaŝino"について
## コンセプト
> 伝統的な「フォームを表示し」「POSTされる内容でデータストアを更新し」「データストアに保持されている内容を元に画面を表示する」だけのアプリケーションを通信時間が0に近い状態で走らせれば、いわゆるSPA的な操作感が得られるのでは？

という考えに基づき、Sinatra[^1]を用いて構築された伝統的なWebアプリケーションをブラウザ内で動作させる環境を提供するパッケージが"Bormaŝino"です。
この構成により以下の項目を実現しました。
- アプリケーションの実装は伝統的なWebアプリケーションのそれと似通っており、学習負荷が低い
- 既存のrubygems等の資産を流用できる

## 現状出来ること
- いわゆるSPA的な画面遷移
- LocalStorageの読み書き
- ユーザ入力の取扱い

## 構成
次の2つのパッケージから構成されています。
- [rubygem](https://rubygems.org/gems/bormashino)
- [NPMパッケージ](https://www.npmjs.com/package/bormashino)

また、アプリケーションのテンプレートを用意してあります。
https://github.com/keyasuda/bormashino-app-template

## 依存関係
主に以下のプロダクトに依存しています。

- [ruby.wasm](https://github.com/ruby/ruby.wasm)
- [wasi-vfs](https://github.com/kateinoigakukun/wasi-vfs)
- [Sinatra](http://sinatrarb.com/)
- [html5-history-router](https://github.com/BusinessDuck/html5-history-router)


# TodoMVC移植版のコードについて
## ファイル構成
```
.
├── js
│   ├── app.js          ページ読み込み後にruby.wasmを読み込み起動するブートストラップコード
│   └── index.html      アプリケーションの外枠
├── spec
│   └── todo_spec.rb    Todoクラスのユニットテスト
└── src
    ├── app.rb          アプリケーションの中心部。Sinatra::Baseを継承したクラス
    ├── bootstrap.rb    ruby.wasm起動後のブートストラップコード
    ├── todo.rb         Todoクラス。LocalStorageへの読み書きを(Bormashino::LocalStorage経由で)行う
    └── views
        ├── index.erb   Todo一覧を内包する画面
        └── todo.erb    Todo一件を表示するERB断片
```
アプリケーションの実質的な実装はこれらのファイルに記述されています。

## リンクの扱い/ルーティング
https://github.com/keyasuda/bormashino-todomvc/blob/16a8c271044e614679f8483a6cce973750864060/src/views/index.erb#L34-L42
```erb
<li>
  <a href="/" <% if request.path_info == '/' %>class="selected"<% end %>>All</a>
</li>
<li>
  <a href="/active" <% if request.path_info == '/active' %>class="selected"<% end %>>Active</a>
</li>
<li>
  <a href="/completed" <% if request.path_info == '/completed' %>class="selected"<% end %>>Completed</a>
</li>
```
ドメイン名を含まないリンクは画面更新時に自動でonclickイベントハンドラが設定され、クリック時にアプリケーションへのGETリクエストが行われます。

https://github.com/keyasuda/bormashino-todomvc/blob/16a8c271044e614679f8483a6cce973750864060/js/index.html#L15-L16
```html
<p>Created by <a href="https://github.com/keyasuda">@keyasuda</a></p>
<p>An <a href="https://github.com/keyasuda/bormashino">Bormaŝino</a> port of <a href="http://todomvc.com">TodoMVC</a></p>
```
他ドメイン名へのリンクにはイベントハンドラは設定されず、通常のリンクとして機能します。

## フォームの扱い
https://github.com/keyasuda/bormashino-todomvc/blob/16a8c271044e614679f8483a6cce973750864060/src/views/index.erb#L11
```erb
<form action="/todos/all" method="PUT">
```
formは拡張されており、methodに標準のget,postの他put,patch及びdeleteを指定可能です。
リンクと同様に自動でイベントハンドラが設定され、submitでアプリケーションへリクエストが行われます。

### フォーム中の子要素に対する操作
https://github.com/keyasuda/bormashino-todomvc/blob/16a8c271044e614679f8483a6cce973750864060/src/views/todo.erb#L5-L10
```erb
<input
  class="toggle"
  data-bormashino-submit-on="change"
  type="checkbox"
  name="completed"
  <% if todo.completed %>checked<% end %>>
```
フォーム中に`data-bormashino-submit-on`という属性を持った要素がある場合、その要素で指定されているイベント(この例ではonclick)が対象の要素で発生した場合に親となるフォームがsubmitされる仕組みを組み込んであります。これを利用してチェックボックスのクリックでTodoのactive/completed状態を更新しています。

## ローカルストレージの利用
[window.localStorage](https://developer.mozilla.org/ja/docs/Web/API/Window/localStorage)のラッパーとして`Bormashino::LocalStorage`を用意してあります。Todoの内容は[TodoMVCの仕様](https://github.com/tastejs/todomvc/blob/master/app-spec.md#persistence)通りに`todos-bormashino`キー以下にJSONとして格納されます。

https://github.com/keyasuda/bormashino-todomvc/blob/main/src/todo.rb#L3

## JavaScriptとの統合
https://github.com/keyasuda/bormashino-todomvc/blob/main/js/app.js#L20
```javascript
window.bormashino = RubyApplication
```
window.bormashinoにアプリケーション操作用のオブジェクトがセットされています。
https://github.com/keyasuda/bormashino-todomvc/blob/main/src/views/todo.erb#L11
```erb
<label ondblclick='bormashino.router.pushState(<%= "#{request.path_info}?edit=#{todo.id}".to_json %>)'><%= todo.title %></label>
```
本TodoMVCデモ実装ではTodoのダブルクリックで編集モードに入れるためにondblclickハンドラが設定されています。


# おわりに
パッケージ"Bormaŝino"を用いてRubyでTodoMVCと同様のSPAが構築できました。今後、以下のような追加機能開発を予定しています。
- Fetch APIのラッパー実装
- JavaScriptとの統合機能強化
    - 画面更新時に発生するイベントの追加

Railsのように気の利いたコードジェネレータはありませんが、[テンプレート](https://github.com/keyasuda/bormashino-app-template)を用意してありますのでどうぞお試し下さい。

2022-05-15追記: [続きを書きました](https://qiita.com/keyasuda-2/items/96890a75a0072873e456)。

[^1]: に限らずRack protocol準拠のWebアプリケーションフレームワークであれば動く可能性はありますが未検証です