2024-11-24
2024-01-05
目次 †はじめに †前々から気にはなっていた svelte だが、stackoverflow 等が強めに推しているようなのでこの機会に基本的な利用方法等を調査する。 特徴 †フロントエンドフレームワーク 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 だが) ■ 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. 結果は変わらず。 ┌ 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のコンポーネントは .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回だけ評価される。 <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}> また $: を付与する事により任意のステートメントをリアクティブにする事が出来る。 以下の 関数: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}> ルーン文字の使用 †Svelte5からはルーン文字を使用してリアクティブな変数を明示出来る。 以下、ルーン文字の使用有無による実装の違いについて記載する。(内容はシンプルなカウンタ) ルーン文字を使用しない場合 <script> let count = 0; $: count2 = count * 2; </script> count: <input type="button" value="-" on:click={()=>count -= 1} /> {count} <input type="button" value="+" on:click={()=>count += 1}/> ... count * 2 = {count2} ルーン文字を使用する場合 <script> let count = $state(0); // ≒ react の useState let count2 = $derived(count * 2); // ≒ react の useMemo </script> count: <input type="button" value="-" on:click={()=>count -= 1} /> {count} <input type="button" value="+" on:click={()=>count += 1}/> ... count * 2 = {count2} また、ルーン文字が使用される場合、scriptタグ内のトップレベル変数は全てリアクティブではなくなる。 <script> let count1 = 0; // この変数はリアクティブではなくなる // $: count1_2 = count1 * 2; <-- これはエラーになる( `$:` is not allowed in runes mode ) let count2 = $state(0); let count2_2 = $derived(count2 * 2); </script> <!-- こちらは非リアクティブ --> count1: <input type="button" value="-" on:click={()=>count1 -= 1} /> {count1} <input type="button" value="+" on:click={()=>count1 += 1}/> <hr /> <!-- こちらはリアクティブ --> count2: <input type="button" value="-" on:click={()=>count2 -= 1} /> {count2} <input type="button" value="+" on:click={()=>count2 += 1}/> マークアップの基本 †https://svelte.jp/docs/basic-markup 小文字のタグは通常のHTML要素、大文字で始まるタグはコンポーネントを示す。 <script> import Widget from './Widget.svelte'; </script> <div> <Widget /> </div> 値の展開 †https://svelte.jp/docs/basic-markup {}で囲む事により変数の内容を展開する事ができる。 <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タグをエスケープしない イベントの捕捉 †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 bind:プロパティ名={変数名} でバインド可能。 <input bind:value={value} /> <input bind:value /> value値のバインディング †<script> let message = $state('hello'); </script> <input bind:value={message} /> <p>{message}</p> <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 要素が作成されるときに呼び出される関数を指定する事が可能。 <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 基本的に routes配下にディレクトリ 及び ファイルを配置するだけで良い。 └── routes Rest的に /items で商品一覧、/items/{商品ID} を商品詳細ページとするような場合、以下のように構成する。 └── routes 以下にサンプルを記載する。 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; } td:first-child { cursor: pointer; color: #00f; &:hover { text-decoration: underline; } } } </style> レンダリング前のデータ読み込み †レンダリングの前にデータを読み込む必要がある場合、+page.js モジュールにて load 関数をエクスポートする事でこれを実現できる。 https://kit.svelte.jp/docs/routing#page-page-js 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> 共通レイアウト †https://kit.svelte.jp/docs/routing#layout
<div id="nav"> <a>メニュー1</a> <a>メニュー2</a> </div> <div id="contents"> <slot></slot> <!-- ここに各ページの内容が展開される --> </div> <div id="footer"> フッター </div> 環境変数 †.env や .env.development、.env.local 等の内容を読み込む事が出来る。 https://kit.svelte.jp/docs/modules#$env-dynamic-private .env.local PUBLIC_SAMPLE_VAR1=test123 利用側 <script> import { PUBLIC_SAMPLE_VAR1 } from '$env/static/public'; console.log(PUBLIC_SAMPLE_VAR1); </script> 複数コンポーネント間での値の共有 †svelte/store の機能を使用してアプリケーション全体で管理する状態などの管理を行う事が出来る。(ReactのRedux的なもの) 機能はいくつかあるが、以下を利用すれば大抵の事は出来る。( https://svelte.jp/docs/svelte-store )
サンプル <script> import { readonly, writable } from 'svelte/store'; const sampleStore = writable({ num: 1, str: "test1"}); const sampleReadOnly = readonly(sampleStore); // subscribeにて購読可能 sampleReadOnly.subscribe((data)=>{ console.log("change data!", data); }); let inputNum; let inputStr; const update = () => { // セットメソッドを使用する場合 sampleStore.set({num: inputNum.value, str: inputStr.value}); // $参照で直接セットする場合(.svelte ファイルのみで使用可)( Cannot reference store value outside a `.svelte` file ) (svelte 5.0.0 時点) //$sampleStore = {num: inputNum.value, str: inputStr.value}; }; </script> <!-- $によるストア値の参照(.svelte ファイルのみで使用可) --> num1: <input type="text" bind:this={inputNum} value={$sampleStore.num} /> str1: <input type="text" bind:this={inputStr} value={$sampleStore.str} /> <input type="button" on:click={update} value="OK" /> もう少し実用的な例として、ログイン状態によってメニュー表示やログインページへの自動遷移を行う場合の例を記載する。 ユーザ情報を管理するストア ( src/routes/store.js ) //import { page } from "$app/stores"; import { goto } from '$app/navigation'; import { readonly, writable } from 'svelte/store'; const userProfileStore = writable({ isLoggedIn: false, id: null, name: null, email: null, roles: []}); export const userProfile = readonly(userProfileStore); userProfile.subscribe((u) => { // 未ログイン時はログインページに自動遷移 try { const urlAll = window?.location?.href || ''; const uri = urlAll.replace(/^http(s|):\/\/[^/]+(\/[^\?#]+).*/, "$2") //const uri = $page.url.pathname; // .svelte ファイル以外では使用できない ( Cannot reference store value outside a `.svelte` file ) // 未ログイン かつ ログインページでない時 if (!u.isLoggedIn && uri !== "/login") { goto(`/login/`); return; } } catch (e) { } }); export const setUserProfile = (id, name, email, roles) => { //$userProfileStore = { isLoggedIn: true, id, name, email, roles }; userProfileStore.set({ isLoggedIn: true, id, name, email, roles }) }; export const unsetUserProfile = () => { //$userProfileStore = { isLoggedIn: false, id: null, name: null, email: null, roles: []}; userProfileStore.set({ isLoggedIn: false, id: null, name: null, email: null, roles: []}); }; 共通レイアウト ( routes/+layout.svelte ) <script> import { unsetUserProfile, userProfile } from './store.js'; </script> <!-- ヘッダ(メニュー) --> {#if ($userProfile.isLoggedIn === true)} <div id="header"> <ul class="left"> <li><a href="/">TOP</a></li> <li><a href="/sample1">メニュー1</a></li> <li><a href="/sample2">メニュー2</a></li> <li><a href="/sample3">メニュー3</a></li> </ul> <div class="right"> <a href="#" on:click={unsetUserProfile}>ログアウト</a> </div> </div> {/if} <!-- コンテンツ --> <div id="contents"> <slot></slot> </div> <div id="footer"> フッター </div> ログインページ( routes/login/+page.svelte ) <script> //import { goto } from '$app/navigation'; //import { PUBLIC_API_URL_PREFIX } from '$env/static/public'; import { setUserProfile, userProfile } from '../store'; let userId; let password; const doLogin = () => { // ログイン処理 //const url = PUBLIC_API_URL_PREFIX + "/login"; //const options = { // method: 'POST', // headers: {"content-type": 'application/json'}, // body: JSON.stringify({ user_id: userId.value, password: password.value }) //}; //fetch(url, options) //.then((response) => { // if (response.ok) { // response.json().then((res)=>{ // setUserProfile(res.user_id, res.user_name, res.user_email, []); // console.log(res); // goto(`/`); // }); // } else { // console.log("error!"); // } //}) setUserProfile(userId.value, userId.value, "xxxxx@xxxx.com", []); goto(`/`); }; </script> <div id="login"> <div id="login_form"> <div class="row"> <div>ユーザID:</div> <div><input type="text" bind:this={userId} /></div> </div> <div class="row"> <div>パスワード:</div> <div><input type="password" bind:this={password} /></div> </div> <div class="row" on:click={doLogin}> <button>ログイン</button> </div> </div> </div> |