Alpine.jsがそれなりによい、という話
業務でAlpine.jsを使いはじめたので所感などを書いておく。
前回の記事で、業務でRustを使いはじめたと書いたが、引き続きRuby on Railsのアプリケーションも触っている。とあるプロダクトで、Alpine.jsを使ってみている。
コンテキスト
以下の前提がある。
- Ruby on Railsのアプリケーション
- まだProduct Market Fitを探している段階で、変更が常に発生する
- 業務アプリケーションで、C向けではない
以上より、初期的にはJSについてはフレームワークを導入せず、Ruby on Railsの通常のviewのレンダリングを使って、素朴なJavaScriptだけで実装してきた。むしろ、それで済むように仕様側を調整しつつ対応してきた。
ただ、顧客の要件が見えてくるにつれて、フォームがリッチになってきており、たとえば、あるフォーム群を「種別」をあらわすセレクトボックスの内容できりかえる、などの実装も増えてきた。そうなると素朴なJavaScriptの手続き的なコードではだいぶつらくなってくる。簡単な具体例をあげておくと、
<select name="category" id="category-select-form">
<option value="A">
<option value="B">
<option value="C">
</select>
<div class="for-a">
<input type="text">
</div>
<div class="for-b">
<input type="text">
</div>
<div class="for-c">
<input type="text">
</div>
const categorySelectForm = document.querySelector(".category-select-form");
const formsForA = document.querySelector(".for-a");
const formsForB = document.querySelector(".for-b");
const formsForC = document.querySelector(".for-c");
.addEventListener("change", (event) => {
categorySelectFormif (event.target.value === "A") {
.classList.remove("hidden");
formForA.classList.append("hidden");
formForA.classList.append("hidden");
formForAelse if (event.target.value === "B") {
} // ...
} })
のようなイメージで、これだけでも結構手間がかかることがわかるかと思う。実際にはさらに条件が複雑で、種別がAで、選択肢XがYのとき、など、2つ以上の条件を考慮しないといけないことも多い。また、あるフォームの内容に基づいて他のフォームの内容を補完する、などの制御も必要になっている。
検討
以上のような状況を解決するために、JavaScriptのフレームワークを導入することにした。導入にあたって、以下の点を重視した。
- 少人数で開発しているので、SPAとして別に開発などは行わない。あくまで主役はRailsのアプリケーションとなる。
- 上と共通しているが、Ruby on Railsとの統合性を考えたい。Railsのviewの拡張のような形で使えるようにしたい。
- あまり独自の概念を学習しないといけないようなものは避けたい。
- 手続き的なコードでのフォームの切り替えがわずらわしいことを解消したいので、リアクティブで宣言的な書き方ができるようにしたい。
このような観点で考えて、以下のフレームワークを検討した。
- React
- いわずと知れた、現在のデファクトのようなフレームワークだが、Railsとの統合の観点など今回重視したい観点からするとあまりマッチしていなさそうに思えた。
- Vue
- 今の会社はVueのSPAでのプロダクトがメインになっているので、会社との親和性も高そうであり、上の観点もそれなりに満たしていそうに思えたため候補の一つとして検討した。
- Hotwire
- 最近のRailsといえばこれだが、ちょっと書いてみたところあまりリアクティブに書けるとはいえず、既存のペインポイントが解消されるのかかなり疑問だったため見送った。
- Alpine.js
- リアクティブであるものの、覚えることがかなり少なく、とっつきやすそうだった。
- HTMLに埋め込むような形での運用がメインの機能の一つになるので、Railsとも統合できそうだった。
最終的に、VueとAlpine.jsを検討して、今回はAlpine.jsのメリットが勝ちそうだ、と判断した。
Alpine.jsとは
- 覚えることがかなり少ない
- “15 attributes, 6 properties, and 2 methods”
- HTMLの属性で表示制御などを行える
- 公式ページの冒頭の例にもあるが
このような形で書ける。VueやReactを触ったことがある人なら雰囲気でわかるかと思うが、buttonをクリックすることで<div x-data="{ open: false }"> <button @click="open = true">Expand</button> <span x-show="open"> Content...</span> </div>
open
という内部的なデータがfalse
からtrue
になり、span
の内容が表示される。
使ってみた所感
たとえば、冒頭の例をAlpine.jsを使って書き換えてみると、
<div x-data="{ category: 'A' }">
<select name="category" x-model="category">
<option value="A">
<option value="B">
<option value="C">
</select>
<div x-show="category === 'A'">
<input type="text">
</div>
<div x-show="category === 'B'">
<input type="text">
</div>
<div x-show="category === 'C'">
<input type="text">
</div>
</div>
となり、JSのコードは不要となる。既存コードを一部このように書き換えてみたところ、
- だいぶJSのコードが少なくなった。
- 上の例のようにHTMLに直接表示するかどうかのロジックは書けるようになるので、JS側のロジックは減った。
- 宣言的に書けるので、見通しがよくなった。
- 手続き的に「Aの場合はXXXを表示してYYYを非表示にして…」のようにするよりは
x-if
で書くほうがだいぶ見通しがよい。
- 手続き的に「Aの場合はXXXを表示してYYYを非表示にして…」のようにするよりは
- 既存のviewをそのまま使えるので書いた資産が無駄にならない。
- ある程度複雑なこともできる
ComponentとしてJSやTSファイルに書き出すやりかたもできる。
x-data
にメソッドを定義しておくと、それをclick
のタイミングなどに呼出すようにできる。たとえば、
.data('sampleComponent', (initData) => { Alpineopen: false, toggle() { this.open = !this.open; }; })
としておき、
<div x-data="sampleComponent"> <button @click="toggle()"> <div x-show="open"> Content</div> </div>
のように書くことができる。HTMLに直接大量のコードを書くのが気持ち悪い場合はこのようにしてロジックをまとめられる。
プラグイン機構があり、たとえば、 Mask pluginをいれると、
x-mask
でフォームのフォーマットを強制できるなど、ちょっとリッチなこともできるようになっている
総じて、おおむねよかった。もちろんReactやVueのような複雑なことはできないので、そこは割り切りが必要になる。そのため、万人におすすめできるものではないと思う。ただ、今回のような特定の状況にはよくマッチし、いわば「貧者のリアクティブフレームワーク」としてはだいぶよいのでは、と感じた。