Closed5

svelteの公式チュートリアルをやってみる

sendosendo

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

色々聞かれるので好みで設定。

sendosendo

基礎構文

App.svelte
<script>
//JSの記述
</script>

//HTMLの記述

<style>
//CSSの記述
</style>

Vueに寄ってる。タグの通りに書くだけ。分かりやすい。
script styleタグが必要なければ無くてもいい。

App.svelte
<script>
  let name = 'world';
</script>

<h1>Hello {name}!</h1>

Reactと同じく波括弧で変数を埋め込める。属性も同様。
stateは通常の変数で扱うみたい。というかstateの概念がないのかも。
imgaltがないと警告出してくれるみたい。

App.svelte
<p>This is a paragraph.</p>

<style>
  p {
    color: purple; 
    font-family: 'Comic Sans MS', cursive;
    font-size: 2em;
   }
</style>

styleにはそのままCSSを書ける。セレクタのスコープはコンポーネント内のみ適応されるので気にせず書ける。

コンポーネント分割

コンポーネントファイル名は頭文字を大文字にする。

Child.svelte
<p>これは別の段落です。</p>
Parent.svelte
<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()みたいなもの。

App.svelte
<script>
  let string = `この文字列に含まれているのは <strong>HTML!!!</strong>`;
</script>

<p>{@html string}</p>
sendosendo

Reactivity

DOMをアプリケーションの状態に同期し続けさせるための仕組みを reactivityシステムというらしい。

イベントの基本

App.svelte
<script>
  let count = 0;

  function handleClick() {
    count += 1;
  }
</script>

<button on:click={handleClick}>カウントは{count}</button>

on:...でイベントの設定し、値を変更する関数を渡すだけ。

変数と変数の同期

countに依存するdoubled(2倍した値)もつくることができる。その場合は$: doubledと宣言する。

App.svelte
 <script>
 let count = 0;
+ $: doubled = count * 2;

 function handleClick() {
  count += 1;
 }
 </script>

 <button on:click={handleClick}>カウントは{count}</button>

+ <p>{count}2倍は{doubled}</p>

値に依存するのは別の値だけでなく、処理なんかも設定できる。例えばcountが増えるたびにログを出すなど。

App.svelte
 <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)はうまく機能しない

App.svelte
 <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つ示されている。

numbersnumbers自身を代入する

App.svelte
function addNumber() {
  numbers.push(numbers.length + 1);
+  numbers = numbers;
}

正直これでうまくいく理屈はよく分からない。
関数内のnumbersには追加されるけど最終的に=で代入されていないから関数オブジェクトと共に追加された配列が破棄されるから追加されない。それを関数内で代入してあげるということかな?

スプレット構文を使用

多分こっちがメインで使っていく方法。

App.svelte
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);
sendosendo

props

今時のフレームワークなら馴染みのあるはず。親コンポーネントから子コンポーネントに値を渡す。

基本的な値の受け渡し

次の形式で任意のprops名で値を渡す。
<子コンポーネント props名={値}>
ここはReactと一緒。

<script>内でpropsを宣言する。

App.svelte
<script>
  import Nested from './Nested.svelte';
</script>

<Nested answer={42}/>
Nested.svelte
<script>
  export let answer;
</script>

<p>answerは{answer}</p>

宣言時は以下の特徴がある。

  • 頭にexportをつける。jsのexportとは挙動が違うので独自記法として使用(個人的にimportの方が意味が近い気がする)
  • props名は親で定義したprops名と揃える必要がある
  • letconstで挙動が変わる(後述)

デフォルト値の設定

子コンポーネントでpropsのデフォルト値を設定できる。

App.svelte
 <script>
  import Nested from './Nested.svelte';
  </script>

 <Nested answer={42}/>
+ <Nested/>
Nested.svelte
 <script>
-  export let answer;
+  export let answer = 'a mystery';
 </script>

 <p>answerは{answer}</p>

デフォルト値を設定したときの挙動

  • 親でpropsをしていない場合、デフォルト値が適応される
  • 子で値をconstで定義した場合、読み取り専用となる。親のpropsを受け付けず値は常にデフォルト値となる

スプレット構文で渡す

propsがオブジェクトの場合、スプレット構文で渡すことができる。

App.svelte
 <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}/>
Info.svelte
 <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>
sendosendo

HTMLで条件式、ループ、非同期の使用

if文

if文に限らずこれらの式は{}からなる開始タグと終了タグでHTMLを囲む

App.svelte
<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の後が引数になっており、要素とインデックスをとることができる
App.svelte
<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>

インデックスを引数に取る場合

App.svelte
    <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>

配列の要素がオブジェクトなら分割代入も可

App.svelte
    <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>

配列を変更するときの注意

ループしている配列の要素を削除などすると、意図しない挙動だったりする。
厳密に解説すると大変なので分かりやすい記事を置いておく。
https://zenn.dev/ettsu/articles/d31d0c671a14de

非同期処理

HTMLの中で非同期処理使うことができる。以下必要なタグ。

  • 開始タグ:{#await 取得を待つ変数}
  • resolveタグ: {:then 成功時に受け取る値}
  • rejectタグ: {:catch 失敗時に受け取るエラー}
  • 終了タグ: {/await}
    promise扱ったことあれば直観的に分かると思う。
    rejectできないことがわかっている場合はcatchを省略可。
    また成功するまで何も表示しない場合は開始タグを省略可。{#await 取得を待つ変数 then 成功時に受け取る値}となる。
App.svelte
{#await promise then number}
	<p>the number is {number}</p>
{/await}

以下公式の例

App.svelte
<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}
このスクラップは3ヶ月前にクローズされました