作成日: 2024-11-24、最終更新: 2024-12-08

目次

はじめに

前々から気にはなっていた 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 の基本的な利用方法。

TODO: 特徴など

環境構築、プロジェクト作成

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!

ファイル構成

TODO:

svelteのコンポーネント

svelteのコンポーネントは .svelte ファイルに以下の形式で記述する。

<script>
	// ロジックを記述

	// export する事により利用側から指定可能なプロパティを定義可能
	export let arg1;
</script>

<!-- 0個以上のマークアップを記述 -->

<style>
	/* styleを記述 */
</style>

<script context="module"> による初期化

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>

条件分岐 {#if ...}

https://svelte.jp/docs/logic-blocks

{#if expression}...{/if}
{#if expression}...{:else if expression}...{/if}
{#if expression}...{:else}...{/if}

繰り返し {#each ...}

{#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>

Proimiseの状態に応じた分岐 {#await ...}

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}

要素の再作成 {#key ...}

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

TODO:

<select> 値のバインディング

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>

DOM要素の参照

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>

use:action

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} />

ライフサイクル関数など

https://svelte.jp/docs/svelte

<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>

複数コンポーネント間での値の共有

TODO:

https://svelte.jp/docs/svelte-store


トップ   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS