Hotwire でモダンなウェブアプリケーションを作る
はじめに
JSON でなく HTML を送ることでモダンなウェブアプリケーションを開発できる Hotwire に少し前から興味がありました。
Hotwireとは何なのか? を始めとした日本語の情報を拾い読みしていたのですが、
ユーザにとっても、開発の進め方も Progressive Enhancement にできること
と書かれていたりして、わかったような、わからないような。
で、Hotwire を開発した DHH 氏みずから Hotwire の使い所を解説していた Podcast があって聞いてみたら、とてもわかりやすくて納得した。
» 151: DHH – Building HEY with Hotwire | Full Stack Radio
実際にデモアプリを作りながら、Hotwire がどういうものなのかを見ていきます。
クラシックなウェブアプリを作る
まずは現時点で最新の Ruby 3.0.0 & Rails 6.1.0 の環境を用意します。長らく gem install rails で最新の rails をインストールしてから rails new する方法をとってきたのですが、今回Ruby 歴 10 年の私が【絶対に】 gem install rails コマンドを実行しない理由を参考に、bundle init で作った Gemfile に rails のバージョンを指定するやり方で用意しました。
% npm install --global yarn (yarn が未インストールの場合)
% mkdir hotwire_sample
% cd hotwire_sample
% rbenv install 3.0.0 (definition not found と出た場合はその前に brew update && brew upgrade ruby-build)
% bundle init
Gemfile を開き
gem 'rails', '~> 6.1.0'
を追記したら、
% bundle install --path=vendor/bundle
% bundle exec rails new .
で rails をインストールします。
% bundle exec rails s
で rails サーバーを起動し、http://localhost:3000/ にアクセスして、「Yay! You’re on Rails!」のページが表示されればOK。
Rails でデモアプリと言えば、かつてDHHが15分でつくってみせたブログアプリです。
% bundle exec rails g scaffold Post title:string body:text
% bundle exec rails db:migrate
2005年には15分かかっていましたが、いまや一瞬でタイトルと本文が入力できるブログアプリが完成しました。
Hotwire をインストール
% bundle add hotwire-rails
% bundle exec rails hotwire:install
で hotwire をインストールします。
次に app/views/layouts/application.html.erb の head 内に
<%= turbo_include_tags %>
を追加します。
モダン化その1 - 画面遷移しないようにする
Scaffold だけでつくったブログ、僕はこれで十分便利と思うのですけれど、2021年では「いちいち画面遷移するなんてダサい、モダンじゃない」という声が聞こえてきそうです。
そこで記事を追加する New Post のページを、画面遷移せずリスト画面の下の方にそのまま表示するようにします。
iframe で New Post をはめこむようなイメージです。実際には Hotwire の Turbo Frames という機能の turbo_frame_tag を使います。
app/views/posts/index.html.erb の New Post のリンクを
<%= turbo_frame_tag 'new_post', src: new_post_path, target: :_top %>
に入れ替え、app/views/posts/new.html.erb の中身全体を turbo_frame_tag 'new_post' で囲んでしまいます。
<%= turbo_frame_tag 'new_post' do %>
<h1>New Post</h1>
<%= render 'form', post: @post %>
<%= link_to 'Back', posts_path %>
<% end %>
turbo_frame_tag に続く 'new_post' の部分にはユニークなIDを付け、同じIDの部分が対応しています。つまり、index.html.erb の turbo_frame_tag 'new_post' の部分に、app/views/posts/new.html.erb の中身全体がそのまま表示されます。
app/controllers/posts_controller.rb の def create 内で posts コントローラーの show にリダイレクトしている部分はコメントアウトし、index にリダイレクトするように変更します。
# format.html { redirect_to @post, notice: 'Post was successfully created.' }
format.html { redirect_to posts_url, notice: 'Post was successfully created.' }
これで、画面遷移せず、記事を登録できるモダンなアプリの出来上がりです。この変更には、リダイレクト先を変える以外、モデルやコントローラーの修正はほとんど必要ありません。
モダン化その2 - リアルタイムでページを更新する
ブログのアプリでその必然性はほとんどないと思うのですが、モダンなアプリというのは、データが変更されたときにリロードすることなくリアルタイムにページが更新されるものです。
2000年-2004年アメリカに住んでいたとき、サッカー日本代表チームの試合の模様をテキストで伝える実況サイトを見ていて、5秒間ごとにリロードボタンを押しながら一喜一憂していたものですが、2021年ではもうそんなことは許されません。
そこで Hotwire の Turbo Streams を使います。
これを使うには redis が必要なので
% brew install redis
で redis をインストールします。
記事が更新されたときに、更新されたことを通知するため、app/models/post.rb の Post モデルに broadcasts_to を追記します。通知先となる 'posts' は、後述する
に付ける id です。class Post < ApplicationRecord
broadcasts_to ->(_post) { 'posts' }
end
Turbo Streams を使ってデータをリアルタイムに更新するには更新する部分を partial ファイルに切り出す必要があります。ブログ記事を列挙している部分を partial ファイルに置き換えます。
新たに app/views/posts/_post.html.erb というファイルを追加し、index.html.erb から tr 部分を移してきます。id には dom_id(post) を設定します。
<tr id="<%= dom_id(post) %>">
<td><%= post.title %></td>
<td><%= post.body %></td>
<td><%= link_to 'Show', post %></td>
<td><%= link_to 'Edit', edit_post_path(post) %></td>
<td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
tr 部分を parial ファイルに移したので、app/views/posts/index.html.erb の tbody の部分は以下のようになります。
<%= turbo_stream_from 'posts' %>
<tbody id="posts">
<% @posts.each do |post| %>
<%= render post %>
<% end %>
</tbody>
turbo_stream_from 'posts' で post インスタンスに関する変更内容を WebSocket 経由で受け取れるようになります。
以上でモダン化その2が完了です。
一方のブラウザで記事の内容を更新したり、削除すると、もう一方のブラウザにすぐに反映されます。
まとめ
ここまでで、クラシックなアプリに 1) 遷移しない画面、2) リアルタイムに更新される画面、というモダンなウェブアプリケーションに特徴的な機能を追加しました。
意図的ですが、JavaScript は一行も書いていません。しかしさらに細かくモダンな機能を追加していくにはさすがに JavaScript を書く必要があり、そこは Hotwire の Stimulus を使うことになります。Stimulus の導入は若干大変なので、次の機会にでもブログで紹介しようと思います。
Hotwire を少し使ってみてわかったのは、サーバーサイドでHTMLをレンダリングするクラシックなウェブアプリケーションをベースにして、必要な箇所に Turbo Frames を使って遷移を省いたり、Turbo Streams を使ってリアルタイム性を追加したり、細かな機能追加には Stimulus を使っていくというのが Hotwire のアプローチだということでした。
冒頭の Progressive Enhancement (段階的な改善)というのは、そういうことなのだ納得しました。
Podcast での DHH 氏の解説の端々に、「Ruby 最高、全部 Ruby で書きたい。JavaScript はできるだけ書きたくない」という Ruby 愛が感じられます。2019年、東京で開催された Rails Developer Meetups の基調講演 で、DHH は、ほとんどのアプリケーションは JavaScript のフレームワークなんて必要としていない、もっとシンプルに作れるはずだと語っていました。
デモのために無理やり Turbo Streams を使ったブログ記事の更新のように、そのままで十分なところは通常のサーバーサイドレンダリングのままに、たとえばチャットの画面のように必要なところだけをモダン化していけば良いというのが DHH および Hotwire のアプローチなのだと思います。
参考
さらに詳細を知りたい場合は、Hotwire | Drifting Ruby のチュートリアル動画に沿って、チケット管理システムのデモアプリを作ってみることから始めるのが良いと思います。
2021/01/28 21:10:00
プロフィール
株式会社まちクエスト代表、つくる社LLC代表。
Scratchで楽しく学ぶ アート&サイエンス、Raspberry Piではじめる どきどきプログラミングを書きました。
オンラインコンテンツ: 大人のためのScratch
Amazonから図書館検索 Libron、iPhoneアプリ ひらがなゲーム かなぶん を作っています。
Email: webmaster at champierre dot com
Twitter @jishiha
@jishiha をフォローする最近のエントリー
- Working as a programmer (Rubyist) in an international environment while living in Tokyo
- JavaScriptとRubyとで配列内の目的のオブジェクトの前後を取得する
- モノリス vs マイクロサービス
- Scratch Day in Tokyo 2023 Show & Tell 振り返り
- ChatGPTによるGeoJSONの作成から修正まで:実体験からの感想
- いまさら Lirbon を紹介する
- IPv6 パススルー有効かどうかの見分け方
- CSV形式のオープンデータをブラウザの地図上でかんたんに確認できるglnmapsというツールを作りました
- 20歳の自分に人生のアドバイスができるとしたら、どんなメッセージを送りますか?
- PoseNet2Scratch を使っていつでもどこでも「キラーン!」と賢くなる
- 「統一教会」や「原理」といえば、思い出す大学生のときの思い出
- Rails 7アプリ on Heroku でPostGISを使い、外部APIを使わず緯度経度から県名を導く
- Kaggle
- 長男が留学でシアトルに発ってから約2週間が経とうとしている
- ファン歴30年の僕が選ぶデビュー30周年スピッツの曲ベスト10
- 手軽にVPNネットワークを構築できるTailscaleを使い、ngrokなしでローカルで開発しているサイトに外部からアクセスする
- GitHub Actions で独自 Scratch を動かす
- 英語力を手軽に測ることができる CASEC をアラフィフになって受けた話
- Hotwire でモダンなウェブアプリケーションを作る
- ファーレ立川アート、街なかに100以上パブリックアート...
アーカイブ
- 2023年12月(1)
- 2023年09月(1)
- 2023年06月(1)
- 2023年05月(1)
- 2023年02月(1)
- 2023年01月(1)
- 2022年11月(1)
- 2022年10月(2)
- 2022年09月(1)
- 2022年07月(1)
- 2022年03月(1)
- 2022年01月(1)
- 2021年09月(1)
- 2021年03月(2)
- 2021年02月(2)
- 2021年01月(1)
- 2020年11月(1)
- 2020年10月(32)
- 2020年09月(32)
- 2020年08月(2)
- 2020年06月(1)
- 2020年03月(1)
- 2020年02月(7)
- 2019年12月(1)
- 2019年10月(1)
- 2019年09月(2)
- 2019年08月(1)
- 2019年06月(1)
- 2019年05月(2)
- 2019年04月(2)
- 2019年02月(3)
- 2019年01月(3)
- 2018年11月(1)
- 2018年10月(1)
- 2018年09月(3)
- 2018年08月(4)
- 2018年07月(6)
- 2018年06月(4)
- 2018年04月(3)
- 2018年03月(1)
- 2018年02月(2)
- 2018年01月(2)
- 2017年12月(3)
- 2017年11月(3)
- 2017年08月(1)
- 2017年07月(1)
- 2017年06月(1)
- 2017年05月(4)
- 2017年04月(2)
- 2017年03月(1)
- 2017年02月(1)
- 2017年01月(3)
- 2016年12月(2)
- 2016年11月(2)
- 2016年10月(2)
- 2016年09月(1)
- 2016年07月(3)
- 2016年06月(2)
- 2016年05月(1)
- 2016年04月(1)
- 2016年03月(3)
- 2016年02月(3)
- 2016年01月(7)
- 2015年12月(3)
- 2015年08月(1)
- 2015年07月(1)
- 2015年05月(1)
- 2015年01月(2)
- 2014年12月(2)
- 2014年11月(1)
- 2014年08月(3)
- 2014年07月(3)
- 2014年06月(2)
- 2014年05月(1)
- 2014年04月(2)
- 2014年02月(1)
- 2014年01月(2)
- 2013年12月(6)
- 2013年11月(2)
- 2013年10月(5)
- 2013年09月(3)
- 2013年08月(2)
- 2013年07月(1)
- 2013年06月(4)
- 2013年04月(2)
- 2013年03月(4)
- 2013年02月(1)
- 2013年01月(3)
- 2012年12月(4)
- 2012年11月(2)
- 2012年10月(1)
- 2012年09月(2)
- 2012年08月(3)
- 2012年06月(2)
- 2012年05月(4)
- 2012年03月(2)
- 2012年02月(1)
- 2012年01月(1)
- 2011年12月(4)
- 2011年11月(2)
- 2011年10月(5)
- 2011年09月(3)
- 2011年08月(6)
- 2011年07月(2)
- 2011年05月(3)
- 2011年04月(2)
- 2011年03月(1)
- 2010年11月(2)
- 2010年07月(1)
- 2010年05月(7)
- 2010年04月(11)
- 2010年03月(3)
- 2010年02月(4)
- 2010年01月(5)
- 2009年12月(4)
- 2009年11月(5)
- 2009年10月(8)
- 2009年09月(2)
- 2009年07月(5)
- 2009年06月(4)
- 2009年05月(4)
- 2009年04月(3)
- 2009年03月(7)
- 2009年02月(3)
- 2009年01月(10)
- 2008年12月(1)
- 2008年11月(3)
- 2008年10月(2)
- 2008年09月(5)
- 2008年08月(5)
- 2008年07月(4)
- 2008年06月(11)
- 2008年05月(13)
- 2008年04月(7)
- 2008年03月(8)
- 2008年02月(3)
- 2008年01月(7)
- 2007年12月(9)
- 2007年11月(7)
- 2007年10月(6)
- 2007年09月(1)
- 2007年08月(4)
- 2007年07月(6)
- 2007年06月(8)
- 2007年05月(10)
- 2007年04月(8)
- 2007年03月(13)
- 2007年02月(10)
- 2007年01月(6)
- 2006年12月(11)
- 2006年11月(19)
- 2006年10月(18)
- 2006年09月(1)
- 2006年08月(4)
- 2006年07月(9)
- 2006年05月(1)
- 2006年03月(1)
- 2005年09月(1)
- 2005年07月(2)
- 2005年06月(7)
- 2005年05月(6)
- 2005年04月(12)
- 2005年03月(5)
- 2005年02月(6)
- 2005年01月(13)
- 2004年12月(3)
- 2004年11月(2)
- 2004年10月(4)
- 2004年09月(19)
- 2004年08月(29)
- 2004年07月(18)
- 2004年06月(11)
- 2004年05月(17)
- 2004年04月(32)
- 2004年03月(13)
- 2004年02月(11)
- 2004年01月(17)
- 2003年12月(2)
- 2003年11月(9)
- 2003年10月(10)
- 2003年09月(6)
- 2003年08月(8)
- 2003年07月(20)
- 2003年06月(21)
- 2003年05月(22)
- 2002年07月(1)
- 2002年06月(3)
- 2002年05月(3)
- 2002年04月(4)