Railsの現在地 / Rails以外からTurbo(Hotwire)を使う
この記事はnote株式会社 Advent Calendar 2022 9日目の記事です。
Railsの現在地
DHHらは、ビジネスモデルが確定していないレベルの初期のスタートアップにおいて、複雑なSPAでのページレンダリングを採用したWebシステムを構築することはオーバーエンジニアリングでは? というところに、強い課題意識を持っているように見える。フロントエンド、サーバサイド問わず複雑なアーキテクチャにしすぎると生産性悪くない?というわけだ。あのTwitter社ですら儲かるか儲からないかわからないレベルの黎明期にはその思想に賛同してかRailsを採用していた。
近年DHHらは、RailsベースのGmailっぽいプロダクトをリリースしていて、新進気鋭のフレームワークであるHotwire(Turbo)を積んだRails 7でGmailの「あの操作性」を実現できるレベルに進化させた。ReactやVue.jsなどのライブラリを使わずとも実現できることを示した。
以下は個人的に面白かった読み物
Turboの概観
TurboはRails 7で導入されたHotwireフレームワークの一部である。公式サイトの謳い文句には、"The speed of a single-page web application without having to write any JavaScript."(シングルページアプリケーションの速度をJavaScriptを書くことなく実現する)とある。要は今までのRailsの仕組みに乗った上でJavaScriptを書かずに、SPAっぽいアプリが書けるライブラリである。Turboをアプリに導入すると、こちらも公式サイトにあるように、"Turbo Drive accelerates links and form submissions by negating the need for full page reloads."(ページのフルリロードをせずに、リンクのクリックやフォーム送信を高速化できる)、というメリットがある。簡単に言うと、Turboがブラウザで動作してリンクのクリックやフォーム送信を監視することで、ページの部分更新を可能にする仕組みらしい。
turboがすべてのコアとなる実体のライブラリになっている(TypeScript製)。turbo-rails はRails用の薄いヘルパーライブラリで、turboのインストール、Turbo Frameタグの生成メソッドや、ActionController用のturboレスポンス生成メソッドを自動でいい感じに使えるようにしてくれる。stimulus は、turboで生成したHTMLページのDOMを動的に書き換える役割を持つ。DHHらはturboとstimulusをあわせてHotwireと呼んでいるものと思われるが、この2つは別々に動作するので片方づつ使っても良い(今までJQuery(久しぶりに聞いた)でDOM書き換えしていたところを、stimulusで置き換えたりできる。)。gem turbo-railsはRails専用のgemとなっているので、その他のフレームワークで使う場合にはturbo単品で使うことになる。
turboの中の仕組みは、株式会社万葉さんのnoteが詳しいのでぜひご一読されたい。
TurboをRails以外から使う
turboは、Rails以外の環境でも単品のライブラリとして使うことができる。本節では、Ruby製のWebフレームワークであるSinatraから、turboの機能を使ってみることにする。つくるのはなんの変哲もないSinatraアプリで、サーバ側で用意したviews配下のerbテンプレートをレンダリングして返す。
$ tree
.
├── Gemfile
├── Gemfile.lock
├── README.md
├── app.rb
└── views
├── index.erb
└── new.erb
1 directory, 6 files
$ cat Gemfile
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "sinatra"
$ bundle exec ruby app.rb
[2022-11-30 16:05:57] INFO WEBrick 1.4.4
[2022-11-30 16:05:57] INFO ruby 2.6.8 (2021-07-07) [universal.x86_64-darwin21]
== Sinatra (v2.2.1) has taken the stage on 4567 for development with backup from WEBrick
[2022-11-30 16:05:57] INFO WEBrick::HTTPServer#start: pid=53569 port=4567
こうすると昔ながらのチャットアプリ(掲示板?)っぽい画面が起動する。
準備
まずTurboをアプリから使えるするようにする必要がある。webpackなどのJSのbundlerを使ってjsファイルを固めた上でクライアントに配信する方式を使ってもよいが、面倒なので、CDNからライブラリを直に読み込むことにする。正味これだけでTurboが利用できるようになる。
# in views/index.erb
<html>
<head>
<meta charset="utf-8" />
<title>Turbo Sinatra</title>
<script type="module">
import hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo';
</script>
</head>
Turbo Frameで画面を分割する
Turbo FrameはTurboの主要な機能の一つで、ページを構成するHTML要素をTurbo Frameなる単位に<turbo-frame>タグで分割する。(他にもメリットがあるが)分割したFrame単位でのページ書き換えが可能になる。
以下は先述の一覧画面のリンクがおいてある部分だが、通常リンクを踏むと画面遷移が発生してしまう。しかし、Turboがブラウザで動作している状態で、<turbo-frame> タグで囲われたリンクをブラウザがリクエストすると、Turboがブラウザのリクエスト(/new に対するGETリクエスト)とレスポンスをインターセプトして、ページの一部だけ(当該<turbo-frame>の部分)を書き換える。
# in views/index.erb
<turbo-frame id="new-page">
<div style="display: flex;">
Hello, I am good boy.
<a href="/new"> Enter room page </a>
</div>
</turbo-frame>
# in views/new.erb
<turbo-frame id="new-page">
hogehoge
<form action="/enter_room" method="post" url="">
<input type="text" name="name"> </input>
<input type="submit" value="enter"> </input>
</form>
</turbo>
# in app.rb
require "sinatra"
get "/" do
erb :index
end
get "/new" do
erb :new
end
結果的にページ遷移は発生せずに、room入室要素が一覧画面にレンダリングされた。この場合ほぼサーバサイドでの挙動の修正は必要なく、既存のHTMLパーツを<turbo-frame>で囲えば、部分更新、そのままならページ遷移となるので無駄がない。
Turbo Streamで画面をいろいろ更新する
Turbo Frameでの画面の部分更新は、リクエストをインターセプトしての更新となるので、特定のTurbo Frame一箇所のみにとどまっていた。Turbo StreamはHTML要素(text/html)ではなく、Turboオリジナルのコンテントタイプ(text/vnd.turbo-stream.html)を返却することで、Turboに複数のHTML要素の更新を指示することができる。
# in app.rb
post "/enter_room" do
content_type "text/vnd.turbo-stream.html; charset=utf-8"
custom_tag = <<~EOS
<turbo-stream action="append" target="messages">
<template>
<div id="method">
This div will be appended to the element with the DOM ID "messages".
</div>
</template>
</turbo-stream>
<turbo-stream action="replace" target="count">
<template>
<div id="count">
999
</div>
</template>
</turbo-stream>
EOS
body custom_tag
end
先述のコードは、roomの作成リクエストハンドル処理である。まず、HTTPヘッダーにcontent-type: text/vnd.turbo-stream.htmlを付与する。さらにTurbo独自のカスタムタグ<turbo-stream>要素をbodyに含むことで、ブラウザ側でTurboが画面の要素をよしなに更新してくれる。<turbo-stream>タグはアクションが色々定義されていて、「まるまる要素を置き換える」「要素を後ろにくっつける」など、動的なページ変更に必要な動作が用意されている。この例では、messages というidの要素の末尾にdiv要素を追加しつつ、countというidの要素を新しいdivタグで置き換えている。
この仕組みは秀逸だと感じた。RailsのActionControllerであれば、コンテントネゴシエーションによって、返却するレスポンスを切り替える機能があるので、開発初期はSSRで生のHTMLを返し、次にTurboでSPAっぽい挙動に変更、最後にJSONを返すAPIの穴を開けてSPA化するといった、「徐々にアプリを進化させていくこと」が容易になると思った。
諸々所感
先述の通りTurboはframework agnosticでRails以外からも利用できるが、あらゆる面でRailsで快適に使えるように周辺gemのレイヤーが最適化されていてRailsで使うのが圧倒的に利便性・生産性が高いと思われる。例えばgem turbo-rails を導入すると、ActionControllerに自動的にturboレスポンス生成用のメソッドが生えるなど。別の言語やFWからTurboを利用するには、turbo-rails gemと同じようなものを用意する必要があって、じゃあそのFWの機能を使えばいいじゃん(JSON返してNuxtなりNextなりでSPAを作ればいいじゃん)となってしまうと思った。
Turbo FrameやTurbo Streamでサーバサイドでレンダリングされるページの分割やパーツ化を効率化したが、本格的にSPAライクなアプリを開発するなら、UIをパーツ(コンポーネント)化し管理するライブラリ(Storybook的なやつ)が必要かもしれない。この辺はview_componentが担っていくのかもしれない。最近ではview_componentをStorybookっぽく表示できるgemもでてきているらしい。
まとめ
TurboはDHHが持っていたRailsの思想を継承し、スモールスタート -> ビッググロースのスタートアップ向けのFWに進化させたものであるといえる。「ReactはVueよりパフォーマンスがよい」とか「今時ページ遷移はユーザー体験が悪い」などの言説は度々Web界隈で見られる。要はプロダクトやビジネスモデルが先で、適切な技術を適切なレベルで使おうよそういうふうにRailsを改造してみたよみんな使ってね的な、現代のWeb技術に対するDHHなりの提案なのではないかとRails 7の進化を見て筆者は思ったのであった。