【JavaScript】Svelteを触ってみた

この記事は[レコチョク Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku)の17日目の記事となります。

# はじめに
こんにちは。株式会社レコチョクでmurketというサービスのフロントエンドエンジニアをしている坂本と申します。
最近は作業のおともにエイハブさんの「アクター」や「クィホーティ」を聴いています。

さて、本題に入ろうかと思うのですが、みなさんはJavaScriptのフレームワークといえば何を思い浮かべるでしょうか。Vue.js？それともReact？
JavaScriptのフレームワークといえばこの二強という印象がありますが、今回は最近勢いのある第三勢力**Svelte**を触ってみたので記事にします。

# Svelteとは
ここでSvelteの概要について軽く触れておきたいのですが、要約が~~面倒くさい~~難しかったのでChatGPT先生に「JavaScriptのフレームワークであるSvelteについて、300文字程度でJavaScript中級者に対して説明してください。」と聞いたら以下のような答えが返ってきました。

> Svelteはコンパイル時にUIを構築する革新的なアプローチを採用したJavaScriptフレームワークです。ReactやVueのような従来のフレームワークがブラウザでの実行時にDOMを操作するのと対照的に、Svelteはビルド時に必要なコードを生成し、ブラウザが理解できる最適化された命令セットに変換します。このため、実行時のライブラリ依存がなく、軽量で高速な実行が可能です。Svelteコンポーネントは可読性が高く、宣言的なHTML、CSS、JavaScriptのコードで構成されており、リアクティブなステート管理を直感的に行えます。開発体験はシンプルでありながら、パフォーマンスとメンテナンス性のバランスを巧みにとっています。

だそうです。つまりVue.jsやReactとの大きな違いは、**Svelteはコンパイラであり仮想DOMを使用しない**ところにあります。そのため実行速度が他のフレームワークに比べて優れているとのこと。
その辺りの仕様についても検証してみたいところですが、まずはこの記事ではSvelteKit（SvelteとViteで構成されたフレームワーク）を使っていいねボタンを作ってみることで、ざっくりとしたディレクトリ構成やリアクティビティシステムに触れるところまでをやってみます。

# SvelteKitのインストールとセットアップ
ではさっそく試しに以下のコマンドを打ち込んでSvelteを手元で動かしてみます。`myapp`の部分には適宜好きなアプリ名を入力してください。
`npm create svelte@latest myapp`
セットアップするのにいくつか質問されるので、以下を参考に環境構築をしてみてください。

```
Ok to proceed? (y)
```
yを押してインストールを進めます。

```
┌  Welcome to SvelteKit!
│
◆  Which Svelte app template?
│  ● SvelteKit demo app (A demo app showcasing some of the features of SvelteKit
- play a word guessing game that works without JavaScript!)
│  ○ Skeleton project (Barebones scaffolding for your new SvelteKit app)
│  ○ Library project(Barebones scaffolding for your new Svelte library)
```

上から

- デモアプリ
- SvelteKitでアプリを作成
- Svelteのライブラリを作成

になります。今回は2つ目を選択しました。
さっくりSvelteがどんなものか知りたい人は１つ目を選択してみても良いと思います。選ぶと単語当てクイズアプリが作成されます。

```
◆  Add type checking with TypeScript?
│  ● Yes, using JavaScript with JSDoc comments
│  ○ Yes, using TypeScript syntax
│  ○ No
└
```
TypeScriptを使うかどうかを選びます。今回は1つ目（使わない）を選択しました。

```
◆  Select additional options (use arrow keys/space bar)
│  ◻ Add ESLint for code linting
│  ◻ Add Prettier for code formatting
│  ◻ Add Playwright for browser testing
│  ◻ Add Vitest for unit testing
│  ◻ Try out Svelte 5 beta
└
```
オプションをスペースキーを押して選択していきます。今回は一旦全部チェックなしで進めました。
ここまででセットアップは完了です。

```
Next steps:
  1: cd myapp
  2: npm install
  3: git init && git add -A && git commit -m "Initial commit" (optional)
  4: npm run dev -- --open
```
Next stepsに従って`cd myapp`でディレクトリを移動し、`npm install`を実行します。手順3は今回必要無いのでスキップします。
`npm run dev -- --open`を実行して以下のような画面が表示されればOKです。

![image-20231201012824165.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2269991/5f2ad795-1bd9-1b84-8fdc-5a5317124616.png)

# ディレクトリの構造
インストールとセットアップが済んだところで、さっそく手を動かしつつディレクトリの構造と各ディレクトリ・ファイルの役割を確認していきます。
（以下の図では`/.svelte-kit`と`/node_modules`を直接いじることは無いので割愛しています。）

```
myapp/
├ src/
│ ├ lib/
│ │ └ index.js
│ ├ routes/
│ │ └ +page.svelte
│ ├ app.html
│ ├ app.d.ts
├ static/
│ └ favicon.png
├ package.json
├ svelte.config.js
├ tsconfig.json
└ vite.config.js
```
## indexページを編集してみる
まず、`src/routes/`にある`+page.svelte`を見てみます。
```svelte:+page.svelte
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
```
このような内容になっており、`npm run dev -- --open`を実行して最初に現れるページの内容がこのファイルに書かれていることがわかりました。
試しに以下のように変更してみます。
```svelte:+page.svelte
<h1>Welcome to my site</h1>
<a href="#">About my site</a>
```
保存すると画面上での表示も変わりましたね！

![image-20231201012937254.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2269991/2ae35af0-1413-1b8e-cf1d-b9a70fd1ecf9.png)

## ページを追加する
では次に、新しいページを追加していきたいと思います。
`src/routes/`配下に`about`という名前でディレクトリを作り、`+page.svelte`という名前でファイルを作成します。内容は一旦以下だけ記述しておきます。
```svelte:/about/+page.svelte
<h1>about</h1>
```
Svelteではルーティングが`src/routes/`配下にあるディレクトリごとに切り分けられており、各ページの内容は`+page.svelte`というファイルに記述していきます。つまり、今作ったページのアドレスは`http://localhost:xxxx/about`になります。
直接URLを叩いて確認しても良いですが、せっかくなのでindexページを以下のように変更してみます。
```svelte:+page.svelte
<h1>Welcome to my site</h1>
<a href="/about">About my site</a>
```
保存したあとにもう一度画面を見てみると、リンクが正しく動作しaboutページが表示されることが確認できました。

## 共通レイアウトを追加する
大まかなルーティングの仕組みが分かったところで、次は共通レイアウトを追加してみます。ヘッダーメニューやフッターなど、全ページに共通して表示させたいものは`src/routes/`配下に`+layout.svelte`という名前でファイルを作成し、そこに記述すれば全てのページに適用されます。
試しに共通のnav要素とfooterを追加してみました。`+page.svelte`の内容は`<slot />`部分に表示されます。
```svelte:+layout.svelte
<div>
  <nav>
    <span>my app</span>
    <ul>
      <li>menu1</li>
      <li>menu2</li>
      <li>menu3</li>
    </ul>
  </nav>

  <main>
    <slot /> // ここに+page.svelteがレンダリングされる
  </main>

  <footer>
    <p>footer</p>
  </footer>
</div>
```
また、スタイルはファイルに`style`タグを置いてその中に直接CSSを記述することもできますし、もちろん別ファイルをインポートすることでも適用できます。その場合はファイルの頭で以下のように宣言します。
```svelte:+page.svelte
<script>
  import "./styles.css"; // 適用したいファイルのパス
</script>
```
ぱっと見でわかりやすいように適当にスタイルを当てたものが以下になります（CSSは割愛します）。

![image-20231201013107211.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2269991/d49adb8a-493f-5965-0c66-a437f482cf00.png)

# リアクティビティシステムを扱ってみる
ここまででざっくりディレクトリの構造について確認してみましたが、次はいいねボタンを設置することでリアクティビティシステムを扱ってみます。
`src/routes`に`LikeButton.svelte`という名前でファイルを作り、以下の内容を記述します。
```svelte:LikeButton.svelte
<script>
  let count = 0;

  function increment() {
    count += 1;
  }
</script>

<div>
  <button on:click={increment}>
    like {count}
  </button>
</div>
```
また、`src/routes/+page.svelte`に`LikeButton.svelte`を設置します。
```svelte:+page.svelte
<script>
  import LikeButton from "./LikeButton.svelte";
</script>

<h1>Welcome to my site</h1>
<a href="/about">About my site</a>
<LikeButton />
```
するとこのようにいいねボタンを設置することができました。

![likebutton.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2269991/e4ee8316-32f3-84ec-60ed-665b562065d9.gif)

次は10回しかいいねボタンを押せないようにし、ボタンの下に残りの回数を表示するようにします。
残り回数は`count`の増加に連動して値を変える必要があるため、これを実装するには**リアクティブ宣言**を使う必要があります。そのため、残り回数を管理する変数（`countsLeft`とします）は以下のように宣言します。
```svelte
$: countsLeft = 10 - count;
```
リアクティブ宣言を行うとき、内容が未定義の変数への代入だけだった場合はsvelteが自動で`let`宣言してくれるため、`countsLeft`の頭に`let`をつける必要はありません。
また、`count`が10に達した時、ユーザーへもういいねボタンを押すことができないことを`alert()`で知らせる処理も追加してみます。
リアクティブ宣言では、`countsLeft`のような値だけでなく以下のように`console.log`やif文といったステートメントもリアクティブに扱うことができます。また、`{}`を使ってブロックを作ることでステートメントをまとめることもできます。
```svelte
$: {
  console.log('like button clicked')
  if (count >= 10) {
    alert('Thanks a lot!');
  }
}
```
`maxCount`という定数を新たに定義して、すでにある処理と上記の記述を合体させたものが以下になります。

```svelte:+page.svelte
<script>
  let count = 0;
  const maxCount = 10;
  $: countsLeft = maxCount - count;
  $: {
    console.log('like button clicked')
    if (count >= maxCount) {
      alert('Thanks a lot!');
    }
  }

  function increment() {
    if (count < maxCount) {
      count += 1;
    }
  }
</script>

<div class="like-button">
  <button on:click={increment}>
    like {count}
  </button>
  <p>You can push {countsLeft} more times.</p>
</div>
```
するとこのようにいいねの残り回数とアラートが表示されるようになりました。

![alert.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2269991/d8eb8ec6-afea-9e91-f9fc-3ec726781944.gif)

# おわりに
ここまでで、SvelteKitで作成されたアプリのディレクトリ構成やリアクティビティシステムに触れてきました。
特徴的なファイル名であったりリアクティブ宣言であったり、あまり見慣れない構文に戸惑う場面は多々ありましたが、他のjsフレームワークを扱った経験があれば慣れることはそこまで難しくは無さそうな印象を受けました。
まだVue.jsやReactと比べるとライブラリやツールが少なかったり情報が充実しているとは言えなかったり（とはいえ有志によって日本語化が進められていたり、DiscordにSvelteの日本コミュニティがあったりします）といった欠点はありそうですが、フロントエンド開発手段の選択肢を増やすためにも引き続き学習を続けていきたいと思っています。

明日の [レコチョク Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku) は18日目「AWS LambdaのCI・CD環境を構築してみた」です。お楽しみに！

# 参考にしたもの
- [Svelte • Cybernetically enhanced web apps](https://svelte.dev/)
- [SvelteKit • 効率的で無駄のない、研ぎ澄まされた Web 開発](https://kit.svelte.jp/)
- [Svelteとは](https://zenn.dev/toshitoma/articles/what-is-svelte)

***
この記事はレコチョクのエンジニアブログの記事を転載したものとなります。

https://techblog.recochoku.jp/9976