Svelte 勉強メモ
Svelte概要
Reactやvue.jsと似た、javascriptフレームワークである
コンパイルが必要である
VueもJSとして走ることに固執しながらも、実態はビルド時にコンパイルが必要な*.vueとかいう独自ファイル形式で開発されるのがほとんどで、「JSに擬態したJSではないなにか」に成り下がってしまっており、だったらもっとシンプルな設計にできるだろ、という考え方は大変に頷ける。
特にReactでいうuseStateやVueのdata()、useEffectなどを使用してリアクティビティを確保する方法は、言われてみれば全く直感的ではなく美しい設計ではない。Svelteはこのあたりを改善する方針で作られたように感じる。
コンパイル必須なのでCDNでインポートして即試す、というわけでにはいかないのが欠点といえば欠点か。
アニメーション機能が標準で用意されている
他ライブラリをインポートしたり2つのマニュアルを交互に参照したりしなくて良いのは利点
セットアップ
公式でSveltekitというフレームワークが用意されているので使う
code:cmd
npm create svelte@latest my-app
<-- ここでtypescriptとかlintの導入を訪ねてくるので適当に答える
cd my-app
npm install
npm run dev
Sveltkitを使う場合
http://localhost:5173にアクセスするとsrc/app.htmlの内容が表示される…が、これはテンプレートであって実際のコンテンツはこれではない。
src/routes内の+page.svelteがhttp://localhost:5173ページのコンテンツ本体を示す(index.htmlみたいなもの)。
サブディレクトリを掘るとその直下の+pages.svelteが中身になる。つまり、src/routes/someapp/+page.svelteを作ればその内容はhttp://localhost:5173/someapp/でアクセスできる。
src/lib内にコンポーネントを置いて各ページからアクセスできる。その場合
import Comp from '$lib/Comp';のように$libエイリアスを使ってsrc/lib/Comp.svelteをインポートできる
基本的な書式
code:js
<script>
let s = "World"
let src = "./image.png"
let count = 0
function countUp() {
count++
}
</script>
<p>Hello {s}</p> <!-- 変数の利用は{<変数名>} -->
<img {src}> <!-- プロパティ名と変数名が同じだった場合の簡易書式 -->
<button on:click={countUp}>{count}</button> <!-- イベントはon:<イベント名> -->
プロパティ名の展開
変数名とプロパティ名が同じならまとめて展開できる
code:js
<script>
const props = {
src:'/tutorial/image.gif',
alt:'dance man',
width:100,
height:100,
}
</script>
<img {...props}>
コンポーネント
code:App.svelte
<script>
import Nested from './Nested.svelte'; //importで他ファイルを取り込む
</script>
<Nested answer={42}/> <!-- コンポーネントの呼び出し。プロパティansterを指定。先頭は常に大文字 -->
code:Nested.svelte
<script>
export let answer = "mistery"; //ansterプロパティを受け取る。デフォルト値を指定できる
</script>
<p>The answer is {answer}</p>
プロパティの展開はコンポーネントにも可能
code:App.svelte
<script>
import Info from './Info.svelte';
const pkg = {
name: 'svelte',
version: 3,
...
};
</script>
<Info name={pkg.name} version={pkg.version} ... />
↓↓↓↓
<Info {...pkg}/>
code:Info.svelte
<script>
export let name;
export let version;
...
</script>
リアクティビティ
$: ...と書くとその行で扱っている変数が「代入」されるたびに再計算・再レンダリングされる
code:js
<script>
let count = 0
$: double = count * 2 //countが代入されるたびに再計算される
function countup() {
count = count + 1 //もちろんcount++でも良い
}
</script>
<p>{count} * 2 = {double}</p>
<button on:click={countup}>+1</button>
この場合、countに値が代入されるたびにdoubleが再計算される
$: { ... }と書いてグループ化も可能
code:js
$: {
dougle = count * 2
console.log("changed")
}
配列の場合、アイテムの内容を更新しても検知されないので、配列自身に再代入して知らせる
code:js
<script>
let arr = []
$: console.log(arr) //arrの更新が検知されるたびに内容を出力
function addnum() {
arr.push(arr.length + 1)
arr = arr //ここでarrの更新を知らせる
}
</script>
<button on:click={addnum}>Add</button>
条件付きレンダリング
{#if <式>}...{:else}...{/if}
{#が頭、{:が継続、{/が終了
code:js
<script>
let loggedIn = false
function toggle() {
loggedIn = !loggedIn
}
</script>
{#if loggedIn}
<button on:click={toggle}>Log out</button>
{:else}
<button on:click={toggle}>Log in</button>
{/if}
{:else if ...}も可能
code:html
{#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}
反復レンダリング
{#each <配列> as <要素>}
{#each <配列> as <要素>, 添字}
{#each <配列> as {<プロパティ名1>, <プロパティ名2>...}, 添字}
code:html
<script>
let cats = [
{ id: 'J---aiyznGQ', name: 'Keyboard Cat' },
{ id: 'z_AbfPXTKms', name: 'Maru' },
{ id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
];
</script>
<ul>
{#each cats as { id, name }, i} <!-- id = cat.id, name = cat.name -->
{i + 1}: {name}
</a></li>
{/each}
</ul>
反復レンダリング中のkeyとid
キーを設定しないと、要素の内容が変更されても検知されない場合がある
{#each <配列> as <要素> (<キー>)}
code:html
<script>
import Thing from './Thing.svelte';
let things = [
{ id: 1, name: 'apple' },
{ id: 2, name: 'banana' },
...
];
</script>
{#each things as thing (thing.id)}
<Thing name={thing.name}/>
{/each}
非同期
APIアクセス等の応答を待つ機能がマークアップの中で標準で用意されている
{#await <変数>}...{:then <ローカル変数>}...{:catch <エラーオブジェクト>}...{/await}
<変数>の変更を待つ→変更を検出したらその内容を<ローカル変数>へ受け取る→エラーが発生したらエラー内容を<エラーオブジェクト>に受け取る。
code:html
<script>
async function getRandomNumber() {
const res = await fetch(/tutorial/random-number);//ランダムな値を返すAPI
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} <!-- promiseの変更を待つ -->
<p>...waiting</p>
{:then number} <!-- 変更内容をnumberで受け取り -->
<p>The number is {number}</p>
{:catch error} <!-- エラーが発生したらエラー内容をerrorで受け取り -->
<p style="color: red">{error.message}</p>
{/await}
イベント
<tag on:<イベント名>={イベントハンドラの関数名、あるいはインラインで記述}>
code:html
<div on:mousemove="{e => m = { x: e.clientX, y: e.clientY }}">
The mouse position is {m.x} x {m.y}
</div>
修飾子
<tag on:<イベント名>|<修飾子>(|<修飾子>...)>="{イベントハンドラ}"
once:一度だけ実行する(実行後に削除される)
code:html
<button on:click|once={console.log('clicked')}>Click me</button>
preventDefault : ハンドラを実行する前にevent.preventDefault()を実行する
stopPropagation : 次の要素にイベントが伝播しないようにevet.stopPropagation()を実行する
passive : タッチ/ホイールイベントのパフォーマンスを向上させます(逆: nonpassive)
self : event.targetがこの要素自身だった場合のみハンドラをトリガします
trusted: ユーザーの操作にのみによってトリガします
コンポーネント間の通信
code:Inner.svelte
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher(); //これで通信ができるようになる
function sayHello() {
dispatch('message', {text: 'Hello!'}); //コンポーネント先でmessageイベントを発火
}
</script>
<button on:click={sayHello}>Click to say hello</button> //clickイベントでsayHelloをコール
code:App.svelte
<script>
import Inner from './Inner.svelte'; //コンポーネント取り込み
function handleMessage(event) {
alert(event.detail.text); //event.detail...から通信内容の取得
}
</script>
<Inner on:message={handleMessage}/> //messageイベント発火時にhandlemessageのトリガ
Sveltkit