🔧

Vueユーザーが雑にSvelte入門する

2023/12/11に公開

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ファイルに書いていきます。

App.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()は要りません。

Svelte
let count = 0

const increment = () => {
  count++;
}
Vue
import {ref} from "vue";

const count = ref(0)
const increment = () => {
  count.value++
}

値の展開

{{}}ではなく、{}です。

Svelte
<p>{hoge}</p>
Vue
<p>{{hoge}}</p>

on

例えばonClickは、Svelteではon:clickです。@v-onの省略なので、本質的には似たようなものです。

Svelte
<button on:click={clicked}>
  count: {count}
</button>
Vue
<button @click={clicked}>
  count: {{count}}
</button>

単方向/双方向バインド

やや罠。慣れれば直感的だとは思います。

  • v-bind (:)=> パラメータに {} を使う
  • v-model => bind:を使う
Svelte
<img src={imageUrl} />
<input bind:value={text} />
Vue
<img :src="imageUrl" />
<input v-model="text" />
※その他:省略記法

属性名と変数名が同じだった場合、省略することができます。

App.svelte
<script lang="ts">
const src = "http://example.com"
</script>

<img {src} />

computed => $:

Vueのcomputedは、Svelteでは$:を使います。癖アリ。
$:の後に書かれた文を読んで、依存する値が変更された際に中身が実行されます。言い換えるのであれば、$:の後ろに書かれたステートメントをリアクティブにする、という意味です。

奇怪な独自構文に見えますが、一応JSのラベル構文[2]をうまく使っているようです。

Svelte
$: squared = count * count
$: isMobile = window.innerWidth < 640
Vue
const squared = computed(() => count.value * count.value)
const isMobile = computed(() => window.innerWidth < 640)

watch => $:

$:の後に書かれた文を読んで、依存する値が変更された際に中身が実行されます。
…ということは、これでwatchを再現できます。

Svelte
let count = 2
$: if (count > 10) {
  console.log("count > 10!")
}
Vue
const count = ref(2)
watch(count, () => {
  if(count.value > 10) {
    console.log("count > 10!")
  }
})

ロジック

if, else

VueはDOM要素にv-ifv-elseを付けますが、Svelteは専用の書き方があります。
DOM要素と結びついていないので、最後にifを閉じることが必要です。忘れずに。

Svelte
{#if count === 0}
  <p>0</p>
{:else if count === 1}
  <p>1</p>
{:else}
  <p>over 1</p>
{/if}
Vue
<p v-if="count === 0">
  0
</p>
<p v-else-if="count === 1">
  1
</p>
<p v-else>
  over 1
</p>

for

以下のようなユーザ一覧があるとします。

Users
const users = [
  {
    name: "foo",
    age: 20,
  },
  {
    name: "bar",
    age: 30,
  },
]

v-forの代わりは{#each}を使います。
今回の例では示しませんが、indexを取ったり、分割代入を使う事もできます[3]

Svelte
{#each users as user}
  <div>
    <p>Name: {user.name}</p>
    <p>Age: {user.age}</p>
  </div>
{/each}
Vue
<div v-for="user in users">
  <p>Name: {{user.name}}</p>
  <p>Age: {{user.age}}</p>
</div>

コンポーネント

コンポーネントの呼び出し

Vueと大差ありません。

Svelte
<script lang="ts">
  import TestComponent from "./TestComponent.svelte"
</script>

<TestComponent />
Vue
<script setup lang="ts">
import TestComponent from "./TestComponent.vue";
</script>

<template>
  <TestComponent />
</template>

props

exportで公開した変数をpropsとして使用できます。
値を指定しておけば、propsが渡されなかった際にデフォルト値として使われます。

App.svelte
<script lang="ts">
  import TestComponent from "./TestComponent.svelte"
  const count = 1
</script>

<TestComponent num={count} />
TestComponent.svelte
<script lang="ts">
  export let num: number
  export let text: string = "default text"
</script>

<p>num is {num}</p>
<p>text: {text}</p>


App.vue
<script setup lang="ts">
import {ref} from "vue";
import TestComponent from "./TestComponent.vue";
const count = ref(1)
</script>

<template>
  <TestComponent :num="count" />
</template>
TestComponent.vue
<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も全く同じです。

App.svelte
<TestComponent>
  <h1>foo bar</h1>
  <p>baz</p>
</TestComponent>
TestComponent.svelte
<div class="card">
  <slot />
</div>


App.vue
<template>
  <TestComponent>
    <h1>foo bar</h1>
    <p>baz</p>
  </TestComponent>
</template>
TestComponent.vue
<div class="card">
  <slot />
</div>

名前付きslot

名前付きスロットの場合はやや違いがあります。
Vueではv-slottemplateにしか当てられませんが、svelteはDOM要素に直接当てられます。
加えてslotの指定方法もタグの属性で設定するような形です。

App.svelte
<TestComponent>
  <h1 slot="name">foo bar</h1>
  <p slot="text">baz</p>
</TestComponent>
TestComponent.svelte
<div class="card">
  <slot name="name" />
  <hr />
  <slot name="text" />
</div>


App.vue
<template>
  <TestComponent>
    <template #name>
      <h1>foo bar</h1>
    </template>
    <template #text>
      <p>baz</p>
    </template>
  </TestComponent>
</template>
TestComponent.vue
<template>
  <div class="card">
    <slot name="name" />
    <hr />
    <slot name="text" />
  </div>
</template>

特殊タグ

ここからはSvelteの特殊タグです。

@html

Vueで言うところのv-htmlです。Markdownなどを使うときに使います。
v-htmlと同じくサニタイズは行わないのでXSSの危険性があります。取り扱い注意です。

App.svelte
<script lang="ts">
  const text = "Super <strong>POWER</strong>"
</script>

<p>{@html text}</p>

@debug

指定した値が変更されるたび、ログに出力+devtoolsでpauseがかかります。
めちゃめちゃ優秀ですね。

App.svelte
<script lang="ts">
  let count = 1
</script>

<button on:click={() => {count++}}>
  count: {count}
</button>

{@debug count}

@const

ローカル変数を定義することができます。
{#if}{#each}, <Component />の中でのみ使えます。

App.svelte
{#each users as user}
  {@const text = `${user.name} (${user.age})`}
  <p>{text}</p>
{/each}

まとめ

触ってみるとかなり好感触で、Vueより簡潔に書けるのが魅力的でした。大抵新しいフレームワークは学習コストが高めですが、Composition APIと大体同じでちょっと構文が違うぐらいなので、全然すぐにでも使い始められそうな感じです。

今回は触れませんでしたが、Tween, Transitionなどアニメーション周りも充実していました。aちょっとした動きならanime.jsも要らなくなりそうです。より色々なExampleを参考にしたい方は、公式のドキュメントに色々乗っているのでこちらもご確認ください。

https://svelte.jp/examples/dynamic-attributes

おまけ

ZennではPrismJSのシンタックスハイライトができますが、Svelteは対象外です。だいたい構文がVueと似ているので、vue:App.svelteとかにするとそれっぽく表示できます。Prismくん、対応待ってます。

脚注
  1. https://svelte.jp/docs/element-directives#bind-property ↩︎

  2. https://svelte.jp/docs/typescript#limitations-reactive-declarations ↩︎ ↩︎

  3. https://svelte.jp/docs/logic-blocks#each ↩︎

Discussion