LoginSignup
9
3

More than 1 year has passed since last update.

来るかも知れないSvelteの最小ハンズオン

Last updated at Posted at 2022-02-09

TechConnect!2022年2月のリレー記事です。
リンク情報システムのengineer.hanzomon のグループメンバでリレーしています(Facebookはこちら)。


少し古いWEB+DB PRESS(Vol.122 2021年5月)を見たらSvelte(スヴェルト)というフロントのフレームワークが載っていました。
その記事があったことをサッパリ覚えていなかったのですが、ちょいと調べてみると、JavaScriptの利用動向に関する年次調査 The State of JavaScript Surveyにて、2021年はSolidとSvelteが90%の満足度であり、ReactとVueを抜いています。

image.png

Svelteの所感

  • 公式が言っているとおり、ビルド、ホットリロードが速いです。
  • コード量も確かに少なくて済む感じがした。
  • コード記述は、Vueと比較して、より直感的と感じました。

そんなSvelteを勉強がてら、ハンズオンで使えるような記事を書いてみました。

作るもの

SvelteKitを使って簡易なTODOアプリを作っていきます。
(SvelteKitは、VueでいうところのNuxtにあたるもの)

image.png


バージョン

  • svelte 3.46.4
  • sveltekit 1.0.0-next.260

前提

  • SveleteとSveleteKitの学習に重きをおいて、CSSは完全排除。
  • Svelte、SvelteKitの多くの機能を説明するハンズオンではないです(ストアやモーション等々)。
  • 入力チェック等々は範囲外としています。

1. プロジェクト作成

SvelteKitのプロジェクトを作成します。
nodeのバージョン変更はnodistで行っています。

nodist 14.15.4
nodist npm match

npm init svelte@next svelte-handson

# 構築オプション
√ Which Svelte app template? » Skeleton project
√ Use TypeScript? ... No
√ Add ESLint for code linting? ... Yes
√ Add Prettier for code formatting? ... Yes

cd svelte-handson
# ライブラリインストール
npm install
# 起動
npm run dev -- --open

npm run dev--open(または-o) オプションを付けることで、自動でブラウザが起動、画面表示が行われます。

正常に起動すると、この画面が表示されます。
image.png

2. 自動生成されたファイルを見てみる

VSCodeでsvelte-handsonフォルダを開いてみましょう。
初期状態は以下のフォルダ、ファイル構成になります。

image.png
srcには、ページやコンポーネントを格納します。
staticは、静的な画像などを配置する場所になります。

/svelte.config.jsを覗いてみましょう。

/svelte.config.js
import adapter from '@sveltejs/adapter-auto';

/** @type {import('@sveltejs/kit').Config} */
const config = {
    kit: {
        adapter: adapter()
    }
};

export default config;

SvelteKitの設定はsvelte.config.jsに記載することになります。

import adapterの箇所はデプロイ先に合わせたビルド物の生成を制御します。
今回はローカル実行なので、よきに計らってくれる@sveltejs/adapter-autoのままにします。

他に以下のようなアダプタがあります。

  • Netlify(adapter-netlify)
  • Vercel(adapter-vercel)
  • SSR向け(adapter-node)
  • SSG向け(adapter-static)

SPAについては、fallbackを設定することでSPAになるそうです。
https://github.com/sveltejs/kit/tree/master/packages/adapter-static

次に/src/routesの中を見てみます。

  • /src/routes/index.svelteが最初に表示されているページです。
  • /src/routesにページやエンドポイントを作成していくことになります。
  • エンドポイントも/src/routesに作っていくことになります。
  • 巷のサンプルでは、フロントのページ系ファイルと、サーバサイド系のファイルが同一フォルダに配置さているものが多いようですが、整理が悪い感じがするので、このハンズオンでは、サーバサイド系は/src/routes/apiに配置していきます。

3. 追加ボタン

トップページに「追加」ボタンを追加してみます。
ついでに見出しも「TODO」に変更します。

<h1>TODO</h1>
<a href="/todo/add">追加</a>

image.png

4. 追加ページの作成

/src/routes/todo/add.svelteを新規作成します。

/src/routes/todo/add.svelte
<script>
    import { goto } from '$app/navigation';
    import * as api from '$lib/api_client.js';
    let todo = '';

    // TODOを新規登録する
    async function submit(event) {
        await api.post('/api/todo', { todo });
        goto('/');
    }
</script>

<h1>TODO追加</h1>

<form on:submit|preventDefault={submit}>
    <input type="text" placeholder="TODO" bind:value={todo} />
    <br />
    <button> 追加 </button>
</form>

image.png

追加ページの説明

/src/routes/todo/add.svelte(JS部)
<script>
    import { goto } from '$app/navigation';
    import * as api from '$lib/api_client.js';
    export let todo = '';

    // TODOを新規登録する
    async function submit(event) {
        await api.post('/api/todo', { todo });
        goto('/');
    }
</script>

$app/navigationは、SvelteKitの組込みモジュールです。
先頭が$のものはSvelteKitの組込みモジュールと思ってください($libだけ例外)。
単純なページ遷移でgoto()を利用します。

export letでtodoを宣言していますが、これはプロパティーになります。
exportを付けることでコンポーネント外から当該プロパティーにアクセスが可能になります。
また、同一ページ内のload()(後述)から、プロパティーに値を設定する場合もexport付与が必要です。
従って、外のコンポーネントや、load()からプロパティーにアクセスせず、ページ内でのバインドのみで使用する場合は、exportは不要です。
(なので、実はこの追加ページでは、exportは不要でlet todo = '';で十分だったりします)

submit()では、TODO追加のエンドポイントをコールしています。

/src/routes/todo/add.svelte(HTML部)
<form on:submit|preventDefault={submit}>

formのsubmitイベント発動時にsubmit()をコールする場合の書き方です。

<input type="text" placeholder="TODO" bind:value={todo} />

bind:value={todo} で、入力値をプロパティーにバインドさせます。
バインドさせることでスクリプト側で値が参照可能になります。

5. 追加エンドポイントの作成

/src/routes/api/todo/index.svelteを新規作成します。

/src/routes/api/todo/index.svelte
import todoDb from '$lib/TodoDb';

// TODOを追加する
export async function post({ request }) {
    const data = await request.json();

    todoDb.add(data.todo);

    return { body: {} };
}

http://localhost:3000/api/todoのエンドポイントです。
function名は、HTTPメソッド名で記述することで、当該リクエストに準じたfunctionがコールされます。

ここでは、postでTODOの新規登録を受け付け可能とします。
引数でrequestをもらえるので、JSONとして受け取り、todoDbに登録しています。

postのみを実装しているので、http://localhost:3000/api/todoをブラウザでアクセスすると、404になります。

APIのクライアントを作っていきましょう。

6. APIクライアントの作成

エンドポイントをコールするためのクライアントを作成します。

/src/lib/api_client.jsを新規作成します。

/src/lib/api_client.js
// リクエスト送信先
const BASE_HOST = 'http://localhost:3000';

// リクエストを送信する
async function send({ method, path, data }) {
    const opts = { method, headers: {} };

    opts.headers['Content-Type'] = 'application/json';
    if (data) {
        opts.body = JSON.stringify(data);
    }

    return fetch(`${BASE_HOST}${path}`, opts)
        .then((r) => r.text())
        .then((json) => {
            try {
                return JSON.parse(json);
            } catch (err) {
                return json;
            }
        });
}

// GETリクエストを送信する
export function get(path) {
    return send({ method: 'GET', path });
}

// POSTリクエストを送信する
export function post(path, data) {
    return send({ method: 'POST', path, data });
}

// PUTリクエストを送信する
export function put(path, data) {
    return send({ method: 'PUT', path, data });
}

get、post、putを行うためのfunctionと、そこからコールするsend()の構成です。
send()では、HTTPメソッド、URL、およびデータを元にHTTPリクエストを投げています。

7. TodoDbの作成

今回は、RDBなどは用いず、JSの配列でデータを保持させています。
よって、サーバ再起動でデータはクリアされます。

/src/lib/TodoDb.jsを新規作成します。

/src/lib/TodoDb.js
// TODOを保持するクラス
class TodoDb {
    constructor() {
        // データ
        this.data = [];
    }

    // 追加する
    add(todo) {
        const entity = { id: this.data.length, todo, finished: false };
        this.data.push(entity);
    }

    // 更新する
    update(todoEntity) {
        const entity = {
            id: todoEntity.id,
            todo: todoEntity.todo,
            finished: todoEntity.finished
        };
        this.data[todoEntity.id] = entity;
    }

    // 全件返却
    getAll() {
        return this.data;
    }

    // 指定IDのデータを返す
    get(id) {
        return this.data[id];
    }
}

const todoDb = new TodoDb();
export default todoDb;

/src/lib配下にJSを作成することで、外部から$lib/xxxで参照可能となります。
TodoDb自体はSvelteの話ではないので、詳細は割愛します。
Todoデータを配列で管理しているだけです。

8. トップページ(TODO一覧)

データの追加ができるようになったので、一覧ページを作っていきます。

/src/routes/index.svelteを開きます。
以下のように修正します。

/src/routes/index.svelte
<script context="module">
    import * as api from '$lib/api_client.js';

    export async function load({ params, session }) {
        // TODOを全件取得する
        const res = await api.get('/api/todos');
        return {
            props: { todos: res }
        };
    }
</script>

<script>
    import { invalidate } from '$app/navigation';
    export let todos = [];

    // TODOを完了にする
    async function compClick(id) {
        await api.put(`/api/todo/${id}/complete`);
        invalidate('/');
    }
</script>

<h1>TODO</h1>
<a href="/todo/add">追加</a>

<ul>
    {#each todos as todo}
        <li>
            <a href="/todo/edit/{todo.id}">{todo.todo}</a>
            {#if !todo.finished}
                <button on:click={compClick(todo.id)}>完了</button>
            {:else}
                済
            {/if}
        </li>
    {/each}
</ul>

画面表示時にコールされるload()でTODOデータ取得を行い、一覧を生成しています。

image.png

一覧ページの説明

ブロックごとに説明します。

/src/routes/index.svelte(データの取得)
<script context="module">
    import * as api from '$lib/api_client.js';

    export async function load({ params, session }) {
        // TODOを全件取得する
        const res = await api.get('/api/todos');
        return {
            props: { todos: res }
        };
    }
</script>

load()はコンポーネントが作成される前に実行されます。
データをエンドポイントから取得する場合は、load()で行う必要はないようですが、統一的にload()で書くようにするのが良いと思われます。

上記では、エンドポイント/api/todosからTODOを全件取得し、returnしています。
propsに格納してreturnすることで、このページのプロパティーに自動でセットしてくれます。

/src/routes/index.svelte(イベント処理部)
<script>
    import { invalidate } from '$app/navigation';
    export let todos = [];

    // TODOを完了にする
    async function compClick(id) {
        await api.put(`/api/todo/${id}/complete`);
        invalidate('/');
    }
</script>

load()で取得したデータはtodosに自動でセットされます。
compClick()はTODOを完了にするエンドポイントをコールします。その後、SvelteKitの組込みであるinvalidate()によって、一覧データを再描画させています。
invalidate() は、load()でfetchしてるものについて、すべて再実行してくれます。

/src/routes/index.svelte(HTML部)
<h1>TODO</h1>
<a href="/todo/add">追加</a>

<ul>
    {#each todos as todo}
        <li>
            <a href="/todo/edit/{todo.id}">{todo.todo}</a>
            {#if !todo.finished}
                <button on:click={compClick(todo.id)}>完了</button>
            {:else}
                済
            {/if}
        </li>
    {/each}
</ul>

Svelteは#eachでループ処理を記述します。
分岐は#if:else/ifです。
#:/と、記号の使い方が少々気持ち悪いですが、Svelteでは、#はブロックの始まり、/はブロックの終わりを意味し、:はブロックの継続を示します。

buttonのon:clickcompClickをコールしています。

9. 編集ページの作成

/src/routes/todo/edit/[id].svelteを新規作成します。
[id]の部分はRESTのパラメータで置換される部分となります。

例えば、ブラウザから/todo/edit/5にアクセスすると、上記のファイルが実行されます。

/src/routes/todo/edit/[id].svelte
<script context="module">
    import * as api from '$lib/api_client.js';

    export async function load({ params }) {
        // TODOの取得
        const res = await api.get(`/api/todo/${params.id}`);
        return {
            props: { todo: res }
        };
    }
</script>

<script>
    import { goto } from '$app/navigation';

    export let todo;

    // 編集したTODOを更新登録する
    async function submit(event) {
        await api.put(`/api/todo/${todo.id}`, { todo });
        goto('/');
    }
</script>

<h1>TODO編集</h1>

<form on:submit|preventDefault={submit}>
    <input type="text" placeholder="TODO" bind:value={todo.todo} />
    <br />
    <button> 更新 </button>
</form>

image.png

編集ページは、一覧ページと追加ページで説明した内容と構成が同じため、詳細は割愛します。
const res = await api.get(`/api/todo/${params.id}`);の、params.idの部分がRESTパラメータを取得している箇所となります。
ファイル名を[id].svelteとしているので、paramsからidの名前で値を取得することができます。

10. 1件データ取得と更新登録のエンドポイント

/src/routes/api/todo/[id]/index.jsを新規作成します。

/src/routes/api/todo/[id]/index.js
import todoDb from '$lib/TodoDb';

// TODOを更新する
export async function put({ request }) {
    const data = await request.json();

    todoDb.update(data.todo);

    return { body: {} };
}

// TODOを1件返す
export async function get({ params }) {
    // RESTパラメータからIDを抽出
    const { id } = params;

    const todo = todoDb.get(id);
    return { body: todo };
}

put()はrequestからデータを取得し、todoDbの更新処理に渡しています。
get()はRESTパラメータからidを取得し、当該idのデータをtodoDbから受け取っています。
RESTパラメータの取得は、前述の[id].svelteと同じ考え方です。
このケースでは、フォルダ名でRESTパラメータを定義しています。

11. TODO完了のエンドポイント作成

/src/routes/api/todo/[id]/complete.jsを新規作成します。

/src/routes/api/todo/[id]/complete.js
import todoDb from '$lib/TodoDb';

// TODOを完了にする
export async function put({ params }) {
    const { id } = params;

    const todoEntity = todoDb.get(id);
    todoEntity.finished = true;
    todoDb.update(todoEntity);

    return { body: {} };
}

put()にて、指定のIDのデータを一旦取得し、finishedtrueを代入、todoDbの更新を行っています。


おしまい。
お疲れさまでした!

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3