前々から気にはなっていた svelte だが、stackoverflow 等が強めに推しているようなのでこの機会に基本的な利用方法等を調査する。
https://survey.stackoverflow.co/2024/technology#2-web-frameworks-and-technologies
https://stackoverflow.blog/2023/10/31/why-stack-overflow-is-embracing-svelte/
フロントエンドフレームワーク Svelte の基本的な利用方法。
npx sv create
いきなりエラーになる
■ Failed to install dependencies │ npm error code EBADENGINE │ npm error engine Unsupported engine │ npm error engine Not compatible with your version of node/npm: @sveltejs/vite-plugin-svelte@4.0.1 │ npm error notsup Not compatible with your version of node/npm: @sveltejs/vite-plugin-svelte@4.0.1 │ npm error notsup Required: {"node":"^18.0.0 || ^20.0.0 || >=22"} │ npm error notsup Actual: {"npm":"10.9.1","node":"v21.5.0"} : │ └ Operation failed.
環境変数 NODE_VERSION に 16 セットすれば解決するっぽい。(少し古い issue だが)
https://github.com/sveltejs/kit/discussions/8583
■ Failed to install dependencies │ npm ERR! code EBADENGINE │ npm ERR! engine Unsupported engine │ npm ERR! engine Not compatible with your version of node/npm: eslint@9.15.0 │ npm ERR! notsup Not compatible with your version of node/npm: eslint@9.15.0 │ npm ERR! notsup Required: {"node":"^18.18.0 || ^20.9.0 || >=21.1.0"} │ npm ERR! notsup Actual: {"npm":"9.8.1","node":"v18.16.0"} : │ └ Operation failed.
結果は変わらず。
node バージョンが原因である事は間違いないようなので、結局 nodeのバージョンを色々変えてみて、
自分の環境では最終的に node: 20.9.0、npm: 10.1.0 でエラーがなくなった。
┌ Welcome to the Svelte CLI! (v0.6.5) │ ◇ Where would you like your project to be created? │ ./sample1 │ ◇ Which template would you like? │ SvelteKit minimal │ ◇ Add type checking with Typescript? │ Yes, using Typescript syntax │ ◆ Project created │ ◇ What would you like to add to your project? (use arrow keys / space bar) │ prettier, eslint │ ◇ Which package manager do you want to install dependencies with? │ npm │ ◆ Successfully setup add-ons │ ◆ Successfully installed dependencies │ ◇ Successfully formatted modified files │ ◇ Project next steps ─────────────────────────────────────────────────────╮ │ │ │ 1: cd sample1 │ │ 2: git init && git add -A && git commit -m "Initial commit" (optional) │ │ 3: npm run dev -- --open │ │ │ │ To close the dev server, hit Ctrl-C │ │ │ │ Stuck? Visit us at https://svelte.dev/chat │ │ │ ├──────────────────────────────────────────────────────────────────────────╯ │ └ You're all set!
svelteのコンポーネントは .svelte ファイルに以下の形式で記述する。
<script> // ロジックを記述 // export する事により利用側から指定可能なプロパティを定義可能 export let arg1; </script> <!-- 0個以上のマークアップを記述 --> <style> /* styleを記述 */ </style>
https://svelte.jp/docs/svelte-components#script-context-module
context="module" 属性をもつ <script> タグはモジュールが最初に読み込まれる時に1回だけ評価される。
また module script で定義された変数はリアクティブではない。
※変数内容自体は更新されるが、再レンダリングの対象にはならない。
<script context="module"> console.log("this is script(module) tag"); let initValue = 10; </script> <script> let value = initValue; console.log("this is script tag"); const onClick = () => { initValue += 1; value += 1; console.log(`initValue: ${initValue}`); console.log(`value: ${value}`); }; </script> <div>initValue: {initValue}</div> <!-- 再レンダリングされない --> <div>value : {value}</div> <!-- 再レンダリングされる --> <input type="button" on:click={onClick} value="button" />
scriptタグ内で宣言された変数は基本的にリアクティブになる。
※変更を自動的に検知して必要に応じて再レンダリングされる。
<script> let count = 0; const countUp = () => { count++; }; </script> <p>count: {count}</p> <input type="button" value="Add" on:click={countUp}>
また $: を付与する事により任意のステートメントをリアクティブにする事が出来る。
※React でいう useMemo 的な事が可能。
以下の 関数:multi は count2 が変わった時に呼び出され、リアクティブ変数 count2_2 の内容を更新する。
<script> let count1 = 0; const count1Up = () => { count1++; }; let count2 = 0; const count2Up = () => { count2++; }; const multi = (val) => { console.log("do multi"); return val * 2; }; $: count2_2 = multi(count2); </script> <h2>useMemo</h2> <p>count1: {count1}</p> <input type="button" value="Add" on:click={count1Up}> <hr /> <p>count2: {count2}, count2 * 2 = {count2_2}</p> <input type="button" value="Add" on:click={count2Up}>
https://svelte.jp/docs/basic-markup
小文字のタグは通常のHTML要素、大文字で始まるタグはコンポーネントを示す。
<script> import Widget from './Widget.svelte'; </script> <div> <Widget /> </div>
https://svelte.jp/docs/basic-markup
{}で囲む事により変数の内容を展開する事ができる。
また、Javascript式を書くことも可能。
<script> let count = 0; function handleClick() { count = count + 1; } </script> <p>count: {count}</p> <!-- 変数の内容をそのまま展開 --> <p>count + 10 = {count + 10}</p> <!-- JavaScript式を書くことも可能 --> <button on:click={handleClick}>+</button>
https://svelte.jp/docs/logic-blocks
{#if expression}...{/if} {#if expression}...{:else if expression}...{/if} {#if expression}...{:else}...{/if}
{#each expression as name}...{/each} {#each expression as name, index}...{/each} {#each expression as name (key)}...{/each} {#each expression as name, index (key)}...{/each} {#each expression as name}...{:else}...{/each}
サンプル
<ul> {#each items as item} <li>{item.name} x {item.qty}</li> {/each} </ul>
https://svelte.jp/docs/logic-blocks#await
await ブロックを使用すると、Promise が取りうる 3 つの状態(pending(保留中)、fulfilled(成功)、rejected(失敗))に分岐できる。
{#await expression}...{:then name}...{:catch name}...{/await} {#await expression}...{:then name}...{/await} {#await expression then name}...{/await} {#await expression catch name}...{/await}
サンプル
{#await promise} <!-- promise is pending --> <p>waiting for the promise to resolve...</p> {:then value} <!-- promise was fulfilled or not a Promise --> <p>The value is {value}</p> {:catch error} <!-- promise was rejected --> <p>Something went wrong: {error.message}</p> {/await}
https://svelte.jp/docs/logic-blocks#key
key ブロックは式(expression)の値が変更されたときに、その中身を破棄して再作成する。
{#key expression}...{/key}
サンプル
{#key value} <Component /> {/key}
https://svelte.jp/docs/special-tags
{@html ...} ... HTMLタグをエスケープしない
{@debug ...} ... 指定した変数の値が変更されるたびログ出力
{@const ...} ... マークアップ内でローカル定数を定義する
https://svelte.jp/docs/element-directives#on-eventname
on:eventname={handler}
on:eventname|modifiers={handler}
<button on:click={handleClick}> count: {count} </button>
| を使ってDOMイベントに修飾子(modifiers)を追加する事も可能。
<form on:submit|preventDefault={handleSubmit}> <!-- `submit` イベントのデフォルトの動作が止められているためページはリロードされない --> </form>
https://svelte.jp/docs/element-directives#bind-property
https://svelte.jp/docs/element-directives#binding-select-value
<script> let selected = ''; </script> {selected || '空値'} が選択されています<br /> <select bind:value={selected}> <option value="">(未選択)</option> <option value="a">a</option> <option value="b">b</option> <option value="c">c</option> </select>
https://svelte.jp/docs/element-directives#block-level-element-bindings
描画結果から clientWidth, clientHeight, offsetWidth, offsetHeight を取得する事が出来る。
<script> let width = 0; let height = 0; </script> <div bind:offsetWidth={width} bind:offsetHeight={height} style="padding: 10px; background: #efe; width: 200px;"> width: {width}, height: {height}<br /> </div>
https://svelte.jp/docs/element-directives#bind-group
<script> let radioChecked = 'a'; let multiChecked = []; </script> <div> {radioChecked} が選択されています<br /> <input type="radio" bind:group={radioChecked} value="a" />a <input type="radio" bind:group={radioChecked} value="b" />b <input type="radio" bind:group={radioChecked} value="c" />c </div> <div> {multiChecked} が選択されています<br /> <input type="checkbox" bind:group={multiChecked} value="a" />a <input type="checkbox" bind:group={multiChecked} value="b" />b <input type="checkbox" bind:group={multiChecked} value="c" />c <input type="checkbox" bind:group={multiChecked} value="d" />d </div>
https://svelte.jp/docs/element-directives#bind-this
<script> function moveBack() { history.back(); } let myInput; let info_message = ''; const checkInput = () => { info_message = myInput.value + "が入力されています"; } </script> <div> {info_message}<br /> <input bind:this={myInput} type="text" value="" /> <button on:click={checkInput} >check</button> </div>
https://svelte.jp/docs/element-directives#style-property
<script> let color = 'red'; let bgColor = 'green'; </script> <!-- この2つは同等 --> <div style:color="red">...</div> <div style="color: red;">...</div> <div style:color={color}>変数を使用</div> <div style:color>プロパティと変数の名前が一致する場合の短縮形</div> <div style:color style:width="12rem" style:background-color={bgColor}>複数のスタイルを含めることが可能</div> <div style:color|important="blue">important ハック</div>
https://svelte.jp/docs/element-directives#use-action
https://svelte.jp/docs/svelte-action
素が作成されるときに呼び出される関数を指定する事が可能。
※要素がアンマウントされるときに呼び出される destroy メソッドをもつオブジェクトを返す事も可能。
<script> export let arg; const myElement = (arg) => { console.log("created myElement!"); return { destroy: function(){ console.log("destroy myElement!"); } }; }; </script> <div use:myElement> This is My Element. ( arg: {arg} ) </div>
https://svelte.jp/docs/component-directives#on-eventname
<script> const onChange = (e) => { console.log(`"${e.target.value}" が入力されました`); }; const onClick = () => { console.log("ボタンがクリックされました"); }; </script> <input type="text" on:change={onChange} /> <input type="button" on:click={onClick} value="click me" />
createEventDispatcher を使用してカスタムイベントを発火する事も可能。
<script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); </script> <!-- そのまま親に転送する場合 --> <input type="button" on:click value="forward event" /> <!-- カスタムイベントを発火する場合 --> <input type="button" on:click={()=>dispatch('notify1', `this is dispatchEvent`)} value="custom event(notify1) " />
この場合、利用側は on:イベント名 でイベントを補足する事が出来る。
<script> const onClick = (e) => { console.log(e); }; const onNotify1 = (e) => { console.log(e); console.log(e.detail); }; </script> <MyConponent on:click={onClick} on:notify1={onNotify1} />
<script> import { onMount, beforeUpdate, afterUpdate, onDestroy } from 'svelte'; export let text = ''; const onChangeText = (e) => { console.log(e); text = e.target.value; }; onMount(()=>{ console.log("onMount"); }); beforeUpdate(() => { console.log("beforeUpdate"); }); afterUpdate(() => { console.log("afterUpdate"); }); onDestroy(() => { console.log("onDestroy"); }); </script> <h2>ライフサイクル関数</h2> <input type="text" on:change={onChangeText} value={text} />
https://kit.svelte.jp/docs/routing
https://kit.svelte.jp/docs/routing#page-page-js
https://kit.svelte.jp/docs/load
基本的に routes配下にディレクトリ 及び ファイルを配置するだけで良い。
※+page.svelte そのディレクトリ配下のルートにアクセス時に使用される。
└── routes
├── +page.svelte
├── sample1
│ └── +page.svelte
└── sample2
└── +page.svelte
Rest的に /items で商品一覧、/items/{商品ID} を商品詳細ページとするような場合、以下のように構成する。
└── routes
├── +page.svelte
└── items
└── +page.svelte ... 一覧ページ用
└── [id]
└── +page.svelte ... 詳細ページ用
以下にサンプルを記載する。
routes/items/SampleData.svelte
<script context="module"> export const items = [ {"id": "0001", name: "商品0001", price: 1100}, {"id": "0002", name: "商品0002", price: 2200}, {"id": "0003", name: "商品0003", price: 3300}, {"id": "0004", name: "商品0004", price: 4400}, {"id": "0005", name: "商品0005", price: 5500}, ]; </script>
routes/items/+page.svelte
<script> import { goto } from '$app/navigation'; import { items } from './SampleData.svelte'; const pageMode = (id) => { const res = items.filter((x)=>x.id == id); if (!res.length) { return; } const rec = res[0]; goto(`/items/${id}`) }; </script> <h2>商品一覧</h2> <table class="tbl"> <thead> <tr> <th>商品ID</th> <th>商品名</th> <th>価格</th> </tr> </thead> <tbody> {#each items as x} <tr> <td on:click={()=>pageMode(x.id)}>{x.id}</td> <td>{x.name}</td> <td>{x.price}</td> </tr> {/each} </tbody> </table> <style> .tbl { th,td { border: 1px solid #333; } } </style>
レンダリングの前にデータを読み込む必要がある場合、+page.js モジュールにて load 関数をエクスポートする事でこれを実現できる。
https://kit.svelte.jp/docs/routing#page-page-js
https://kit.svelte.jp/docs/load
routes/items/[id]/+page.js
import { error } from '@sveltejs/kit'; import { items } from '../SampleData.svelte'; /** @type {import('./$types').PageLoad} */ export function load({ params }) { const filtered = items.filter((x)=>x.id == params.id); if (filtered.length) { return filtered[0]; } error(404, 'Not found'); }
load 関数の戻り値は page で data プロパティから取得する事が出来る。
https://kit.svelte.jp/docs/load#page-data
routes/items/[id]/+page.svelte
<script> export let data; </script> <h2>商品詳細</h2> <div>商品ID: {data.id</div> <div>商品名: {data.name}</div> <div>価格 : {data.price}</div>