はじめてのSvelte - 基礎から応用まで
読む所要時間: 35 分
背景
JavaScriptのフロントエンド開発に関わる技術は移り変わりが目まぐるしく、日々新しいツールやフレームワークが誕生しています。Svelteも近年注目を集めているフロントエンドフレームワークの一つです。2021年のStackOverFlowの開発者サーベイでは、Svelteが開発者から最も愛されているツールとして選ばれました。本稿では、Svelteの基礎的な開発方法から、Storeを使った状態管理やアニメーションの適用方法など、応用までご紹介します。
目標
このチュートリアルを最後まで進めると、Svelteの基礎から応用までを学べるとともに、以下のようなタスク管理アプリを作成できます。
Svelteとは?
Svelteは、ブラウザ上で動作するユーザーインターフェースを作成するためのオープンソースフレームワークです。
ReactやVue.jsとの違い
SvelteはReactやVue.jsと同様に、ユーザーインターフェースを作成するためのコンポーネントベースのJavaScriptフレームワークです。
しかし、SvelteとReactやVue.jsには決定的な違いがあります。Svelteは仮想DOMを使用しません。SvelteはTypeScriptで書かれたコンパイラであり、ビルド時にJavaScriptをコンパイルするのに対し、ReactやVue.jsは実行時に仮想DOMを使ってアプリケーションコードを解釈します。
Svelteとほかのフレームワークの違いについて詳しくは、ブログ記事「SvelteとReactの基本を比較」を参照してください。
Svelteを使うメリット
Svelteを使うメリットは以下のとおりです。
仮想DOMを使用しない
Svelteの最大のメリットは、コンパイラであるため、仮想DOMを使用しないことです。DOMの差分確認をするアルゴリズムは効率的ですが、実行コストが高くなります。Svelteは、コードをバニラJavaScript (*1)にコンパイルするため、アプリケーションの高速化とパフォーマンスの向上が期待できます。
(*1)ライブラリやフレームワークを使わないでJavaScriptを書くこと。
軽量で高パフォーマンス
Svelteは、高度に最適化されたバニラJavaScriptを生成します。実行時のフレームワークのオーバーヘッドがほぼなくなり、バンドルサイズやメモリ使用量が最低限で済みます。このため、アプリケーションのロードと実行の高速化を実現できます。
真のリアクティブシステム
Svelteはリアクティブに作られています。Svelteは、仮想DOMなどの仲介者を必要とせずにDOMを直接変更するバニラJavaScriptを生成します。また、リアクティブなStoreがビルトインで装備されているので、状態管理専用ライブラリを使用する必要がありません。変数の値を変更するだけでUIに反映されます。
記述するコードを少なくできる
ライフサイクルフックなどのボイラープレートを記述する必要がなく、また状態管理専用ライブラリを使用する必要がないため、記述するコードがほかのフレームワークより少なくて済みます。これにより、コードの可読性を上げることができます。
アニメーションやエフェクトがビルトインで装備されている
Svelteにはアニメーションやエフェクトが組み込まれており、洗練されたUIでインタラクティブな動作を簡単に実装できます。
アプリケーションの構造
Svelteの概要がわかったところで、本稿で作成するアプリケーションの構造をご紹介します。
App
: アプリケーションの実行エントリーポイントとなるルートコンポーネント。このコンポーネントでユーザーが氏名を入力する画面を表示します。ToDoInputForm
: タスクを新規追加する入力フォームのためのコンポーネント。ToDoList
: タスクの表示、チェックボックスとタスク削除機能のためのコンポーネント。
一般的に、このようなシンプルで小規模なアプリケーションでは3つもコンポーネントを準備する必要はなく、Appコンポーネントひとつで全ての動作をまかなうことが多いです。今回はコンポーネント間のデータ共有について学習するために、意図的に細かくコンポーネントを分けています。
必要なツール
Svelteプロジェクトを作成する
まずはSvelteプロジェクトを作成しましょう。degitと呼ばれるGitリポジトリをコピーするツールを使用してプロジェクトを作成します。
ターミナルを開き、任意のディレクトリで以下のコマンドを実行します。
このコマンドで、my-svelte-projectフォルダを作成し、Svelteのデフォルトのプロジェクトテンプレートをフォルダ内にダウンロードします。cd
コマンドで新しく作成したプロジェクトに移動します。
次に、プロジェクトで必要な依存パッケージをダウンロードします。以下のコマンドを実行します。
このコマンドで、package.jsonに含まれるdependencies
とdevDependencies
をnode_modules
内にインストールします。
package.jsonを開くと、以下のようなパッケージが確認できます。
ここで、Svelteがコンパイラーだということが改めて読み取れます。コンパイラーでは、アプリケーションコードはビルド時にバニラJavaScriptに変換されます。このため、ほとんどのパッケージは開発時に使用するパッケージであるdevDependencies
に格納されています。
devDependencies
に含まれるrollup
は、Svelteで使用されるモジュールバンドラーです。rollupはWebpackのように、モジュール式のコードを、ブラウザが容易に解析してUIに表示できるようなファイルにまとめます。.svelte
ファイル、ファイルに含まれるモジュール、propsなどを、HTML、CSS、JavaScriptファイルに変換します。
degitでは、デフォルトでhttp://localhost:5000/
が使用されます。macOS Monterey以降を使用している場合は、AirPlay Receiverがポート5000を使用しているので正しく動作しません。この問題を防ぐために、package.json
のstart
スクリプトで別のポートを使用するように変更します。package.json
のstart
スクリプトを以下のように変更します。
お使いの環境により、--port
の後に来るポート番号を変更してください。
この例ではhttp://localhost:8080でアプリケーションにアクセスできます。
次に開発サーバーを起動します。ターミナルで以下のコマンドを実行します。
実行すると、以下のようなアウトプットがターミナルに表示されます。
アウトプットから、アプリケーションがポート8080で起動していることがわかります。http://localhost:8080にアクセスすると、以下のスタート画面が表示されます。
- node_modules: package.jsonに記載されている
devDependencies
とdependencies
が含まれています。 - public: Svelteがコンパイル処理で出力したリソースが含まれるフォルダです。build/bundle.jsとbuild/bundle.cssには最適化されたJavaScriptとCSSが含まれています。bundle.jsとbundle.cssはpublic/index.htmlでインポートしています。
- src: アプリケーションのコンポーネントを格納するフォルダ。アプリケーションのスタート地点となるmain.jsとApp.svelteがデフォルトで含まれています。新規のコンポーネントをこのフォルダに格納します。
Svelteコンポーネントの仕組みをさらに詳しく解説します。まずはsrc/main.jsを見てみましょう。
main.jsはSvelteアプリケーションのスタート地点です。アプリケーションのメインコンポーネントであるApp.svelteをインポートし、新しいApp
インスタンスを作成しています。app
で設定オブジェクトを作成し、target
とprops
を渡しています。
target
はdocument.body
に設定されており、App
コンポーネントで生成されたHTMLをアプリケーションのdocument.body
に格納することを指定しています。props
はApp
コンポーネントに渡すプロパティを含むオブジェクトを割り当てています。
本稿では、このprops: { name: 'world'}
を使用しないので、オブジェクトごと削除してください。
次に、App.svelteを見てみましょう。
このように、Svelteのコンポーネントは<script>
、HTMLテンプレート、<style>
の3部分で構成されています。
<script>
には、コンポーネントのJavaScriptコードを記述します。App.svelteでは、name
変数をプロパティとしてエクスポートしています。本稿ではname
は使用しないので、後ほど削除します。
HTMLテンプレートには、コンポーネントのHTMLを生成するためのマークアップコードを含めます。<style>
にはコンポーネントにのみスコープされるCSSを含めます。
新しいコンポーネントを作成する
アプリケーションの構造が理解できたところで、新規のコンポーネントを作成しましょう。
srcフォルダ配下にToDoInputForm.svelteファイルを作成してください。
Propsと算出プロパティ
SPAでは、データを複数のコンポーネントで共有する際、親コンポーネントから子コンポーネントにデータを渡すことが一般的です。この導線はProps(Properties)と呼ばれるデータを宣言して実装します。
Svelteでは、<script>
ブロック内でexport
キーワードを使用して変数を宣言するだけで、データを他のコンポーネントに渡すことができます。本稿では、ToDoInputFormコンポーネントにuserName
変数を追加し、ユーザー名を他のコンポーネントに共有できるようにします。
ToDoInputForm.svelteに以下のコードを追加します。
ここでは、「{ユーザー名}のタスクリスト:」というテキストをヘッダーとして出力します。
このコンポーネントをアプリケーション上で表示するには、メインコンポーネントであるApp.svelteでToDoInputForm.svelteコンポーネントを参照する必要があります。
App.svelteを以下のコードに変更します。
<script>
ブロックとHTMLテンプレートでToDoInputForm
コンポーネントを参照しています。<ToDoInputForm />
タグをHTMLテンプレートに含めることにより、ToDoInputFormコンポーネントのHTMLがAppコンポーネントのHTMLに含まれるようにします。
Svelteでは算出プロパティもサポートされています。
App.svelteの<script>
ブロックとHTMLテンプレートの内容を以下のように変更します。
算出プロパティはリアクティブです。lastName
やfirstName
などの算出プロパティに依存する変数が変更された場合、userName
の値が自動的に更新されます。
ファイルを保存し、npm run dev
を実行して再度アプリケーションを実行してください。
ブラウザで動作確認に使用したポート(例: http://localhost:8080)にアクセスすると、以下の画面が表示されます。
App.svelteで、
<script>
ブロック内のlastName
とfirstName
の値を以下に変更し、ファイルを保存します。
すると、算出プロパティが値の変更を検知し、以下のようにユーザー名が切り替わります。
ここではIf-elseロジックの効果がわかりやすいよう、一旦簡素なHTMLを適用しています。このHTMLは後ほど変更します。
App.svelteの<script>
ブロックとHTMLテンプレートの内容を以下のように変更します。
ファイルを保存してください。
このコードでは、「ユーザーが氏名を入力したかどうか」という状態をnameEntered
で管理しています。ユーザーが氏名を入力したら、nameEntered
はtrue
になります。nameEntered
がtrue
の場合は名前の入力画面を表示し、false
であればタスクリストを表示します。このレスポンシブな動作を、{#if [条件]} ... {:else} ... {/if}
で実装します。
また、if...else
に加えてelse if
も使用したい場合は、{:else if [条件]}
で実装できます。詳しくは、Svelte 公式ドキュメントを参照してください。
ブラウザで動作確認に使用したポート(例: http://localhost:8080)を開くと、{:else}
の条件が満たされ、氏名の入力画面が表示されます。
フォーム送信機能は後ほど追加していきますが、この時点で
if...else
が機能しているかを確認しましょう。nameEntered
の値をtrue
に変更し、ファイルを保存します。すると、{#if}
の条件が満たされ、タスクリスト画面が表示されます。nameEntered
の値を再びfalse
に戻し、ファイルを保存します。
双方向データバインディング
次に、データバインディングを導入します。通常、Svelteアプリケーションでは親から子へデータを渡すトップダウンの構造を取ります。しかし、フォームなど複数の方向からデータを更新したい場合もあります。このような変更で、データバインディングが便利です。データバインディングを使用すると、属性の値を設定し、その属性の変更を検知し反応するリスナーを設定できます。
このデータバインディングを、氏名入力フォームで使用します。App.svelteのHTMLテンプレートの<input>
フォームを以下のように変更します。
input
要素に入力される値は、{lastName}
や{firstName}
変数に紐付けられます。この紐付けにより、データを双方向で同期させることができます。
これまでユーザーの氏名は「山田太郎
」をハードコードしていましたが、初期値を空文字列に設定し、ユーザーがフォームで入力した値でユーザー名を更新するようにします。
App.svelteの<script>
ブロック内のlastName
とfirstName
の値を以下のように変更します。
初期状態では、lastName
とfirstName
の値が空文字で、ユーザーが入力要素の値を変更すると、それに応じて値が更新されるようになりました。
イベント
次に、Svelteでのイベントの実装方法を学習します。フォームでユーザーが氏名を入力後、入力内容を送信するためのボタンを実装します。
App.svelteのHTMLテンプレートの{:else}
ブロックの内容を以下のように変更します。
ここでは、氏名入力後に入力内容を送信し、タスクリスト画面に進むためのボタンをクリックしたときのsubmit
イベントを検知し、イベントに対しての反応を定義しています。
フォームにhandleSubmit
イベントハンドラー関数とsubmit
イベントを紐付けています。ユーザーが「タスク管理を始める」ボタンをクリックすると、handleSubmit
関数が呼び出され、イベントのハンドリングが行われます。
また、フォームのブロック全体にon:submit|preventDefault={handleSubmit}
修飾子を使用することにより、handleSubmit
ハンドラーが走る前にevent.preventDefault()
を走らせます。
フォームはデフォルトの動作として、form
要素に送信先が指定されていない場合、現在のURLに対してフォームの内容を送信します。現在のURLに対してフォームの送信が行われると、ページが自動的にリロードされてしまいます。この動作を防ぐために、event.preventDefault()
を走らせます。
次に、handleSubmit
イベントハンドラーを追加しましょう。
App.svelteの<script>
ブロックの最後の行に、以下のコードを追加します。
このコードで、ボタンがクリックされ、姓と名が両方入力されていると、nameEntered
がtrue
になります。これにより、{#if nameEntered}
の条件が満たされ、タスクリストが表示されます。
ファイルを保存し、ブラウザで動作確認に使用したポート(例: http://localhost:8080)を開きます。以下のような画面が表示されます。
氏名を入力し、「タスク管理を始める」をクリックします。
画面がタスク管理画面に切り替わります。
ここでは、toDoItems
配列を宣言し、text
とstatus
のプロパティを含むオブジェクトを割り当てます。このデータを{#each}...{/each}
ブロックを用いてリスト表示します。{#each}...{/each}
の各要素が<li>
要素としてHTMLに追加されます。checkbox
バインディングを使って、status
をチェックボックスが選択される度に切り替えます。
また、class
ディレクティブの省略版であるclass:クラス名={条件}
の構文を使って、status
の値がtrue
であれば、<span>
要素のクラスがchecked
になり、ユーザーがチェックしたタスクに取り消し線のCSSが適用されるようにします。on:click
とremoveFromList
関数を紐付け、タスクを削除できるようにします。
ファイルを保存し、ブラウザで動作確認に使用したポート(例: http://localhost:8080)を開きます。氏名を入力し、タスクリスト画面に移ると以下のような画面が表示されます。
これで、タスクのチェックと削除ができるようになりました。
Store
この状態だと、タスクの削除はできますが新規での追加ができません。ToDoInputForm.svelteにタスクの入力フォームを追加します。ToDoInputFormコンポーネントで入力されたタスクのデータをToDoListコンポーネントのタスクリストに追加するには、前述したPropsを利用できます。子コンポーネントAから親コンポーネントにデータを渡し、さらに親コンポーネントから別の子コンポーネントBに渡すことにより、兄弟コンポーネントにデータを共有できます。
ですが、アプリケーションが大きくなるにつれて、このような実装は複雑化しやすくなります。この問題を解決するには、Storeが有用です。
Storeは、状態が変更した時にsubscribe
メソッドで状態を購読しているコンポーネントに一斉通知するためのオブジェクトです。
まず、状態を保存するためのファイルを作成します。/srcフォルダの配下にstore.jsを作成してください。
以下のコードをstore.jsにペーストしてください。
ここでは、svelte/store
モジュールから、書き込み可能なStoreを表すwritable
関数をインポートしています。
次に、タスクリストのデータであるtoDoItems
をstore.jsに移動し、各コンポーネントからtoDoItems
に購読するよう設定します。
store.jsを以下のように変更します。
ToDoInputForm.svelteのコードを以下のように変更します。
ここでは、store.jsから状態のデータをインポートしています。また、新規のタスクを追加するためのフォームとボタンを提供しています。
ここで、$toDoItems = [...$toDoItems, {text: newItem, status: false}]
に注目してみてください。SvelteのStoreでは、通常以下のようなsubscribe
メソッドを使用して状態変更の通知を受け取ります。
これに加えて、コンポーネントが破壊(destroyed)された際に、通知の購読を止める処理を導入する必要もあります。購読する状態が増えると、この構文を何度も繰り返すことになり、コードの量が増えていまします。ここで便利なのが、Auto subscriptionです。Auto subscriptionを使うと、$toDoItems
のように、状態の変数の前に$
を付け加えるだけで、自動的に変数の状態変化の通知を受け取ることができます。
最後に、ToDoListコンポーネントにも状態を共有します。
ToDoList.svelteを以下に変更します。
ここで行った変更は、<script>
ブロック内でtoDoItems
配列を定義する代わりにstore.jsから配列をインポートしたことと、HTMLテンプレートでtoDoItems
を$toDoItems
に変更したことのみです。
ファイルを保存し、ブラウザで動作確認に使用したポート(例: http://localhost:8080)を開きます。ユーザー名を入力し、タスクリスト画面に移ると以下のような画面が表示されます。
入力フォームが新たに追加されました。
入力フォームにタスクを追加すると、
以下のようにToDoList.svelteコンポーネントの部分にデータが共有されていることが確認できます。
ライフサイクル
Svelteを含むSPAフレームワークのコンポーネントにはonMount
から始まり、onDestroy
で終わるライフサイクルがあります。このライフサイクルの間で起こるイベントごとに様々な処理を行うことができます。
ライフサイクルのイベントで最もよく使われるのが、DOMのレンダリングが完了した直後に発生するonMount
です。
このonMount
イベントを使って、新規タスク入力フォームに自動的にフォーカスを合わせます。この機能を実装することにより、ユーザーが新規タスクを追加する際にフォームをクリックする必要がなくなります。
onMount
を使うことにより、コンポーネントのレンダリングの完了直後にフォーカスを当てることができます。
ToDoInputForm.svelteを以下のコードに変更します。
SvelteでonMount
を使用するには、svelte
モジュールからonMount
関数をインポートする必要があります。これを、<script>
ブロックのimport { onMount } from "svelte"
で実行しています。
newItemInputForm
変数を宣言し、null
に初期設定します。
bind:this
ディレクティブは、渡された変数に要素のDOMノードへの参照を設定します。このbind:this
を使って、<input>
要素にnewItemInputForm
を渡します。onMount
はコンポーネントがレンダリングされた後に実行されるため、onMount
メソッドが実行された時点ではすでに入力フォームが存在しています。このため、レンダリングが完了する前にnewItemInputForm
には入力フォームが設定されており、フォームに対して即効でfocus()
メソッドを実行できます。
ファイルを保存し、ブラウザで動作確認に使用したポート(例: http://localhost:8080)を開きます。ユーザー名を入力し、タスクリスト画面に移ると以下のような画面が表示されます。
入力フォームに青色のフォーカスが掛かっていることが確認できます。
アニメーション
最後に、アニメーション機能をご紹介します。Svelteにはデフォルトでアニメーション機能が備わっており、専用パッケージやプラグインをダウンロードする必要がありません。これにより、美しいアニメーションを、アプリケーションのサイズを肥大させずに導入することが可能です。
本稿では、ふわっと要素を出したり消したりする「フェード(fade)」と、横方向から要素を出したり消したりする「スケール(scale)」アニメーションに着目します。
そのほかにSvelteにビルトインされているアニメーションについて詳しくは、Svelte公式ドキュメントを参照してください。
フェードとスケールを使って、タスクリストに項目を追加・削除する動作にトランジションを追加します。
ToDoListコンポーネントの<script>
ブロックとHTMLテンプレートを以下に変更します。
ここでは、Svelteモジュールからtransition
フォルダに含まれるfade
とscale
関数をインポートしています。
また、タスクリストに含まれる全ての項目に対してトランジションを適用しています。Svelteでは、トランジションを追加する際にtransition
ディレクティブを使いますが、ここではその短縮系のin
とout
を使用しています。要素が追加されたときにはin
に指定されているscale
トランジションが、削除されたときにはout
に指定されているfade
トランジションが実行されます。
duration
でトランジションの速度を500msに設定しています。
ファイルを保存し、ブラウザで動作確認に使用したポート(例: http://localhost:8080)を開きます。ユーザー名を入力し、タスクリスト画面に移り、タスクリストの項目を削除したり、追加したりすると以下のようなトランジションが実行されることが確認できます。
これでタスク管理アプリケーションが完成しました。このチュートリアルに出てきたコードはGithubリポジトリでご確認いただけます。
まとめ
いかがだったでしょうか。ReactやVue.jsなどのウェブ開発フレームワークやライブラリと比較して、SvelteはJavaScriptベースの新しいアプローチを提供しています。Svelteはコンパイラーとして動作することで、高度に最適化されたJavaScript、HTML、CSSコードを生成します。
Svelteは学習も比較的容易で、ソースコードも非常に読みやすいです。初めてSvelteを試してみた方も短時間で高度なアプリケーションを作成できます。ぜひこのチュートリアルをもとに、もっと踏み込んだSvelteアプリケーション開発に挑戦してみてください。
Twilio Blogに投稿してみたい方や、フィードバック、登壇、勉強会のお誘いなど気軽にsnakajima[at]twilio.comまでご連絡ください。開発中のプロジェクトに関してはGithub(smwilk)を覗いてみて下さい。