Vueユーザーが雑にSvelte入門する
Unagi-Network Advent Calendar 2023 Day 11の記事です。
Svelte、なんかおもろそう
普段はVue(v3, Composition API)をよく使用しているのですが、最近よく話を聞くSvelteが気になって入門してみました。同じコードをVueで実現する場合と比較してみると分かるのですが、Composition APIと書き方が似ているため、学習コストは低そうです。
導入
Svelte
+ Typescript
+ sass
+ Vite
環境だったらこんな感じです。Viteのテンプレートはすぐ構築できるので楽ですね。
pnpm create vite test-svelte-app --template svelte-ts
pnpm add -D sass
基本
コンポーネントごと.svelte
ファイルに書いていきます。
<script lang="ts">
let test = "foo bar"
</script>
<div>
<p class="test">{test}</p>
</div>
<p>ほげふが</p>
<style lang="sass">
.test
font-size: 10rem
</style>
Composition APIとの違いは以下の3点です。
-
<script>
にsetup
はいらない -
<template>
はなく、そのままHTMLタグを書いていく- top-levelのタグが1つ以上でも問題ない
-
<style>
はデフォルトでcomponent-scoped
なので、scoped
は書かなくていい
Reactiveな変数
ローカル変数がReactive
なので、ref()
は要りません。
let count = 0
const increment = () => {
count++;
}
import {ref} from "vue";
const count = ref(0)
const increment = () => {
count.value++
}
値の展開
{{}}
ではなく、{}
です。
<p>{hoge}</p>
<p>{{hoge}}</p>
on
例えばonClickは、Svelteではon:click
です。@
はv-on
の省略なので、本質的には似たようなものです。
<button on:click={clicked}>
count: {count}
</button>
<button @click={clicked}>
count: {{count}}
</button>
単方向/双方向バインド
やや罠。慣れれば直感的だとは思います。
-
v-bind
(:
)=> パラメータに{}
を使う -
v-model
=>bind:
を使う
<img src={imageUrl} />
<input bind:value={text} />
<img :src="imageUrl" />
<input v-model="text" />
※その他:省略記法
属性名と変数名が同じだった場合、省略することができます。
<script lang="ts">
const src = "http://example.com"
</script>
<img {src} />
$:
computed => Vueのcomputed
は、Svelteでは$:
を使います。癖アリ。
$:
の後に書かれた文を読んで、依存する値が変更された際に中身が実行されます。言い換えるのであれば、$:
の後ろに書かれたステートメントをリアクティブにする、という意味です。
奇怪な独自構文に見えますが、一応JSのラベル構文[2]をうまく使っているようです。
$: squared = count * count
$: isMobile = window.innerWidth < 640
const squared = computed(() => count.value * count.value)
const isMobile = computed(() => window.innerWidth < 640)
$:
watch => $:
の後に書かれた文を読んで、依存する値が変更された際に中身が実行されます。
…ということは、これでwatch
を再現できます。
let count = 2
$: if (count > 10) {
console.log("count > 10!")
}
const count = ref(2)
watch(count, () => {
if(count.value > 10) {
console.log("count > 10!")
}
})
ロジック
if, else
VueはDOM要素にv-if
やv-else
を付けますが、Svelteは専用の書き方があります。
DOM要素と結びついていないので、最後にifを閉じることが必要です。忘れずに。
{#if count === 0}
<p>0</p>
{:else if count === 1}
<p>1</p>
{:else}
<p>over 1</p>
{/if}
<p v-if="count === 0">
0
</p>
<p v-else-if="count === 1">
1
</p>
<p v-else>
over 1
</p>
for
以下のようなユーザ一覧があるとします。
const users = [
{
name: "foo",
age: 20,
},
{
name: "bar",
age: 30,
},
]
v-for
の代わりは{#each}
を使います。
今回の例では示しませんが、index
を取ったり、分割代入を使う事もできます[3]。
{#each users as user}
<div>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
</div>
{/each}
<div v-for="user in users">
<p>Name: {{user.name}}</p>
<p>Age: {{user.age}}</p>
</div>
コンポーネント
コンポーネントの呼び出し
Vueと大差ありません。
<script lang="ts">
import TestComponent from "./TestComponent.svelte"
</script>
<TestComponent />
<script setup lang="ts">
import TestComponent from "./TestComponent.vue";
</script>
<template>
<TestComponent />
</template>
props
export
で公開した変数をpropsとして使用できます。
値を指定しておけば、propsが渡されなかった際にデフォルト値として使われます。
<script lang="ts">
import TestComponent from "./TestComponent.svelte"
const count = 1
</script>
<TestComponent num={count} />
<script lang="ts">
export let num: number
export let text: string = "default text"
</script>
<p>num is {num}</p>
<p>text: {text}</p>
<script setup lang="ts">
import {ref} from "vue";
import TestComponent from "./TestComponent.vue";
const count = ref(1)
</script>
<template>
<TestComponent :num="count" />
</template>
<script setup lang="ts">
interface TestProps {
num: number
text?: string
}
const props = withDefaults(defineProps<TestProps>(), {
text: "default text"
})
</script>
<template>
<p>num is {{ props.num }}</p>
<p>text: {{ props.text }}</p>
</template>
slot
slotの書き方も大体同じです。
普通のslot
普通のslotの場合はVueもSvelteも全く同じです。
<TestComponent>
<h1>foo bar</h1>
<p>baz</p>
</TestComponent>
<div class="card">
<slot />
</div>
<template>
<TestComponent>
<h1>foo bar</h1>
<p>baz</p>
</TestComponent>
</template>
<div class="card">
<slot />
</div>
名前付きslot
名前付きスロットの場合はやや違いがあります。
Vueではv-slot
はtemplate
にしか当てられませんが、svelteはDOM要素に直接当てられます。
加えてslotの指定方法もタグの属性で設定するような形です。
<TestComponent>
<h1 slot="name">foo bar</h1>
<p slot="text">baz</p>
</TestComponent>
<div class="card">
<slot name="name" />
<hr />
<slot name="text" />
</div>
<template>
<TestComponent>
<template #name>
<h1>foo bar</h1>
</template>
<template #text>
<p>baz</p>
</template>
</TestComponent>
</template>
<template>
<div class="card">
<slot name="name" />
<hr />
<slot name="text" />
</div>
</template>
特殊タグ
ここからはSvelteの特殊タグです。
@html
Vueで言うところのv-html
です。Markdownなどを使うときに使います。
v-html
と同じくサニタイズは行わないのでXSSの危険性があります。取り扱い注意です。
<script lang="ts">
const text = "Super <strong>POWER</strong>"
</script>
<p>{@html text}</p>
@debug
指定した値が変更されるたび、ログに出力+devtoolsでpauseがかかります。
めちゃめちゃ優秀ですね。
<script lang="ts">
let count = 1
</script>
<button on:click={() => {count++}}>
count: {count}
</button>
{@debug count}
@const
ローカル変数を定義することができます。
{#if}
や{#each}
, <Component />
の中でのみ使えます。
{#each users as user}
{@const text = `${user.name} (${user.age})`}
<p>{text}</p>
{/each}
まとめ
触ってみるとかなり好感触で、Vueより簡潔に書けるのが魅力的でした。大抵新しいフレームワークは学習コストが高めですが、Composition APIと大体同じでちょっと構文が違うぐらいなので、全然すぐにでも使い始められそうな感じです。
今回は触れませんでしたが、Tween, Transitionなどアニメーション周りも充実していました。aちょっとした動きならanime.jsも要らなくなりそうです。より色々なExampleを参考にしたい方は、公式のドキュメントに色々乗っているのでこちらもご確認ください。
おまけ
ZennではPrismJSのシンタックスハイライトができますが、Svelteは対象外です。だいたい構文がVueと似ているので、vue:App.svelte
とかにするとそれっぽく表示できます。Prismくん、対応待ってます。
Discussion