Rubyで通信を伴うSPAを構築してみました

# 前回までのあらすじ
[前回の記事](https://qiita.com/keyasuda-2/items/ac6e4ff4f64ab10c5ae5)では、[RubyのWebAssemblyポート](https://github.com/ruby/ruby.wasm)を使いSPA(single-page application)を構築するためのパッケージを開発し、[TodoMVC](https://todomvc.com/)を[移植しました](https://github.com/keyasuda/bormashino-todomvc)。

# はじめに
TodoMVCは通信を伴わずブラウザ上のみで完結するアプリケーションだったため、構築用パッケージ"[Bormaŝino](https://github.com/keyasuda/bormashino)"は特に通信系の機能は持っておらず、[jsモジュール](https://github.com/ruby/ruby.wasm/tree/main/ext/js#ruby-extension-to-manipulate-javascript-api)を用いて[Fetch API](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API)(もしくはXmlHttpRequest等)を扱う必要が有りました。
今回はこの欠けていた部分であるFetch APIラッパー他の実装が完了し、jsモジュール等を生で扱わずとも通信を伴うアプリケーションが実現できるようになりました。また、既存のJavaScript資産を組み合わせての開発も更に容易になっています。
新たに実装した機能を扱うデモとして[地理院地図](https://maps.gsi.go.jp/)を扱うアプリケーションを実装しましたので、本記事ではそのコードの要点を解説します。

# デモ
https://bormashino-demo-gsi-maps.vercel.app/

# コード解説
## ファイル構成
```
.
├── js
│   ├── app.js       # ブートストラップコード
│   ├── index.html   # アプリケーションの外枠
│   ├── map.js       # 地図表示コード
├── src
│   ├── app.rb       # アプリケーションの中核 Sinatra::Baseを継承したクラス
│   ├── bootstrap.rb # Ruby側のブートストラップコード
│   ├── gsi_api.rb   # 国土地理院の地名検索APIのクライアント
```
画面の上半分は`map.js`、下半分は`app.rb`が主な処理を担っています。

## 通信処理
地名検索を国土地理院のAPIで行う通信を`Fetch API`のラッパーである`Bormashino::Fetch`で行っています。
```ruby
    Bormashino::Fetch.new(
      resource: "https://msearch.gsi.go.jp/address-search/AddressSearch?q=#{CGI.escape(query)}",
      resolved_to: callback_to,
      options:,
    ).run
```
https://github.com/keyasuda/bormashino-demo-gsi-maps/blob/main/src/gsi_api.rb#L6-L10

`Bormashino::Fetch.new`は以下のキーワード引数を取ります。
|キーワード|対応するFetch APIの引数|備考|
|--|--|--|
|resource|第1引数(resource)|必須, String|
|init|第2引数(init)|省略可, Hash|
|resolved_to|-|必須, 応答を渡すエンドポイントのパス, String|
|options|-|省略可, エンドポイントへ渡されるHash|

`Bormashino::Fetch#run`を叩くと内部ではFetch APIが呼び出されリクエストが実行されます。応答が得られると`resolved_to`で指定されたエンドポイントパスへPOSTリクエストが行われ応答内容が渡されます。
```ruby
  # 地名検索APIの結果が返ってきたら呼び出されるエンドポイント
  post '/address_callback' do
    options = JSON.parse(params[:options])
    @cache.set_item(options['q'], params[:payload])

    redirect to("/?q=#{CGI.escape(options['q'])}")
  end
```
https://github.com/keyasuda/bormashino-demo-gsi-maps/blob/main/src/app.rb#L33-L39

エンドポイントへは次のようなパラメータが渡されます。
|名称|内容|備考|
|--|--|--|
|status|返ってきたステータスコード|数値(をto_sしたもの)|
|payload|得られた応答内容|文字列|
|options|`Bormashino::Fetch.new`に渡した内容を`to_json`したもの|文字列|

## ページタイトル
検索した地名がページタイトルに入るようにしました。
```ruby
<template class="bormashino-head">
  <title><%if params['q'] %><%= params['q'] %> - <% end %>Bormaŝino GSI maps demo</title>
</template>
```
https://github.com/keyasuda/bormashino-demo-gsi-maps/blob/main/src/views/index.erb#L22-L24

`<template class="bormashino-head">`で囲まれた内容が`<head>`へ挿入されます。react-helmetに似た機能です。

## 画面更新イベント
Sinatraが何らかの値を返し画面が更新された際にイベントを発生させるようにしました。このイベントを捕捉するイベントリスナーを定義すると画面更新時にJavaScriptで任意の処理を実行できます。
今回のデモでは上半分にLeafletで地図を表示しており、下半分の地点一覧が更新される度に地図上のマーカーを更新しています。
```javascript
app.addEventListener('bormashino:updated', (e) => {
  // 画面が更新される度に実行される
})
```
https://github.com/keyasuda/bormashino-demo-gsi-maps/blob/main/js/map.js#L15

# おわりに
既存のJavaScript資産を活用しつつRubyで大半が実装されたSPAが構築できるようになりました。
[開発用のテンプレート](https://github.com/keyasuda/bormashino-app-template)も今回の変更を含んだgem/NPMパッケージを使用するように更新されておりますので、どうぞお試し下さい。