svelteの公式チュートリアルをやってみる
The State of JS 2021を見ていて満足度100%のsvelteについて興味を持ったためチュートリアルをやりながらメモを残す。ちなみにZennはさっき登録した。初投稿。
私について
- もうすぐエンジニアになれそうな未経験
- 第一志望に内定もらえたらフロントメインのフルスタックエンジニア
- 主な使用技術はNext.js(React)
学習環境
公式チュートリアルはweb上で実行できるが、新しい書き方がVSCodeでどんな補完のされ方するのか見たいので両方使う。
Svelteとは?
チュートリアルもここから始まっているので沿ってメモしておく。ここではかなりざっくりの説明だけど要約するとこんな感じ。
- svelteは早い
- 可読性、管理性などに強いUIという点はReact、Vueと近い
- React達との違いは実行時にコードを解釈するのではなく、ビルド時にJavaScriptに変換するから早い
チュートリアル以外に書いてある特徴
ここでは早いことばっか書いてあるけどチュートリアル以外の、ネットに落ちている情報としては
- 厳密にはライブラリではなくコンパイラ
- 記述が少なく済む
- 仮想DOMを使わない(ここが早いの要因)
- 真にリアクティブ
主にこんな感じ。
環境構築
つい何日か前にSvelte Kit1.0が公開された。
公式もこれで環境作ることを推奨していたので使う。
npm create svelte@latest my-app
cd my-app
npm install
npm run dev
色々聞かれるので好みで設定。
基礎構文
<script>
//JSの記述
</script>
//HTMLの記述
<style>
//CSSの記述
</style>
Vueに寄ってる。タグの通りに書くだけ。分かりやすい。
script
style
タグが必要なければ無くてもいい。
<script>
let name = 'world';
</script>
<h1>Hello {name}!</h1>
Reactと同じく波括弧で変数を埋め込める。属性も同様。
stateは通常の変数で扱うみたい。というかstateの概念がないのかも。
img
のalt
がないと警告出してくれるみたい。
<p>This is a paragraph.</p>
<style>
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
style
にはそのままCSSを書ける。セレクタのスコープはコンポーネント内のみ適応されるので気にせず書ける。
コンポーネント分割
コンポーネントファイル名は頭文字を大文字にする。
<p>これは別の段落です。</p>
<script>
import Child from './Child.svelte';
</script>
<p>これは段落です。</p>
<Child/>
<style>
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
記述としてはscriptでインポートしてきてReact同様<Component/>
で追加。
注目すべきは、親でp
に適応したスタイルが子のp
には適応されていないこと。きちんとコンポーネント内でスコープが閉じていることが分かる。
HTMLの埋め込み
HTMLを直接埋め込むには{@html ...}
を使う。ReactのdangerouslySetInnerHTML()
みたいなもの。
<script>
let string = `この文字列に含まれているのは <strong>HTML!!!</strong>`;
</script>
<p>{@html string}</p>
Reactivity
DOMをアプリケーションの状態に同期し続けさせるための仕組みを reactivityシステムというらしい。
イベントの基本
<script>
let count = 0;
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>カウントは{count}回</button>
on:...
でイベントの設定し、値を変更する関数を渡すだけ。
変数と変数の同期
count
に依存するdoubled(2倍した値)
もつくることができる。その場合は$: doubled
と宣言する。
<script>
let count = 0;
+ $: doubled = count * 2;
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>カウントは{count}回</button>
+ <p>{count}の2倍は{doubled}</p>
値に依存するのは別の値だけでなく、処理なんかも設定できる。例えばcount
が増えるたびにログを出すなど。
<script>
let count = 0;
- $: doubled = count * 2;
+ $: console.log(count)
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>カウントは{count}回</button>
- <p>{count}の2倍は{doubled}</p>
また処理をオブジェクトにまとめて実行させることも可能。
$: {
console.log('カウントは ' + count);
alert('カウントは ' + count);
}
if文も実行可能。
$: if (count >= 10) {
alert('カウントが高いです');
count = 9;
}
配列、オブジェクトはうまくいかない
Svelte のリアクティビティはReactのsetStateと似ていて代入によって同期する。
なので配列やオブジェクトは破壊的メソッド(pop
shift
splice
)はうまく機能しない
<script>
let numbers = [1, 2, 3, 4];
function addNumber() {
numbers.push(numbers.length + 1);
}
$: sum = numbers.reduce((t, n) => t + n, 0);
</script>
<p>{numbers.join(' + ')} = {sum}</p>
<button on:click={addNumber}>追加</button>
上の例だと"追加"ボタンを押してもnumbers
に数値は追加されるものの、sum
で再計算が反映されない。
解決方法が2つ示されている。
numbers
にnumbers
自身を代入する
function addNumber() {
numbers.push(numbers.length + 1);
+ numbers = numbers;
}
正直これでうまくいく理屈はよく分からない。
関数内のnumbers
には追加されるけど最終的に=
で代入されていないから関数オブジェクトと共に追加された配列が破棄されるから追加されない。それを関数内で代入してあげるということかな?
スプレット構文を使用
多分こっちがメインで使っていく方法。
function addNumber() {
- numbers.push(numbers.length + 1);
+ numbers = [...numbers, numbers.length + 1];
}
ReactのsetStateを使っていれば分かりやすい。
ここはReactと違うが配列やオブジェクトのプロパティへの代入は値自体への代入と同じように動作する。
obj.foo += 1
array[i] = x
function addNumber() {
numbers[numbers.length] = numbers.length + 1;
}
しかし下記のような間接的な代入はできない。
const foo = obj.foo;
foo.bar = 'baz';
function quox(thing) {
thing.foo.bar = 'baz';
}
quox(obj);
props
今時のフレームワークなら馴染みのあるはず。親コンポーネントから子コンポーネントに値を渡す。
基本的な値の受け渡し
親
次の形式で任意のprops名で値を渡す。
<子コンポーネント props名={値}>
ここはReactと一緒。
子
<script>
内でprops
を宣言する。
<script>
import Nested from './Nested.svelte';
</script>
<Nested answer={42}/>
<script>
export let answer;
</script>
<p>answerは{answer}</p>
宣言時は以下の特徴がある。
- 頭に
export
をつける。jsのexport
とは挙動が違うので独自記法として使用(個人的にimport
の方が意味が近い気がする) -
props
名は親で定義したprops
名と揃える必要がある -
let
とconst
で挙動が変わる(後述)
デフォルト値の設定
子コンポーネントでprops
のデフォルト値を設定できる。
<script>
import Nested from './Nested.svelte';
</script>
<Nested answer={42}/>
+ <Nested/>
<script>
- export let answer;
+ export let answer = 'a mystery';
</script>
<p>answerは{answer}</p>
デフォルト値を設定したときの挙動
- 親で
props
をしていない場合、デフォルト値が適応される - 子で値を
const
で定義した場合、読み取り専用となる。親のprops
を受け付けず値は常にデフォルト値となる
スプレット構文で渡す
props
がオブジェクトの場合、スプレット構文で渡すことができる。
<script>
import Info from './Info.svelte';
const pkg = {
name: 'svelte',
version: 3,
speed: 'blazing',
website: 'https://svelte.dev'
};
</script>
- <Info name={pkg.name} version={pkg.version} speed={pkg.speed} website={pkg.website}/>
+ <Info {...pkg}/>
<script>
export let name;
export let version;
export let speed;
export let website;
</script>
<p>
The <code>{name}</code> package is {speed} fast.
Download version {version} from <a href="https://www.npmjs.com/package/{name}">npm</a>
and <a href={website}>learn more here</a>
</p>
HTMLで条件式、ループ、非同期の使用
if文
if文に限らずこれらの式は{}
からなる開始タグと終了タグでHTMLを囲む
<script>
let x = 7;
</script>
{#if x > 10}
<p>{x} is greater than 10</p>
{:else if 5 > x}
<p>{x} is less than 5</p>
{:else}
<p>{x} is between 5 and 10</p>
{/if}
- 開始タグ:
{#if ~}
- elseタグ:
{:else}
、{:else if ~}
- 終了タグ:
{/if}
ループ
基礎構文
ループ処理で使うタグは2つ
- 開始タグ:
{#each 配列 as 要素, インデックス(省略可)}
- 終了タグ:
{/each}
開始タグはas
の後が引数になっており、要素とインデックスをとることができる
<script>
let cats = [
{ name: 'くろ', age: 3 },
{ name: 'てん', age: 4 },
{ name: 'とら', age: 11 }
];
</script>
<ul>
{#each cats as cat}
<li>{cat.name}は{cat.age}歳</li>
{/each}
</ul>
インデックスを引数に取る場合
<script>
let cats = [
{ name: 'くろ', age: 3 },
{ name: 'てん', age: 4 },
{ name: 'とら', age: 11 }
];
</script>
<ul>
- {#each cats as cat}
+ {#each cats as cat, index}
- <li>{cat.name}は{cat.age}歳</li>
+ <li>{index + 1}.{cat.name}は{cat.age}歳</li>
{/each}
</ul>
配列の要素がオブジェクトなら分割代入も可
<script>
let cats = [
{ name: 'くろ', age: 3 },
{ name: 'てん', age: 4 },
{ name: 'とら', age: 11 }
];
</script>
<ul>
- {#each cats as cat, index}
+ {#each cats as {name, age}, index}
- <li>{index + 1}.{cat.name}は{cat.age}歳</li>
+ <li>{index + 1}.{name}は{age}歳</li>
{/each}
</ul>
配列を変更するときの注意
ループしている配列の要素を削除などすると、意図しない挙動だったりする。
厳密に解説すると大変なので分かりやすい記事を置いておく。
非同期処理
HTMLの中で非同期処理使うことができる。以下必要なタグ。
- 開始タグ:
{#await 取得を待つ変数}
- resolveタグ:
{:then 成功時に受け取る値}
- rejectタグ:
{:catch 失敗時に受け取るエラー}
- 終了タグ:
{/await}
promise
扱ったことあれば直観的に分かると思う。
reject
できないことがわかっている場合はcatch
を省略可。
また成功するまで何も表示しない場合は開始タグを省略可。{#await 取得を待つ変数 then 成功時に受け取る値}
となる。
{#await promise then number}
<p>the number is {number}</p>
{/await}
以下公式の例
<script>
async function getRandomNumber() {
const res = await fetch(`/tutorial/random-number`);
const text = await res.text();
if (res.ok) {
return text;
} else {
throw new Error(text);
}
}
let promise = getRandomNumber();
function handleClick() {
promise = getRandomNumber();
}
</script>
<button on:click={handleClick}>
generate random number
</button>
{#await promise}
<p>...waiting</p>
{:then number}
<p>The number is {number}</p>
{:catch error}
<p style="color: red">{error.message}</p>
{/await}