はじめに

jQueryの代替になるらしいAlpine.jsが気になっていたので、手元で試してみました。Todoアプリを段階的に作りながら学んでいく形で進めたのですが、思った以上にサクッと動いて面白かったので、その過程をまとめておきます。

Alpine.js とは

公式サイトでは以下のように紹介されています。

A rugged, minimal tool for composing behavior directly in your markup.

HTMLに直接振る舞いを書き込むための、堅牢で最小限のツール、という位置づけです。「Think of it like jQuery for the modern web」とも書かれていて、モダンなjQueryの後継というポジショニングのようです。

導入はCDNのscriptタグを1行足すだけで完了します。

1<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>

ビルドツールやnpmは不要で、これだけで使い始められます。構成要素は15個の属性、6個のプロパティ、2個のメソッドとかなりコンパクトな印象です。

基本のディレクティブを試す

Alpine.jsでは x- プレフィックスが付いたHTML属性を「ディレクティブ」と呼びます。まずはカウンターから始めて、Todoアプリに発展させていきました。

カウンター — x-data / x-text / @click

最初に作ったのはシンプルなカウンターです。

1<div x-data="{ count: 0 }">
2  <p>カウント: <strong x-text="count"></strong></p>
3  <button @click="count++">+1</button>
4  <button @click="count--">-1</button>
5  <button @click="count = 0">リセット</button>
6</div>

x-data がコンポーネントの状態を定義する起点で、ここがAlpineのキモです。この div の中でだけ count へアクセスできるスコープになっています。x-text でテキストをバインドし、@clickx-on:click の省略形)でイベントを処理する形です。

Todoリスト — x-model / x-for

カウンターの次に、入力フォームとリスト描画を足してTodoアプリにしました。

1<div x-data="{
2  newTodo: '',
3  todos: [],
4  addTodo() {
5    if (this.newTodo.trim() === '') return
6    this.todos.push({ text: this.newTodo.trim(), done: false })
7    this.newTodo = ''
8  },
9  removeTodo(i) { this.todos.splice(i, 1) },
10  get remaining() { return this.todos.filter(t => !t.done).length }
11}">
12  <input x-model="newTodo" @keydown.enter="addTodo()" placeholder="やることを入力...">
13  <button @click="addTodo()">追加</button>
14
15  <ul>
16    <template x-for="(todo, i) in todos" :key="i">
17      <li>
18        <input type="checkbox" x-model="todo.done">
19        <span x-text="todo.text" :class="todo.done && 'done'"></span>
20        <button @click="removeTodo(i)">削除</button>
21      </li>
22    </template>
23  </ul>
24
25  <div x-show="todos.length > 0" x-transition>
26    残り <strong x-text="remaining"></strong> 件
27  </div>
28</div>

x-data の中身が増えましたが、構造は同じです。プロパティとメソッドを持つJavaScriptオブジェクトを渡しているだけです。this で同じオブジェクト内のプロパティにアクセスできます。

新しく使ったディレクティブを整理するとこうなります。

ディレクティブ役割
x-modelinputの値とデータの双方向バインド
x-for配列をループして要素を描画。<template> タグ上で使う
x-show条件が false なら display: none にする
x-transitionx-show の切替にアニメーションを付ける
:class条件に応じてCSSクラスを切り替え

get remaining() はJavaScript標準のゲッター構文です。プロパティとしてアクセスされた時に毎回関数が走る仕組みで、Alpine固有の機能ではありません。

jQuery と何が違うのか

正直、カウンターやTodo程度の規模だと「jQuery でもよくない?」と感じました。そこで同じTodoアプリをjQueryとAlpineで並べて書いてみたのですが、違いが見えてきました。

jQuery(命令的)

1$('#add').click(function() {
2  var text = $('#input').val().trim()
3  if (!text) return
4  todos.push({ text: text, done: false })
5  $('#input').val('')
6  render()  // 手動で再描画
7})
8
9$('#list').on('change', 'input', function() {
10  todos[$(this).closest('li').index()].done = !todos[$(this).closest('li').index()].done
11  render()  // 手動で再描画
12})
13
14$('#list').on('click', '.del', function() {
15  todos.splice($(this).closest('li').index(), 1)
16  render()  // 手動で再描画
17})
18
19// DOM を全部自分で組み立てる
20function render() {
21  $('#list').empty()
22  todos.forEach(function(t) {
23    // li, checkbox, span, button を手動で生成...
24  })
25  // 統計表示も手動で更新...
26}

Alpine(宣言的)

1<input x-model="newTodo" @keydown.enter="addTodo()">
2<template x-for="(todo, i) in todos">
3  <li>
4    <input type="checkbox" x-model="todo.done">
5    <span x-text="todo.text" :class="todo.done && 'done'"></span>
6  </li>
7</template>
8<div x-show="todos.length > 0">
9  残り <strong x-text="remaining"></strong> 件
10</div>

核心的な違いは render() 関数の有無です。jQueryは状態を変えるたびに「DOMを自分で直せ」と手動で再描画しますが、Alpineは「状態を変えたら画面は任せろ」という設計です。イベント3箇所すべてに render() を書く必要がなくなります。

機能が増えるほど手動で面倒を見る箇所がjQueryでは膨れていくので、ある程度の規模になるとこの差は大きいなという印象を持ちました。

設計思想

公式ドキュメントや作者の発信から、Alpine.jsの設計思想を整理してみました。

HTMLファーストと局所性

Tailwind CSSがCSSをHTMLの属性に書くように、AlpineはJSをHTMLの属性に書く。どちらも「関心の分離(HTML/CSS/JSを別ファイルに分ける)」より「局所性(関連するものを近くに置く)」を優先する思想です。

JSファイルを別に書かなくてもいいのは地味にうれしいポイントでした。HTMLを見れば振る舞いが全部わかるので、コードの行き来が減ります。

最後に

Alpine.jsを触ってみた感想としては、軽量でいいなというのが率直な印象です。jQueryよりモダンなのはわかったし、ビルド不要で使えるのもうれしい。状態管理を意識しなくていいのは楽でした。

個人的にはAI時代における「局所性」の価値にも注目しています。AIにコードを書かせる場合、Reactのように状態・コンポーネント・スタイルが複数ファイルに散らばっていると、コンテキストを渡すのが大変です。Alpineやhtmxのように1つのHTMLで完結する構造は、人間だけでなくAIにとっても扱いやすいのではないでしょうか。

使いどころの整理としてはこんなイメージです。

レベル選択肢
手動DOM更新で十分jQuery
手軽にリアクティブにしたいAlpine.js
本格的なSPAを作るReact / Vue

コンテンツ中心のページにちょっとした対話性を足す用途であれば、Alpineは良い選択肢だなという印象を持ちました。htmxとの比較や併用も気になるので、また気が向いたら遊んでみたいところです。