はじめに
この記事はRUNTEQ Advent Calendar 2022 14日目のものです。
はじめまして!プログラミングスクールRUNTEQにて、未経験からエンジニア転職を目指して学習中の柴犬です。🐶
「これからポートフォリオを作成するぞ!!」「Hotwireって何ができるの?」となってる方々が興味を持ってくれたらいいなぁと思って書いてみました。
Rails7やHotwireについては下記の「事前知識」に軽くまとめました。必要ない方はスキップして下さい🙏
事前知識
Rails6から7の変更点
-
JavaScriptバンドラーの変更
Rails6でデフォルトだったwebpacker
からRails7ではimportmap-rails
を使うことになりました。importmap-railsによってJavaScriptのトランスパイルやビルドを必要とせず、CDN経由でライブラリを取得できるようになりました。
つまりWebpackやYarn、npmに依存せず開発ができる。一方でReactやTypeScriptの相性は良くないみたいですimportmapを試してみたい方はこちらが参考になります。
-
Hotwireの導入
Hotwireを簡単に説明すると、JavaScriptをあまり使わずにSPA(Single Page Application)を実現するための仕組みです。※その他の変更点は自身で調べてみて下さい🙇♂️
Rails 7: importmap-rails gem README(翻訳)
Rails 7.0 で標準になった importmap-rails とは何なのか?
Hotwireとは?
Html Over The WIREの略です。必殺技みたいでかっこいいですね!!
ReactやVueではクライアントからのリクエストに対して、サーバー側がJSON
を返し、それを受け取ったクライアントがDOMを構築してHTMLをレンダリングします。
一方、Hotwireではクライアントからのリクエストに対して、サーバー側がHTML
を返すので、クライアント側でレンダリングする必要はありません。
つまり、開発者はサーバーサイドのみに集中できます。🙌
Hotwireは以下の技術の総称です。(※今回はTurbo
だけ使いました。)
- Turbo(Web開発向け)
- Turbo Drive
- Turbo Frames
- Turbo Streams
- Turbo Native(モバイルアプリ開発向け)
- Stimulus(Web開発向け)
- Strada(モバイルアプリ開発向け)
※詳細は割愛します。気になる方はこちら
Hotwireとは?
Hotwireとは何なのか?
作ったもの
※RUNTEQ Advent Calendar 2022 12/7日担当の方の記事
とテーマが被ってしまった(いや、ほんと偶然です笑)ため、一部割愛します。
以下の機能をもった画像投稿アプリ
・投稿の編集
・投稿の削除
・ページネーション(無限スクロール)
・検索機能
開発環境・使用技術
・M1 macbookAir 2020
・ruby3.1.0
・rails 7.0.4
・esbuild
・node 16.18.1
・yarn 1.3.2
・tailwind css
・daisyUI
セットアップ(ほぼ自分用のメモです🙇♂️)
rails new
rails new post-hotwire -c tailwind -j esbuild -d postgresql -skip-jbuilder –skip-action-mailbox –skip-action-text -MCT
rails new時のオプション
-j esbuild
importmapではdaisyUIが使えません。(2022/12/14時点)
webpacker(shakapacker)やesbuildを使う必要があるのですが、今回は後者を使いました。(脱Node.jsならず)
JavaScriptバンドラーを指定せずに、-c tailwind
とするとデフォルトでimportmap-railsが使われます。
今回はesbuildを指定しているのでcssbuilding-rails
経由でtailwind cssをインストールしています。
cssを指定せずにrails new後、tailwindcss-rails
をインストールするとscaffold実行時に勝手にレイアウトを整えてくれるのでより簡単です。
-C
Turbo Streams(WebSocket)を使う際は内部的にRedisとAction Cableを使うので注意しましょう。
DB作成
bin/rails db:create
Created database 'post-hotwire_development'
Created database 'post-hotwire_test'
tailwindの公式プラグインをインストール
yarn add @tailwindcss/typography @tailwindcss/forms @tailwindcss/line-clamp @tailwindcss/aspect-ratio
daisyuiをインストール
yarn add daisyui
tailwindの設定を追加
module.exports = {
content: [
'./app/views/**/*.html.erb',
'./app/helpers/**/*.rb',
'./app/assets/stylesheets/**/*.css',
'./app/javascript/**/*.js'
],
// 以下を追記
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/line-clamp'),
require('@tailwindcss/aspect-ratio'),
require('@tailwindcss/typography'),
require("daisyui")
]
}
各機能の説明
さて、ここからが本番です。(前置きが長い?アーニャさんのように大人の余裕を持ちましょう!)
各機能でURLが変わっていないことにも注目してくださいね!🔎
1. 投稿の編集
# 一部省略
def edit; end
def update
if @post.update(post_params)
flash.now.notice = '投稿を更新しました。'
else
flash.now.alert = '投稿を更新できませんでした。'
render :edit, status: :unprocessable_entity
end
end
if節がtrueのとき、Railsがよしなにupdate.turbo_stream.erb
をrender
してくれます。
<%= turbo_stream.replace @post %>
<%= turbo_stream_flash %>
上記を省略せずに書くと
<%= turbo_stream.replace dom_id(@post) do %> <%#= dom_id(@post)で"post_○"を指す =%>
<%= render partial: "posts/post", locals: { payment: @payment } %>
<% end %>
<%= turbo_stream_flash %>
こうなります。Railsのよしな力高すぎんか?
replace
でid="post_○"が一致するものを書き換えます。
<%# 長くなるので省略 %>
<%= turbo_frame_tag post do %>
<%= image_tag post.image %>
<%= post.title %>
<%= post.content %>
<% end %>
<!-- 上記コードで生成されるHTML -->
<turbo_frame id="post_○">
<img src="○○">
タイトル
内容
</turbo_frame>
turbo_stream_flash
はフラッシュメッセージを表示する独自メソッドとして実装しました。
module ApplicationHelper
def turbo_stream_flash
turbo_stream.update 'flash', partial: 'shared/flash'
end
end
2. 投稿の削除
def destroy
@post.destroy!
flash.now.notice = '投稿を削除しました。'
end
<%= turbo_stream.remove @post %>
<%= turbo_stream_flash %>
理屈は編集のときと同じです。
remove
で指定したidがつけられているturb_frameタグ
で囲まれた投稿を削除します。
※ちなみに新規投稿の際は同じ理屈でprependメソッドを使いました。
3. ページネーション(無限スクロール)
gem kaminari
を使って実装しました。
<%= turbo_frame_tag "posts-page-#{@posts.current_page}" do %>
<% if @search_posts %>
<%= render @search_posts %>
<% else %>
<%= render @posts %>
<% end %>
<%= turbo_frame_tag "posts-page-#{@posts.next_page}", loading: :lazy, src: path_to_next_page(@posts) %>
<% end %>
path_to_nex_page(@posts)
はkaminariのメソッドで/posts2/
など次のページのパスを返します。
src
属性を指定すると、/posts2/のページロード時ににマッチした部分を置き換えます。
さらにloading
オプションをlazy
にするとページロード時ではなくタグがページに現れたときに読み込むようになります。
以上の流れで無限スクロールを実装しています。(難しい…)
https://techracho.bpsinc.jp/hachi8833/2021_09_17/111148
https://turbo.hotwired.dev/reference/frames#lazy-loaded-frame
4. 検索
gem ransack
を使って実装しました。
class ApplicationController < ActionController::Base
before_action :set_search
def set_search
@search = Post.ransack(params[:q])
@search_posts = @search.result.order(created_at: :desc).page(params[:page]).per(12)
end
end
<%# 一部抜粋&省略 %>
<%= search_form_for @search, html: { data: { turbo_frame: "posts-list" } }, url: posts_path do |f| %>
<%= f.search_field :title_or_content_cont, placeholder: "検索ワード" ,class: "input input-bordered" %>
<%= f.submit class: "btn btn-ghost btn-circle" %>
<% end %>
<%= turbo_frame_tag "posts-list" do %>
<%# 全てのコードをturbo_frame_tagで囲む %>
<% end %>
index.html.erb内のコードは全てturbo_frame_tag
で囲み、id="posts-list"としているので、検索を実行するとindex.html全てが検索結果の投稿に置き換わります。
最後に
今回は「なんかかっこいいから!!」全部SPAにしてみました。(浅っ!!!)
実際には開発スピード重視で一部にHotwireを導入する使い方で良いと思います。
VueやReactほど細かいことはできませんが、Hotwierを使えばある程度同じ機能は簡単に実現できそうです。
これこそRailsの思想に基づいたフロントエンド開発って感じがして個人的に好きです。笑
「バックエンドはRails、フロントエンドはReact(Next.js)でSPA作ってやるぜ!!」「だけど、なるべく時間をかけたくない…」「開発効率もUI/UXも重視ししたい…」
そこのあなた、Hotwireを使って快適なRailsライフを送ってみませんか?