この記事は、リレーブログ企画「24卒リレーブログ」の記事です。
はじめに
初めまして、新卒1年目の緑川です。
現在、OJTジョブローテの1期目として、会員システムグループの第二開発チームに所属しています。主な業務はオプションサービスの開発と運用であり、フロントエンド・バックエンド問わず様々な技術を日々学んでいます。
開発にあたってSvelteというフレームワークを使用する場面があり、そのためにSvelteチュートリアルをやってみたので、この記事ではそこで重要だと思った点や難しかった点、知らなかった用語などを自分なりの理解でまとめていきたいと思います。あくまでチュートリアルの説明の補足のようなものなので、そちらを読んでいる前提で執筆します。また、各項目の解説の密度は、自分の理解度に依拠しているために偏りがあり、一部の項は一纏めにしています。
なお、チュートリアルはSvelteとSvelteKit(Svelteの拡張版のようなもの。今回は割愛)それぞれの基本と応用とで4つのPartに分かれていますが、今回はPart1:Basic Svelteをやりました。
Svelteとは
まずSvelteについて説明します。Svelteとは、前述のチュートリアルによれば、
web アプリケーションを構築するためのツールです。他のユーザーインターフェースフレームワークと同様、マークアップ(markup)、スタイル(styles)、振る舞い(behaviours) を組み合わせたコンポーネントでアプリを 宣言的(declaratively) に構築することができます。
Svelte を理解するには、HTML、CSS、JavaScript の基本的な知識が必要です。
https://learn.svelte.jp/tutorial/welcome-to-svelte
だそうです。少し使ってみた感じではHTMLとJS(JavaScript)をシームレスに融合させ、その上簡単に書けるようにしたものといった印象を受けました。自分はJSについても詳しくはないので、本記事で解説する単語の中にはJSのものも含まれています。
Introduction
Svelteにようこそ
ここから解説に入っていきます。
Svelteチュートリアルは以下のような画面で進行します。
左上がファイル構造を表しています。右上がファイルの中身で、ここを変更するとリアルタイムで下の表示に反映されます。
- フットプリント…プログラムが動作する際のメモリ使用量の多さ
- コンポーネント…次項で解説される
- オーバーヘッド…プログラムが行う作業のために間接的に生じる余計な負荷
Your first component, Dynamic attributes
ここでは、変数やコードを中括弧で埋め込むというSvelteの基本的な動作が解説されています。変数の中身にはテキストだけでなく画像も使用することができます。
Styling
これは要するにCSSです。
Nested components
この項目はSvelteを学ぶにあたって重要です。この後もほぼ全ての項でコンポーネントのインポートが行われます。
HTML tags
@html
によってHTMLタグを使うことができます。これは後述のBindings/Textarea inputsでも出てきます。
- DOM…Document Object Modelの略。Webページの要素やコンテンツなどをツリー構造で表現したもの
- brob…Binary Large Objectの略。単にバイナリデータの塊を表現したもの
- サニタイズ…脆弱性を突こうとする入力を別の表記に置き換え無害化することを指す。 英語では「消毒する」などの意味がある
Reactivity
Assignments
カウントを増やす関数と、カウントを表示するテキストを作り、「クリックした時にこの関数を実行する」とボタンに設定することでそれらを結びつけています。
Declarations, Statements, Updating arrays and objects
他の変数に依存する変数は、$:
をつければ自動的に連動計算してくれます。また、$:
は変数だけでなく文やブロックなどにも付与できます。ただし、連動計算は代入によって起こるものであり、リストに追加する場合はもう一工夫必要だそうです。
このリアクティブ宣言という概念は開発でよく使う印象があります。
- statements…プログラミングにおける文のこと
Props
Declaring props, Default values, Spread props
Introduction/Stylingの項などでコンポーネント同士の独立性についての話がありましたが、もちろんデータの受け渡しも可能です。受け渡しにあたってはimport
とexport
をセットで使用します。これも開発には欠かせないものです。
プロパティはデフォルト値も設定することができ、しない場合はundefinedになります。また、渡す変数が複数あり、かつその数と名前が一致していれば、一気に代入できます。
Logic
If blocks, Else blocks, Else-if blocks
他のあらゆる言語、フレームワークと同様、if、else、else-ifも実装されています。これらのブロックは#
にはじまり/
に終わります。elseのような間に挟むものには:
がつきます。
HTMLでこのような処理を実装しようとするとJSを組み込む必要がありますが、Svelteであれば地続きの感覚で記述できます。
Each blocks
#
にはじまり/
に終わるのはifと同じです。配列やそれに類するものをあらかじめ宣言しておけば、eachブロックを利用して一気に代入できます。
Keyed each blocks
ここは少し理解に時間がかかった部分です。
まず初期化時に、Thing
コンポーネントにthings
の中身が渡され、emoji
はthings
のname
に対応したものがセットされます。things
が決めているのはname
とコンポーネントの数なので、things = things.slice(1);
によって配列の先頭を消すと、コンポーネントはbanana
〜egg
の4つになります。しかし、emoji
を決めているのはThing
であり初期化時点から動かないこと、そしてコンポーネントは末尾から消える法則があることから、ズレが生じるようです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Thing.svelte <script> const emojis = { apple: '🍎', banana: '🍌', carrot: '🥕', doughnut: '🍩', egg: '🥚' }; export let name; const emoji = emojis[name]; </script> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// App.svelte <script> import Thing from './Thing.svelte'; let things = [ { id: 1, name: 'apple' }, { id: 2, name: 'banana' }, { id: 3, name: 'carrot' }, { id: 4, name: 'doughnut' }, { id: 5, name: 'egg' } ]; function handleClick() { things = things.slice(1); } </script> <button on:click={handleClick}> Remove first thing </button> {#each things as thing (thing.id)} <Thing name={thing.name} /> {/each} |
shift
メソッドを使わずslice
メソッドを用いているのは、shiftがsliceと違って配列を直接変更するものだからです。Reactivity/Updating arrays and objectsで述べられたように、リアクティビティは代入によってトリガーされるため、shift
メソッドを使う場合は以下のように記述する必要があり、冗長になります。
1 2 |
things.shift(); things = things; |
Await blocks
utils.jsのgetRandomNumber()
は、まず/random-number
というAPIエンドポイントにリクエストを送信して結果が帰るまで待ち、レスポンスが成功した場合レスポンスの本文をテキストとして取得するのを待ち、それをApp.svelteに渡します。res.ok
は通常、HTTPステータスコードが200-299の範囲内にある場合にtrueだそうです。また、waitingメッセージを表示するため、APIにはあえて遅延が組み込まれているようです。エンドポイントの実装はサーバサイド(バックエンド)で行われており、ここからは見えません。
1 2 3 4 5 6 7 8 9 10 11 |
// utils.js export async function getRandomNumber() { const res = await fetch('/random-number'); if (res.ok) { return await res.text(); } else { throw new Error('Request failed'); } } |
App.svelteはlet promise = getRandomNumber();
と#await promise
で、一連の処理が終わるのを待ちます。{number}
があるのにlet number
などがないのは不思議に思えますが、{:then number}
がgetRandomNumber()
の返した値をnumber
として割り振っているのだと思われます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// App.svelte <script> import { getRandomNumber } from './utils.js'; let promise = getRandomNumber(); function handleClick() { promise = getRandomNumber(); } </script> {#await promise} <p>...waiting</p> {:then number} <p>The number is {number}</p> {:catch error} <p style="color: red">{error.message}</p> {/await} |
Events
DOM events, Inline handlers, Event modifiers
Reactivity/Assignmentsではbutton
にイベントハンドラを付与していましたが、div
に付与することもできるそうです。インラインで宣言することも、修飾子をつけることで条件・属性を追加することもできます。
Component events
Inner.svelteの1行目にimport { createEventDispatcher } from 'svelte';
とありますが、これは今までのように他のファイルからではなくsvelteパッケージから関数をインストールしているということです。
ここでは、ボタンを押すとsayHello
関数が発火し(実行され)、sayHello
関数はmessage
イベントを発生させています。App.svelteの方では、message
イベントが発生したのを受けてhandleMessage
関数が発火しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Inner.svelte <script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); function sayHello() { dispatch('message', { text: 'Hello!' }); } </script> <button on:click={sayHello}> Click to say hello </button> |
- イベントディスパッチャ…イベントを送信するもの。イベントを受信するものはイベントリスナー
Event forwarding, DOM event forwarding
コンポーネントのイベント(前項Component eventsで作ったもの)はバブルしないため、Component eventsでは2層だった構造が3、4層と多層化する場合、中間層はフォワードする(イベントを受け渡す)必要があります。
また、今までbutton
にイベントハンドラを設定する機会が何度かありましたが、もう一つファイルを用意する必要があるような凝ったbutton
の場合は、ファイルをまたぐためにイベントフォワーディングを使用するようです。
- バブルする(バブリング)…イベントが発生した要素から、親要素を通って最上位の要素まで順番に伝わっていくこと
Bindings
Text inputs
Svelteでは、基本的に変数の更新に応じてその変数を使用するコンポーネントも更新されるという処理がされます。Reactivity/Assignmentsの時も、button
コンポーネントに足し算関数を発火させるインベントハンドラを定義することで遠回りに表示を更新していました。これも似たようなもので、bind
を使ってinput
からname
を変更し、name
の変更がh1
に反映されるという処理が行われています。
1 2 3 4 5 6 7 |
<script> let name = 'world'; </script> <input bind:value={name} /> <h1>Hello {name}!</h1> |
Numeric inputs, Checkbox inputs
number
が数値ボックスでrange
が数値バーです。「input.value
を使わなければならない」という話は、前項の説明の「on:input
イベントハンドラを追加し〜」という箇所と同じ話です。
チェックボックスもほとんど同じ感覚でbind
を使用できます。
- numeric…数値という意味
- <label>…htmlの要素の一つ。主にフォーム部品を関連づけるために使う
Select bindings
on:change={() => (answer = '')
は、セレクトボックスで新しい選択肢を選んだ(=質問を切り替えた)時に、答えの部分を空にするという意味です。{selected ? selected.id: '[waiting...]'}
は、selected
が真値である(なんらかの質問を選択している)場合selected.id
が表示され、そうでない場合[waiting...]
が表示されるということを示しています。ここでは、bind
をつけないとselected
が真値になりません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<form on:submit|preventDefault={handleSubmit}> <select bind:value={selected} on:change={() => (answer = '')} > {#each questions as question} <option value={question}> {question.text} </option> {/each} </select> <input bind:value={answer} /> <button disabled={!answer} type="submit"> Submit </button> </form> <p> selected question {selected ? selected.id : '[waiting...]'} </p> |
Group inputs, Select multiple
Group inputsで使用したチェックボックスは、<select multiple>
に置き換えることもできるそうです。こうすると記述が短く分かりやすくなりますが、その代わり、NOTEに書かれているように複数選択する際に少し工夫が必要になります。
- Intl.ListFormat…リストを整形するオブジェクト。オプションを指定することで最終的に表示するものをA, B and Cのような形式にしている
Textarea inputs
import { marked } from 'marked'
でマークダウンをHTMLに変換するためのライブラリmarked
をインポートし、@html marked(value)
でHTMLに変換した文字列value
をHTMLとして解釈しています。
1 2 3 4 5 6 7 8 9 10 11 12 |
<script> import { marked } from 'marked'; let value = `Some words are *italic*, some are **bold**\n\n- lists\n- are\n- cool`; </script> <div class="grid"> input <textarea bind:value></textarea> output <div>{@html marked(value)}</div> </div> |
Lifecycle
onMount
document.querySelector('canvas')
では、ドキュメント内の最初の<canvas>
要素を選択しています。この方法で HTML 内の<canvas>
要素を操作できるようになります。canvas.getContext('2d')
では、選択した<canvas>
要素の描画コンテキストを取得しています('2d'
は 2D 描画コンテキスト)。このコンテキストを通じて、<canvas>
上に図形や線を描いたり、色を塗ったりすることができます。「コンポーネントが破棄されてもloopが動く」とは、<canvas>
を消すと画像が見た目の上では消えるものの、cancelAnimationFrame
がないとバックで処理が継続してしまうということです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// App.svelte <script> import { onMount } from 'svelte'; import { paint } from './gradient.js'; onMount(() => { const canvas = document.querySelector('canvas'); const context = canvas.getContext('2d'); let frame = requestAnimationFrame(function loop(t) { frame = requestAnimationFrame(loop); paint(context, t); }); return () => { cancelAnimationFrame(frame); }; }); </script> |
- svg…画像フォーマットの一種。大きさを自由に変えられる
beforeUpdate and afterUpdate
scrollableDistance = div.scrollHeight - div.offsetHeight
でスクロール可能な総距離を計算し、div.scrollTop > scrollableDistance - 20
で現在のスクロール位置が下端から20ピクセル以内にあるかチェックしています。div.scrollTo(0, div.scrollHeight)
は要素を最下部にスクロールするという意味です。
1 2 3 4 5 6 7 8 9 10 11 12 |
beforeUpdate(() => { if (div) { const scrollableDistance = div.scrollHeight - div.offsetHeight; autoscroll = div.scrollTop > scrollableDistance - 20; } }); afterUpdate(() => { if (autoscroll) { div.scrollTo(0, div.scrollHeight); } }); |
- 状態駆動…アプリケーションの状態(データ)に基づいてUIを自動的に更新する方法。Svelteは基本的に状態駆動の考え方に基づいているが、スクロール位置の取得やスクロールの実行はDOMの直接操作を必要とするため実現が難しい
tick
テキストが新しい値に変更されると、古いテキストを選択している情報は失われます。<script>
の末尾のthis.selectionStart = selectionStart;
とthis.selectionEnd = selectionEnd;
の2行は、現在の選択範囲を過去の選択範囲と同じにするというもので、テキスト変更後も選択範囲を維持しようとしています。しかし、そのままだとテキストの変更がこの2行より後になってしまうので意味がなく、したがってtickが必要となります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<script> let text = `Select some text and hit the tab key to toggle uppercase`; async function handleKeydown(event) { if (event.key !== 'Tab') return; event.preventDefault(); const { selectionStart, selectionEnd, value } = this; const selection = value.slice(selectionStart, selectionEnd); const replacement = /[a-z]/.test(selection) ? selection.toUpperCase() : selection.toLowerCase(); text = value.slice(0, selectionStart) + replacement + value.slice(selectionEnd); this.selectionStart = selectionStart; this.selectionEnd = selectionEnd; } </script> |
Stores
Writable stores, Auto-subscriptions, Readable stores
Storeにおいて明示的にsubscribe
するならばunsubscribe
も必要です。$
記法(自動サブスクリプション)を使えば自動的にリアクティブな更新をしてくれて、そしてコンポーネントが破棄されるときに自動的にunsubscribe
も行ってくれます。Reactivity/Declarationsなどで使用したリアクティブ宣言と自動サブスクリプションは似て非なるものです。
また、Storeは用途によって書き込み可能にしたり読み取りのみ可能にしたりできます。
- subscribe…データの変更を監視し始めること。unsubscribeはその逆で、不要になった監視を停止すること
- リアクティブ宣言…
$:
を使用。依存する値が変更されるたびに自動的に再計算される。コンポーネント内のローカルな状態・計算に使う - 自動サブスクリプション…
$
を使用。Storeの値が変更されるたびに自動的に更新される。グローバルな状態(Store)にアクセスするために使う
Derived stores
他のStoreを参照するStoreです。export const 【exportする変数名】 = derived(【参照するStore】,($【参照するStore】) =>【計算ロジック】);
という形式で記述します。参照するStoreを2回書いていますが、1回目はどのStoreから派生するかを指定していて、2回目では実際のStoreの値にアクセスしています。
1 2 3 4 5 6 |
//stores.js export const elapsed = derived( time, ($time) => Math.round(($time - start) / 1000) ); |
Custom stores
countという値自体に増減などのロジックを組み込んでいます。increment: () => update((n) => n + 1)
はJSのオブジェクトリテラル記法におけるメソッド定義の短縮構文で、increment: function() { return update((n) => n + 1); }
と同じ意味です。
1 2 3 4 5 6 7 8 9 10 11 12 |
// stores.js function createCount() { const { subscribe, set, update } = writable(0); return { subscribe, increment: () => update((n) => n + 1), decrement: () => update((n) => n - 1), reset: () => set(0) }; } |
Store bindings
書き込み可能なStoreはbind
したりコンポーネント内で直接代入することも可能という話です。${$name}
は、$
が二つ続いていてわかりづらいですが、中の$name
がこのStoresの項で学んだことで、外の${}
はテンプレートリテラル(JSの文字列を作成するための機能)です。"Hello " + $name + "!”
と同じです。
1 2 3 |
// stores.js export const greeting = derived(name, ($name) => `Hello ${$name}!`); |
まとめ
今回はSvelteチュートリアルのPart1を体験して学んだことをまとめてみました。評判通り、学習コストが低めで書きやすいという印象でした。まだまだ学習中の身であるため、誤った理解をしている部分もあるかもしれませんが、Svelteの基本を習得しスキルアップした実感を持っています。
Svelteは人気上昇中のフレームワークなので、フロントエンド系の開発に携わっている方は今の内に学んでおくと将来様々な場面で役に立つと思います。自分も学習を続けたいと考えており、次はSvelteKitの基礎であるPart3に挑戦する予定です。
リレーブログ企画「24卒リレーブログ」は、この記事で終了となります。執筆に協力していただいた皆さん、見てくださった皆さん、ありがとうございました。