HotwireはWebアプリケーションを作るための新しいアプローチ

前島真一氏:Hotwireについて話します。前島です。ハンドルネームはwillnetや、netwillnetです。iCAREさんをはじめとして、いろいろな会社で技術顧問をしています。空いた時間を使って、「savanna.io」というお仕事情報SNSを開発しています。savanna.ioは、これから話すHotwireを利用して作っています。

Hotwireがどんなものかを簡単に説明します。Hotwireは、Basecampという会社が作っているjs(JavaScript)フレームワークです。Basecamp社はRailsの作成者のDHHがCTOをしている会社です。

Basecamp社はメールアプリケーションの「hey.com」というサービスを作っているんですが、それはHotwireを使って作られています。

Hotwireという名前のライブラリがあるような印象を受けがちですが、実際は複数のライブラリを統合した総称みたいな感じですね。これから紹介するTurboやStimulusなどのライブラリが実態です。

公式のページの文章を意訳したものですが、「JSONとたくさんのJavaScriptを使わずに、HTMLを送ってモダンなWebアプリケーションを作るためのアプローチ」というふうに書かれています。

jsのフレームワークは、基本的にはJSONを使うのが一般的だと思います。サーバーサイドはJSONをがんばって返して、フロントエンドはそれを受け取って、がんばって解釈して、DOMを構築して、最終的にHTMLにするという流れになるのかなと思います。

すべての画面の状況をjsが管理することになるので、UIを細かく作り込めるのがメリットかと思います。

すごいんですけど、でもそれってけっこう大変なんじゃないですかね? と個人的には思っています。JSONを返すサーバーサイド側と、それを受け取ってHTMLを作るクライアントサイド側の両方にロジックが必要になります。

最近のサービスはブラウザだけで完結することが少なくて、iOSやAndroidなどのアプリが必要になってくることも多いですよね。そうするとサーバーサイドエンジニア、ブラウザのフロントエンドのエンジニア、iOSのエンジニア、Androidのエンジニアといったように関わる人が増えてきます。

Hotwireを使うメリット

Hotwireは、基本的にはサーバーサイド側でがんばります。そうすると、サーバーサイド側にロジックが寄ってきて、クライアントサイドのコード自体は最小限に抑えることができます。

jsをあまり使わないWebアプリケーションと同じのように見えますが、Hotwireを使うと、SPA(Single Page Application)などユーザー体験が良くできるのがメリットです。

少ない人数で開発を進めていけて、もしかしたら1つのチームで全部を担当できる可能性もある。サーバーサイドでHTMLを作るので、HTMLを作れる言語であればどんな言語でも使えるというのはメリットかと思います。

HotwireはBasecamp社製のフレームワークなので、Rails前提なんじゃないの? と思っている人も多いと思うんですけど。Hotwire自体は、Railsにかかわらずどんな言語、どんなフレームワークでも使えるように作られています。

ただHotwireを実際のアプリケーションとして使うためには、ヘルパーメソッドのようなものがいっぱい必要になります。Rails以外で実現しようとすると、自前でそれを書かなければいけません。Railsに関しては、turbo-railsみたいなgemがあって、追加でなにかを書かずにHotwireを使うことができます。

もちろんHotwireはメリットばかりではありません。VueやReactなどのjsフレームワークと比べると、細かいことが圧倒的にできません。HTMLを使うという性質上、iOSやAndroidアプリも基本的にwebviewを使うので、細かいことはできないです。

ですが、さっきも話したとおり、Basecamp社が作っているhey.comというサービスがあり、これはブラウザでも使えるし、iOSでもAndroidでも使えます。みなさんの中でも、使ったことがある人はいると思うんですけど、hey.comは普通に使えるアプリだと思います。

hey.comで表示できるレベルのUIで問題がないサービスは、けっこう世の中にいっぱいあると思っていて、普通に使えるフレームワークなんじゃないかなと思っています。

Hotwireを構成する3つのコンポーネント

具体的な話に移ります。Hotwire自体は、3つのコンポーネントに分かれています。TurboとStimulusとStradaです。Turboはその中でまた4つに分かれていて、Turbo Drive、Turbo Frames、Turbo Streams、Turbo Nativeというのがあります。

Stradaというコンポーネントに関しては、現時点でこういうものがあるよと公式ページに情報があるだけで、実際のフレームワークはまだ使えないので今回は話しませんが、説明を読む限りでは、カメラなどiOSやAndroidなどのネイティブで必要な機能とHTMLをシームレスにつなぐライブラリらしいです。

さっきも話したとおりHotwireはRailsに限らず使うことができます。ただturbo-railsなどRailsを前提にしたツールがあるので、Railsだと簡単にHotwireを使い始められます。なので今回は、このturbo-railsを使っている前提で話をします。

もう1つ、具体的な話を始める前に注意点を話しておきます。fetch APIや、custom elementsや、Intersection Observer APIなど、古いブラウザでは動かないAPIをいっぱい使っているので、IEなどの古いブラウザで動かすのはけっこう大変だという印象があります。

このへんは、polyfillをがんばって入れることでできるとは思っているのですが、まあ、やっぱり大変かなと思っています。

iOSに関しても、バージョン12以上をサポートしている状況で、すぐ12を廃止して13以上にするよと公式のreadmeに書かれていたので、Hotwireを使いたい場合は、基本的にモダンな環境で動かせるアプリケーションを前提とするのがいいのかなと思います。

ページ遷移を速くするTurbolinks

具体的な話をTurboからしていきます。Turboの中には、構成要素の1つにTurbo Driveというものがあります。このTurbo Driveを一言で説明するとすれば、Turbolinksを改善させたものと言えると思います。

Turbolinksって何? という人もいると思うのですが、今日お話を聞いている人は名前を聞いたことがある、もしくは使ったことがある人が大半だと思います。というのも、TurbolinksはRails4.0から標準のライブラリとなっているからです。

これは、すべてのリンクをAjaxで置き換えるという挙動をします。Ajaxで受け取った遷移先のHTMLのうち、bodyの要素だけを現行のbodyの要素と差し替えてページを遷移したことにするというライブラリです。

通常のページ遷移は、HTMLを全部持ってきてパース(解析)して、そのあとにjsやcssのダウンロードをして、それをパース(解析)して、最終的にページが表示されます。

Turbolinksを使うと、最初のページの取得のときだけjsやcssのダウンロードとパース(解析)があって、そのあとのページ遷移ではjsやcssのダウンロードとパース(解析)をスキップできます。これにより、ページ遷移が速くなるのが特徴ですね。

過去Turbolinksが使われなかった理由

ただ、TurbolinksがRailsの標準ライブラリになったからといって、みんなあんまり使っていないですよね。きっと使っていない人のほうが多いんじゃないかなと思います。

Rails4.0が出たのは7年くらい前なんですけど、当時はjQueryやjQueryプラグインが全盛の時代でした。jQueryプラグインは、もちろんTurbolinksを前提として作られてはいないので、一緒には動かないということがよくあって、動かすためにはいろいろとしなければいけない状況でした。

また、Turbolinksを使ってjsを書いたり、テストをしたりするためには、Turbolinksがどのように動いているのかをきちんと知っておく必要がありました。

例えば、通常のページロードは、最初の1回だけが起きるので、DOMContentLoadedみたいなイベントも最初の1回しか発火しない。画面の遷移をするときは、Turbolinksが用意しているイベントが発火するので、それを見る必要がありました。

ほかにもE2Eのテストで、次のページに遷移をして、なにかするということはよくあると思うんですけど、次のページに遷移したことがテスト側からはわからないので、明示的に次のページに存在しているDOM要素をウォッチして判断をする必要があります。

Turbolinksという名前のとおり、リンクをturboにするライブラリなので、formに関しては半分くらいのサポートになっていました。formに関して、普通のリンクと同じようなAjaxでやりたかったら、ある程度は各自で対応するのが必要でした。

以上のことから、ハマる人が多くて、Turbolinksをうまく使えずに削除する、ということがよく行われていたのではないかと思います。

ただ、7年くらい経って状況が変わってきて、SPAのアプリケーションを開発したことがある人も増えていると思います。なので、TurbolinksがSPAっぽいからハマるという人は減っていると思います。

少ない労力でSPAのようなことができるというメリットはまだまだ残っているので、使えるライブラリだと思います。

ちょっと話が逸れるんですけど、今Turbolinksを使っていて、Turboが出たから使ってみようと思った人がいたとします。どうやって移行するか、ドキュメントがない状況なんですけど、実際に試してみて僕がいろいろとハマった点を最近ブログに書いたので、興味がある人はこちらを見てください。

Turbolinksの仕組みと挙動

具体的な話です。Turbo DriveでTurbolinksが改善されました。なにが大きく改善されたかと言うと、formに関してもturbo仕様になりました。

Railsでformを作るときには、form_withというヘルパーメソッドを使うことが多いと思うんですが、これまではTurbolinksを使っているという前提で、デフォルトでformタグの中にdata-remote="true"という属性がついていました。この属性があると、formが自動でAjax仕様になります。

これはTurbolinksのTurbolinks Wayで開発するときにはいいんですが、Turbolinksをオフにしたときにもこれが残って、なぜかformだけAjaxになってうまく動かないことがありました。

Turbo Driveがリリースされて、form_withのdata-remote="true"属性が必須ではなくなったことによって、Rails6.1から、data-remote="true"はデフォルトでオフになりました。これで混乱する人は減ると思います。

Turbo Driveを使ってformでサブミットのボタンを押したときの挙動について、書きました。302などのレスポンスが返るとリダイレクトして、400番台や500番台のレスポンスが返ると、レスポンスボディのボディタグの中身を差し替えて、ページの履歴自体はそのままという挙動です。

400番台が返ったときの挙動をTurbolinksを使ってきちんとやろうとすると、SJRと呼ばれる手動の手法を使う必要がありました。

SJRって何? という話だと思うので、例を挙げます。例えば、Postのcreateアクションでバリデーションエラーが起きたとします。そのときにpost/create.js.erbをレンダリングします。この中で何をやっているかと言うと、shared/renderという部分テンプレートをレンダリングして、jsとして、errorというidを持つDOMのinnerHTMLとしてアサインします。

これをサーバー側から返すと、rails-ujsがこのjsの内容をevalして実行してくれて、errorというidを持っているDOMの中にエラーメッセージが入るという流れです。

サーバーでjsを返して、それをevalすると聞くと、セキュリティ的に大丈夫なの? という気持ちになりますよね。エラーメッセージを表示するレベルなら大丈夫だと思うんですが、「お、SJR便利じゃん」とカジュアルに使い始めると、セキュリティホールにつながる気がします。

実際TurboやTurbolinksを使ったときに、SJR的なことをやりたいという中で、やりたいことは数種類に限られるんじゃないかなと思います。特定のDOMにHTMLを挿入するとか、差し替えるとか、削除するとか、そういうことができれば、開発は十分できると思います。それを提供するのが、次に紹介するTurbo Streamsです。

(次回につづく)