Subterranean Flower

Svelteで始める頑張らないフロントエンド生活 後編

Author
古都こと
ふよんとえんよぷよぐやま!!!

この記事は「Svelteで始める頑張らないフロントエンド生活 前編」の続きです。読み進める前に、先にそちらをご覧ください。

前編のおさらい

Svelteで始める頑張らないフロントエンド生活 前編」ではSvelteの基本的な使い方を学びました。

Svelteでは、変数のバインド、コンポーネントの作成、分岐表示処理などを簡単に実現することができました。Svelteは他のフレームワークほど強力な機能を持っているわけではありませんが、シンプルなアプリを作る際には心強い機能を提供してくれます。

今回作るもの

ノートアプリ

Svelteの基本的な使い方については習得したので、今回はノートアプリを作ってみましょう。以下のようなイメージです:

シンプルですが、ウェブアプリを作る上で重要なことを制作過程で学べます。

Single Page Application(SPA)

Single Page Application(SPA)という言葉を聞いたことがあるでしょうか?普通、ウェブページは複数のページに分かれています。そのためページの数だけファイルを用意する……というのが一般的でした。

しかし2010年頃になって、SPAという考えが流行り始めました。SPAというのはファイルは1ページ分だけ用意し、表示内容はプログラム側で切り替えるというものです。近年ではもはやSPAの方が標準的になってきています。

Svelteでもsvelte-spa-routerというパッケージを利用することで簡単にSPAを作ることが可能なので、やってみましょう。

ソースコード

今回作成するアプリの完成済みソースコードは以下にあります:

https://github.com/subterraneanflowerblog/svelte-spa-note

前準備

Svelteプロジェクトの準備

開発環境の準備については「前編」をご覧ください。ここでは開発環境構築済みとして話を進めてきます。

まずプロジェクトフォルダを作ります。適当なフォルダに移動し、以下のコマンドを叩きます。「svelte-spa」の部分がプロジェクト名になります(他の名前にしてもらっても構いません)。

npx degit sveltejs/template svelte-spa

これで「svelte-spa」フォルダができるので、cdコマンドで中に移動して、npm installします。

cd svelte-spa
npm install

これが済んだら、SPA開発用の追加パッケージ「svelte-spa-router」をインストールします。

npm install --save-dev svelte-spa-router

これで「svelte-spa-router」がインストールされるはずです。

HTML/CSSの準備

作り始める前に、最初に生成されたファイルをちょっといじります。

まずはpublicフォルダの中にあるindex.htmlの2行目をいじります。lang=”en”になっているのでlang=”ja”にしておきます。あとは、タイトルが気になる方は7行目あたりにある<title>も変更して良いでしょう。

<html lang="ja">

次にpublicフォルダの中のglobal.cssを編集します。こちらは内容を全て削除し、以下のように完全に書き換えます:

* {
	margin: 0;
	padding: 0;
	box-sizing: border-box;
}

html,
body {
	font-family: system-ui, sans-serif;
	width: 100%;
	height: 100%;
}

input:focus,
textarea:focus,
button:focus {
	outline: none;
}

これで前準備は完了です。あとはがんばって作っていきましょう。

最初の画面の作成

ノート一覧画面の作成

まずノート一覧画面を作成します。さっそくsrc/App.svelteを編集したいところですが、その前にまずはノート一覧表示コンポーネントを作成します。

srcフォルダの中に「components」フォルダを作成します。

そしてcomponentsフォルダの中に「NoteList.svelte」ファイルを作成します。NoteListコンポーネントはノートデータ一覧配列をpropsとして受け取り、画面に表示します。

<script>
  import { push } from 'svelte-spa-router'
  export let notes;
</script>

<div class="note">
  {#each notes as note, index}
    <div class="card" on:click={() => push(`/edit/${index}`)}>
      <div class="card-title">{note.title}</div>
      <div class="card-content">{note.content}</div>
    </div>
  {/each}
</div>

<style>
  .note {
    color: rgb(63, 62, 60);
  }
  
  .card {
    background-color: rgb(255, 254, 253);
    box-shadow: 0 0 6px rgba(0, 0, 0, 0.2);
    padding: 1em;
    cursor: pointer;
  }

  .card:not(:last-of-type) {
    margin-bottom: 1em;
  }
  
  .card-title {
    font-size: 1.5em;
    font-weight: bold;
    margin-bottom: 0.5em;
  }

  .card-content {
    white-space: pre-wrap;
    line-height: 1.5;
  }
</style>

exportした変数は、外部から値を受け取ることが可能です。ここではnotesという変数をexportしています。

受け取った配列を#eachでループ処理し、それぞれのデータを表示します。#eachでは各データとインデックス(番号)を取得することができます。

各ノートはon:clickでクリックできるようになっており、クリックすると`/edit/${index}`をpushします。pushはsvelte-spa-routerからimportしてきた関数で、詳しくは後で説明します。

次にsrcフォルダ直下に「Home.svelte」を作成します。これがノート一覧画面になります。まだノートを管理する機能がないので、とりあえずはコードに直接ダミーのノートデータを書いておきます。このダミーデータをNoteListに渡し、表示してもらいます。

<script>
  import { push } from 'svelte-spa-router';
  import NoteList from './components/NoteList.svelte';

  const userNotes = [
    { title: 'テストノート', content: 'これはテストです!' },
    { title: 'やることリスト', content: '・Svleteの勉強\n・アプリ作成\n・公開' }
  ];
</script>

<div class="home">
  <h1 class="app-title">Svelte Note</h1>
  <NoteList notes={userNotes}></NoteList>
  <button class="add" on:click={() => push('/add')}>+新しいノート</button>
</div>

<style>
	.app-title {
		margin-bottom: 1em;
	}

	.add {
		display: block;
		background-color: rgb(75, 168, 51);
		border: none;
		box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
		color: white;
		font-size: 1.5em;
		width: 100%;
		padding: 0.5em 0;
		margin: 1em 0;
		cursor: pointer;
	}
</style>

Home画面にはアプリタイトル、NoteList、新しいノートボタンがあります。さて、ここでまたpushが出てきます。新しいノートボタンを押すと’/add’をpushします。

最後に「App.svelte」側からHome画面を呼び出します。

<script>
  import Home from './Home.svelte';
</script>

<main>
  <Home></Home>
</main>

<style>
	main {
		background-color: rgb(255, 251, 246);
		padding: 2em;
		width: 100%;
		height: 100%;
		overflow-y: auto;
	}
</style>

画面の管理を簡単にするため、App.svelteには難しいことは書かないようにします。

ここまでを動かしてみる

とりあえずここまでを動かしてみましょう。npm run devコマンドで起動できます。

npm run dev

コマンドを叩くとlocalhost:5000とかのURLが出てくると思うのでブラウザでそこにアクセスします。

ダミーのノートが表示されたと思います。前編から続けてやっている方はブラウザに妙なキャッシュが残っていて表示が崩れるかもしれません。そのときはshiftキーを押しながらリロードしてください。

まだ何の機能も作っていないので動作はしませんが、2つのノートと新しいノートボタンがクリックできるはずです。そしてクリックしてからよく観察してみると、少し変化が現れています。

URLの後ろの部分が書き換わっています。「/edit/0」という風に。

ハッシュの書き換え

「/edit/0」という文字列はどこかでみたはずです。そう、NoteListでこれをpushしていました。svelte-spa-routerのpush関数は、URL書き換えのための関数だったのです。

先述の通り、SPAでは1枚のページだけを使って、プログラム側から内容を書き換えることで複数ページが存在するように見せかけます。そのためには「今どのページを表示しているのか」を管理する必要があります。

その管理手法のひとつとしてURLでのハッシュがあります。ハッシュというのはURLの末尾についている「#」以降の文字列のことで、ひとつのページ内での位置を表します。

ハッシュを使って表示ページを管理する方法は、最もシンプルで、最も楽です。svelte-spa-routerでもこの方法を採用しており、それを実現するための関数がpushです。

しかしまだハッシュが書き換わっただけで、表示は全く変わっていません。次はページ切り替えを作ってみましょう。

ページ切り替え

新規ノート追加ページ

ページ切り替えを実現するためには、まず複数のページが必要です。ノート一覧ページができたので、次は新規ノート追加ページを作ります。

まず前準備としてデータ読み書きのライブラリを作ります。srcフォルダの下に「lib」フォルダを作り、そこに「storage.js」ファイルを作りましょう。

const storageKey = 'sveltenote/notes';

export const loadNotes = () => {
  const rawNotes = sessionStorage.getItem(storageKey);
  return rawNotes ? JSON.parse(rawNotes) : [];
};

export const saveNotes = (notes) => {
  sessionStorage.setItem(storageKey, JSON.stringify(notes));
};

export const addNote = (note) => {
  const currentNotes = loadNotes();
  const newNotes = [...currentNotes, note];
  saveNotes(newNotes);
}

export const overwriteNote = (index, note) => {
  const currentNotes = loadNotes();
  const newNotes = [...currentNotes];
  newNotes[index] = note;
  saveNotes(newNotes);
};

storage.jsではsessionStorage(タブを閉じるまでの間データを保持してくれる仕組み)を用いてデータの読み書きを行います。

もしブラウザを閉じた後もデータを保持したい場合は、sessionStorageの代わりにlocalStorageを使用してください。

次にsrcフォルダの下のcomponentsフォルダに「NoteEditor.svelte」ファイルを作ります。

<script>
  export let title;
  export let content;
</script>

<div class="editor">
  <input class="title" placeholder="タイトル" bind:value={title}>
  <textarea class="content" placeholder="本文" bind:value={content}></textarea>
</div>

<style>
  .editor {
    display: flex;
    flex-direction: column;
    height: 100%;
  }

  .title {
    display: block;
    background-color: transparent;
    border: none;
    border-left: 2px solid rgb(200, 86, 20);
    font-size: 2em;
    padding: 0.5rem 1rem;
    margin-bottom: 0.3em;
    width: 100%;
  }

  .content {
    flex: 1;
    display: block;
    background-color: transparent;
    border: none;
    border-left: 2px solid rgb(200, 86, 20);
    font-size: 1.1em;
    padding: 1rem 1rem;
    margin-bottom: 0.3em;
    width: 100%;
    resize: none;
  }
</style>

NoteEditorはその名の通りノートのエディタで、タイトルと本文を入力できます。中身としてはinputとtextareを置いているだけです。titleとcontentをexportしており、外部からその値をバインドすることが可能です。

このNoteEditorを使用して新規ノートページを作ります。srcフォルダ直下に「Add.svelte」ファイルを作ります。

<script>
  import { push } from 'svelte-spa-router';
  import NoteEditor from './components/NoteEditor.svelte';
  import { addNote } from './lib/storage';

  let title = '新しいノート';
  let content = '';

  const onSave = () =>  {
    addNote({title, content});
    push('/');
  };
</script>

<div class="add">
  <NoteEditor bind:title={title} bind:content={content}></NoteEditor>
  <div class="button-container">
    <button class="save" on:click={onSave} disabled={!title || !content}>保存</button>
  </div>
</div>

<style>
  .add {
    display: flex;
    flex-direction: column;
    height: 100%;
  }

  .button-container {
    padding: 1em 0;
    text-align: right;
  }

  .save {
    background-color: rgb(62, 68, 163);
    border: none;
    border-radius: 3px;
    color: white;
    font-size: 1em;
    padding: 0.5em 1em;
    cursor: pointer;
  }

  .save:disabled {
    opacity: 0.3;
    cursor: auto;
  }
</style>

Add.svelteにはNoteEditorと保存ボタンがあります。NoteEditorの入力内容をbind:titleとbind:contentでバインドして読み出しています。

保存ボタンを押すとonSave関数が呼ばれます。onSave関数は、lib/storage.jsからimportしてきたaddNote関数でノートを追加保存します。また、ノート追加後に’/’をpushしています。一般に’/’はホーム画面を表すのに用いられます。

これでノート追加画面が完成したので、アプリにこの画面を追加します。

画面とルーティング

先ほど「svelte-spa-routerではURLのハッシュを書き換えることで現在の画面を管理している」と言いました。ハッシュの書き換えはpushで実現できました。次はハッシュによる画面の切り替えです。

画面の切り替え管理のことを、「ルーティング」と言います。svelte-spa-routerにおいてはルーティングは「どのハッシュにどのSvelteコンポーネントを割り当てるか」の対応表に基づき行われます。

ルーティングを行うコンポーネントを「ルータ」と言います。ルータはsvelte-spa-routerに同梱されています。

ルーティングは主に一番中心となるコンポーネント、今回で言えばApp.svelteで行われることが多いです。App.svelteを以下のように書き換えましょう:

<script>
  import Router from 'svelte-spa-router'
  import Home from './Home.svelte';
  import Add from './Add.svelte';
	
  const routes = {
    '/': Home,
    '/add': Add,
    '*': Home
  };
</script>

<main>
  <Router routes={routes}></Router>
</main>

<style>
	main {
		background-color: rgb(255, 251, 246);
		padding: 2em;
		width: 100%;
		height: 100%;
		overflow-y: auto;
	}
</style>

Routerと、今まで作った画面(Home, Add)をimportし、ルーティング対応表を作ります。ここではroutesという変数に入れています。

routesにはハッシュとコンポーネントの対応を記述します。ハッシュが「/」のときはHome、ハッシュが「/add」のときはAdd、その他の場合はHomeを指定しています。

そしてRouterを設置して先ほどのroutesを渡してやれば、あとはRouterが勝手に制御してくれるようになります。Routerを置いている部分がハッシュ値に依存してHomeやAddに切り替わるようになります。

保存したノートの読み込み

ここまでのコードを動かす前に、Homeをちょっと編集します。保存したノートを読み込めるようにします。

いままでダミーデータを使っていたので、そこを削除してlib/storage.jsから読み込みに行くようにします。script部分にloadNotesのimportと、loadNotesを使用して読み込みする処理を追加するだけです。

<script>
  import { push } from 'svelte-spa-router';
  import NoteList from './components/NoteList.svelte';
  import { loadNotes } from './lib/storage';

  const userNotes = loadNotes();
</script>

<div class="home">
  <h1 class="app-title">Svelte Note</h1>
  <NoteList notes={userNotes}></NoteList>
  <button class="add" on:click={() => push('/add')}>+新しいノート</button>
</div>

<style>
	.app-title {
		margin-bottom: 1em;
	}

	.add {
		display: block;
		background-color: rgb(75, 168, 51);
		border: none;
		box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
		color: white;
		font-size: 1.5em;
		width: 100%;
		padding: 0.5em 0;
		margin: 1em 0;
		cursor: pointer;
	}
</style>

これで読み込みもできました。

ここまでを動かしてみる

ここまでできたらいったん動かしてみましょう。npm run devで動かせます。

前とは違い、ノートは何も表示されず、新しいノートボタンだけが表示されていると思います。新しいノートボタンをクリックするとハッシュが変更され、画面が切り替わります。

内容を適当に記述し保存ボタンを押すと、ホーム画面に戻りノートが表示されるはずです。

パラメータ付きルーティング

ハッシュパラメータ

そして残るはノート編集画面だけです!さて、ここで問題が起こります。編集画面では`/edit/${index}`をハッシュとして使用していましたが、つまりindexの数だけルーティングを増やさないといけないのでしょうか?例えば以下のように……:

  • / -> Home
  • /add -> Add
  • /edit/0 -> Edit
  • /edit/1 -> Edit
  • /edit/2 -> Edit
  • 以下略

実はルーティングにおいてはパラメータが使用できます。簡単にいうと変数のようなものです。パラメータを使うことで、似たようなハッシュをすべて同じページに飛ばすことができます。

  • / -> Home
  • /add -> Add
  • /edit/:id -> Edit

ここで「:id」がパラメータになります。パラメータには「:」で始まる自由な名前をつけることができます。

パラメータの値はコンポーネント側から取得可能ですので、これを用いれば編集画面も作れそうです。

ノート編集画面を作る

編集画面を作ります。編集画面は新規ノート画面とほぼ同じです。srcフォルダに「Edit.svelte」を作ります:

<script>
  import { push } from 'svelte-spa-router';
  import NoteEditor from './components/NoteEditor.svelte';
  import { loadNotes, overwriteNote } from './lib/storage';

  export let params = {};

  const note = loadNotes()[params.id];

  let title = note.title;
  let content = note.content;

  const onSave = () =>  {
    overwriteNote(params.id, {title, content});
    push('/');
  };
</script>

<div class="add">
  <NoteEditor bind:title={title} bind:content={content}></NoteEditor>
  <div class="button-container">
    <button class="save" on:click={onSave} disabled={!title || !content}>保存</button>
  </div>
</div>

<style>
  .add {
    display: flex;
    flex-direction: column;
    height: 100%;
  }

  .button-container {
    padding: 1em 0;
    text-align: right;
  }

  .save {
    background-color: rgb(62, 68, 163);
    border: none;
    border-radius: 3px;
    color: white;
    font-size: 1em;
    padding: 0.5em 1em;
    cursor: pointer;
  }

  .save:disabled {
    opacity: 0.3;
    cursor: auto;
  }
</style>

違いはonSave時の処理と、デフォルト値としてlib/storage.jsからノートを読み込んでいるところです。paramsという変数をexportすることで、ハッシュのパラメータ値を受け取れるようになります(※ハッシュパラメータを受け取るには必ずparamsという変数名でなければなりません)。

paramsの中にパラメータの値が入るので、それを取り出して処理します。ここではパラメータに後ほど「id」という名前をつけるので、params.idで取り出せます。

パラメータありルーティング

`/edit/${index}`をpushしたときに編集画面(Edit)に遷移するようにします。ハッシュでパラメータを使うには、「:」で始まる任意の名前をつけます。ここでは「/edit/:id」とします。

App.svelteを編集します。

<script>
  import Router from 'svelte-spa-router'
  import Home from './Home.svelte';
  import Add from './Add.svelte';
  import Edit from './Edit.svelte';
	
  const routes = {
	  '/': Home,
	  '/add': Add,
	  '/edit/:id': Edit,
	  '*': Home
  };
</script>

<main>
  <Router routes={routes}></Router>
</main>

<style>
	main {
		background-color: rgb(255, 251, 246);
		padding: 2em;
		width: 100%;
		height: 100%;
		overflow-y: auto;
	}
</style>

これでハッシュが「/edit/0」のときは:idが0、「/edit/1」のときは:idが1となります。この値が先ほどのEdit.svelteのparams.idに入ります。

ここまでを動かしてみる

ではここまでを動かしてみましょう。新しいノートボタンから適当なノートを追加し、追加されたノートをクリックします。

ハッシュが書き変わり、編集画面へ遷移するはずです。そのまま内容を編集して保存ボタンを押せば、内容が反映されます。

ビルドする

ここまでできたので、ビルドします。

npm run build

これでpublicフォルダにビルドされたファイルが出力されます。

完成!

これでひとまず完成です!ノート一覧の表示、ノートの追加・編集ができました。ノートアプリの制作を題材に、SvelteによるSPA開発を学ぶことができました。

Svelteは簡単に習得できる一方、ReactやVueほど強力なものではありません。ですが、単純なSPAを制作するには十分な力を持っています。

もちろん難しい技術を習得することも大事です。ですが、まずはSvelteから始めてみるのもどうでしょう?気楽で、頑張らない、そんなフロントエンド生活があってもいいのではないでしょうか。

ここで習得した技術で、いろいろ作ってみてください。きっと楽しいはずですから。