Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rails × Hotwire入門:JavaScriptを書かずにSPAのような高速Webアプリを作る方法

ページ遷移やフォーム送信を高速化したい、複雑なページをコンポーネント単位に分割したい——しかもJavaScriptのコードを一切書かずに。そんな願いを叶えてくれるのがHotwireです。本記事では、サーバーサイドレンダリングを実現するツール「Hotwire」を活用して、Railsアプリケーションを次のレベルへ引き上げる方法を、実例を交えながら詳しく解説します。

Hotwireとは?

Hotwireは、JSONの代わりにHTMLを通信経路上でやり取りすることで、JavaScriptを書かずにモダンなWebアプリケーションを構築できるフレームワークです。HTMLベースの配信により、ページの表示速度は大幅に向上します。

レンダリングはサーバーサイドで維持されるため、Railsが昔から大切にしてきた「シンプルで生産性の高い開発体験」をそのまま享受できます。それでいて、従来のシングルページアプリケーション(SPA)特有の速さや軽快な応答性まで失われないのが魅力です。

Hotwireの中核を担っているのはTurboというgemです。Turboは互いに補完し合う一連の技術群で、以下のような役割を果たします。

  • ページ間ナビゲーションの高速化
  • フォーム送信の効率化
  • 複雑なページのコンポーネント分割
  • WebSocket(ActionCable、チャンネル、ストリーミングデータ)を通じたページの部分更新の配信

なぜHotwireを使うべきなのか?

JavaScriptに苦手意識がある方でも、「ページ間の移動がとても速い」という優れたユーザー体験を実現したいなら、Hotwireは最適な選択肢です。

では、Hotwireはどのように動作するのでしょうか。鍵となるのはサーバーサイドレンダリング(SSR)です。SSRはSPAの課題を解決しながら、その主なメリットを維持できるアプローチです。従来型の読み込みのようにレンダリング処理の一部をサーバー側に戻すことで、アプリのロードをより効率的にします。さらに、パフォーマンス改善だけでなく、インデックス登録などのSEO面の問題への対策にも役立ちます。

導入は簡単?

結論から言うと、非常に簡単です。必要なのは以下の3点だけです。

  • Railsプロジェクトのデフォルトパッケージ(Ruby、Ruby on Rails、ActionCable、WebSocket)
  • JS依存関係をまとめて取得してくれるTurbo gem
  • WebSocket接続中の一時データを保存するためのRedis

SPA並みの速度を得るために、わざわざ別の言語(JavaScript)を学ぶ必要はありません。TurboはTurbo DriveTurbo FramesTurbo StreamsTurbo Nativeという4つの要素で構成されています。Driveはリンクやフォームを高速化しネットワーク再読み込みを削減し、Framesはページを独立したコンテキストに分割することで、より効率的な読み込みを可能にします。

Railsでの使い方:ステップバイステップ

ここからは、実際にサンプルアプリを構築しながらHotwireの使い方を見ていきましょう。

今回使用する環境・ツールは以下の通りです。

  • Ruby
  • Ruby on Rails
  • Redis
  • SQLite(デフォルトデータベース)
  • Hotwire gem
  • turbo-rails
  • StimulusJS
  • WebSocket
  • ActionCable

作るものは?

今回は練習として、ソーシャルメディア風の投稿アプリを作成します。

まずターミナルを開き、新しいRailsプロジェクトを作成しましょう。

rails new social-media

続いて、プロジェクトのディレクトリに移動します。

cd social-media

Hotwireの追加

Hotwireのgemをプロジェクトに追加します。

bundle add hotwire-rails

あるいは、Gemfileを直接開いて次の行を追記してもOKです。

gem "hotwire-rails"

後者の方法を選んだ場合は、bundleコマンドを実行してください。

bundle install

その後、Hotwire本体をインストールします。

rails hotwire:install

初期設定の確認

ここで、Hotwireが自動で行った初期設定を確認しておきましょう。Gemfileには、次の行が追加されているはずです。

gem 'redis', '~> 4.0'

なぜRedisが必要なのか?

Redisのgemが追加されたのは、ActionCableがWebSocket接続中の一時データを保存するためにRedisを必要とするからです。ただし、Redisをインストールしただけでは使える状態にはなりません。設定が正しいか確認しましょう。config/cable.ymlファイルは、次のようになっているはずです。

development:
 adapter: redis
 redis://localhost:6379/1

アプリケーション起動時には、Redisサーバー(redis-server)が稼働していることを必ず確認してください。

JS依存関係の確認

package.jsonの依存関係もチェックしておきましょう。

dependencies: {
 @hotwired/turbo-rails: ^7.0.0-beta.5,
 @rails/actioncable: ^6.0.0,
 @rails/activestorage: ^6.0.0,
 @rails/ujs: ^6.0.0,
 @rails/webpacker: 4.3.0,
 stimulus: ^2.0.0
}

モデルの生成

各ファイルの確認が終わったら、postsテーブル用のビュー、コントローラー、モデル、マイグレーションを生成します。カラムはbody(投稿本文)とlikes(いいね数)の2つです。ターミナルで次のコマンドを実行してください。

rails g scaffold posts body:text likes:integer

データベースの作成

すべてのファイルが生成できたら、変更内容をデータベースに反映させます。

rails db:create db:migrate

問題なく完了したら、サーバー(rails server)を起動して動作を確認してみましょう。https://localhost:3000/postsにアクセスすると、投稿ページが表示されるはずです。

投稿一覧画面のスクリーンショット

投稿一覧の表示

次に、投稿を一覧表示できるようにします。app/views/posts/_post.html.erbというファイルを作成し、以下のコードを記述してください。

<div style="background: lightgrey; width: 300px; padding: 10px;">
  <%= post.body %>
  <br>
  <%= link_to :edit, edit_post_path(post) %>
  <%= button_to "likes (#{post.likes || 0})", post_path(post, like: true), method: :put %>
</div>
<br>

バリデーションの追加

bodyフィールドが空欄(null)にならないようバリデーションを設定します。あわせて、新規投稿が作成された際に、その投稿を画面の先頭に即座に表示するようブロードキャストの指示も行います。app/models/post.rbを編集し、次のコードを挿入してください。

class Post < ApplicationRecord
  validates_presence_of :body

  after_create_commit { broadcast_prepend_to :posts }
end

並び順の指定

投稿を新しい順に並べるため、コントローラー(app/controllers/posts_controller.rb)を開いて編集します。

...
def index
  @posts = Post.all.order(created_at: :desc)
  @post = Post.new
end
...

indexビューの仕上げ

最後に、indexビュー(app/views/posts/index.html.erb)を編集して、「新規投稿フォーム」と「全投稿の一覧」を同じページに表示できるようにします。

<%= turbo_stream_from :posts %>

<%= turbo_frame_tag :post_form do %>
  <%= render 'posts/form', post: @post %>
<% end %>

<%= turbo_frame_tag :posts do %>
  <%= render @posts %>
<% end %>

indexへのリダイレクト

さらに、コントローラーのcreateアクションを修正します。デフォルトでは投稿後に詳細ページへリダイレクトされますが、すべてを同一ページ上で完結させるため、一覧ページへ戻るように変更しましょう。

...
def create
  @post = Post.new(post_params)

  respond_to do |format|
  if @post.save
   format.html { redirect_to posts_path }
   format.json { render :show, status: :created, location: @post }
 else
    format.turbo_stream { render turbo_stream: turbo_stream.replace(@post, partial: 'posts/form', locals: { post: @post }) }
    format.html { render :new, status: :unprocessable_entity }
   format.json { render json: @post.errors, status: :unprocessable_entity }
 end
  end
end
...

完成したページは次のような見た目になります。

完成ページのスクリーンショット

ログの確認

投稿を作成すると、ターミナルのログに次のような出力が現れます。

[ActionCable] Broadcasting to posts: "<turbo-stream action=\"prepend\" target=\"posts\"><template><div style=\"background: lightgrey; width: 300px; padding: 10px;\">\n  first post\n  <br>\n  <a href=\"/posts/1/edit\">edit</a>\n  <form class=\"button_to\" method=\"post\" action=\"/posts/4?like=true\"><input type=\"hidden\" name=\"_method\" value=\"put\" /><input type=\"submit\" value=\"likes (0)\" /><input type=\"hidden\" name=\"authenticity_token\" value=\"<token>==\" /></form>\n</div>\n<br>\n</template></turbo-stream>"

ここから読み取れること

これは、ActionCableがTurbo Streamsと連携して超高速度で動作していることを意味します。たとえば次のようなレスポンス時間が確認できます。

Completed 302 Found in 18ms

わずか18ミリ秒で処理が完了しています。まさにHotwireの威力と言えるでしょう。

デプロイに関するFAQ

Railsサーバーとは別に、何か実行ファイルが必要?

いいえ、不要です。ActionCableはUnicorn、Puma、Passengerといった主要なサーバーと問題なく連携できます。

HerokuはHotwireに対応している?

はい、対応しています。Hotwireの基盤であるWebSocketはHerokuで利用可能です。さらに詳しく知りたい場合は、Herokuの公式ドキュメントを参照してください。

Redisを使うもうひとつの理由

Redisを採用する理由は、他にもあります。HerokuでRedisを使わずにアプリを複数のdynoへスケールすると、メッセージが全ユーザーに届かなくなるのです。現在のアプリはステートレスな構造のため、1つ目のdynoに接続したクライアントは、2つ目のdynoに接続したクライアントが送信したメッセージを受け取れません。Redisはメッセージの状態をグローバルなストレージに保存することで、この問題を解決します。

HerokuでのRedis設定

リモートのRedisインスタンスとのCLIセッションを確立するには、heroku redis:cliを使用します。インスタンスを指定しない場合は、REDIS_URLに配置されているインスタンスがデフォルトで選択されます。複数のインスタンスを持っている場合は、接続先のインスタンスを明示的に指定してください。

アプリケーションサーバー側ではどう動く?

各dynoがRedisのpub-subシステムを使うように設定します。すべてのdynoは同じチャンネル(デフォルト)を購読し、メッセージを待ち受けます。各サーバーはメッセージを受信すると、それを接続中のクライアントへ配信します。

なお、subscribeはブロッキング関数のため、メッセージ待機中は実行フローが止まります。そのため購読処理は別スレッドで実行します。また、一度ある接続でsubscribeコマンドを発行すると、その接続ではunsubscribeまたはメッセージ受信しかできないため、Redisへの2本目の接続が必要になる点にも注意しましょう。スケーリングの詳細については、関連ドキュメントを参照してください。

セキュリティについて

現状のアプリケーションは、多くの攻撃にさらされる無防備な状態です。本番運用の前には、WSS(WebSocket Secure)の設定と入力値のサニタイズを必ず行ってください。WebSocketセキュリティについては、専用のガイド資料も参考になります。

まとめ

Hotwireは新規アプリケーションで使えるだけでなく、既存のTurbolinksからのアップグレードにも対応しています。

本記事では、「マジックフレームワーク」と呼ばれるHotwireの仕組みを理解し、実際に小さなアプリケーションを構築するところまで体験しました。統合されたTurbo Streamによるリクエスト処理と、WebSocketによる操作・ナビゲーションを組み合わせれば、JavaScriptをほとんど書かずとも、高速でインタラクティブなWebアプリが実現できます。まずは自分のRailsプロジェクトにHotwireを導入して、その開発体験の変化を感じてみてください。

  1. RailsでTailwind CSSを使う方法|導入から実践的なスタイリングまで徹底解説

    CSSは魔法のような存在ですが、同時に時間のかかる作業でもあります。美しく、機能的で、アクセシブルなサイトは使っていて心地よいものですが、自分でCSSを一から書くのは骨の折れる仕事です。近年はBootstrapをはじめとする多くのCSSフレームワークが登場し、その中でもTailwind CSSは特に注目を集めています。 RailsにはTailwindが標準搭載されていませんが、この記事では新しいRuby on RailsプロジェクトにTailwind CSSを追加する方法を解説します。これにより、デザイン実装にかかる時間を大幅に節約できるでしょう。さらに、Tailwindのユーティリティクラス

  2. Rails5でのAngularの使用

    あなたは前にその話を聞いたことがあります。分散型で完全に機能するバックエンドAPIと、通常のツールセットで作成されたフロントエンドで実行されているアプリケーションがすでにあります。 次に、Angularに移動します。または、AngularをRailsプロジェクトと統合する方法を探しているだけかもしれません。これは、この方法を好むためです。私たちはあなたを責めません。 このようなアプローチを使用すると、両方の世界を活用して、たとえばRailsとAngularのどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた