
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、以前このブログでは以下のような記事をお届けしました。
過去記事: Laravel + Livewire で CRUD を実装してみる 〜 よりシンプルを求めて 〜
理由としては、Vue.js
が2
→3
のメジャーアップデートで少し複雑になり、最大のメリット「シンプルなのに高機能」を失った感があったため、他のテクノロジーも調査してみたかったからです。(とはいえ、現在も私のメインはVue.js
を使ってますが)
そしてその流れで、今回はTwitter
などでも見かける「Svelte」を試してみることにしました。
Svelte
とは、これまでのJavaScript
とは少し違ったアプローチで、フレームワークというよりは「コンパイラ」という位置づけです。
以下は、Svelte
公式ページの説明を DeepL で翻訳したものです。(DeepL
のクオリティ、すごいですね)
Svelte は、ユーザーインターフェイスを構築するための抜本的な新アプローチです。React や Vue のような従来のフレームワークがブラウザ上で作業の大部分を行うのに対し、Svelte はその作業をアプリのビルド時に発生するコンパイルステップにシフトさせます。
そこで
今回は、Laravel + Svelte
の基本的な使い方をまとめてみることにしました。
ぜひ何かの参考になりましたら嬉しいです。
「いらないモノをあげた途端、
逆に目にはモノモライ…」
開発環境: Laravel 8.x
目次 [非表示]
Laravel のインストールについて
今回はビルド環境もからんでくるため、Laravel
でSvelte
を試す場合は新規インストールすることをおすすめします。
Laravel Mix 用の Svelte エクステンションをインストールする
Laravel Mix
にはSvelte
の拡張が用意されています。
ということで、以下のコマンドでこの拡張をインストールしてください。
npm install wewowweb/laravel-mix-svelte
※ ちなみに、もしLaravel
を新規インストールしている場合はnpm install
も実行してください。
Laravel Mix の設定を変更する
次にLaravel Mix
(ビルド環境)でSvelte
が使えるようにします。
webpack.mix.js
const mix = require('laravel-mix');
require('laravel-mix-svelte');
mix.js('resources/js/app.js', 'public/js')
.svelte();
Svelt のコンポーネントをつくる
ここからがSvelte
の作業になります。
コンポーネント本体をつくる
まずはシンプルに「タイトルです」と表示されるだけのコンポーネントをつくってみます。
resources/js/svelte/components/App.svelte
<div>
<h1>タイトルです</h1>
</div>
ビルドできるように登録する
次に、ファイルを作成しただけではコンポーネントが使えるようにはなりませんので、ビルドできるように登録をします。
resources/js/app.js
require('./bootstrap');
import App from './svelte/components/App.svelte';
new App({
target: document.querySelector('#app')
});
ビルドする
では、この状態でビルドしてみましょう。
以下のコマンドを実行してください。
npm run dev
これで(エラーがなければ)/js/app.js
が作成されることになります。
コンポーネントを使う
では、ビルドされたコンポーネントを使ってみましょう。
まずは、ビューです。
resources/views/home.blade.php
<html>
<body>
<!--
ここに挿入されます -->
<div id="app"></div>
<script src="{{ asset('js/app.js') }}"></script>
</body>
</html>
そして、ルートへ登録します。
routes/web.php
Route::get('/', fn() => view('home'));
では、この状態でブラウザを確認してみましょう。
はい
うまく表示されました。
これが基本的な使い方です
JavaScript & CSS をつかう
では、次にコンポーネントの中でJavaScript
とCSS
を使ってみましょう。といっても、これはSvelte
の良い部分ですが、ほぼHTML
と同じように使うことができます。
resources/js/svelte/components/App.svelte
<script>
let time = '';
setInterval(() => {
time = Date.now();
}, 1000);
</script>
<style>
#title {
background-color: #ff0000;
color: #ffffff;
}
</style>
<div>
<h1 id="title">タイトルです</h1>
<div>{time}</div>
</div>
では、これをビルドしてみましょう。
はい
色がつき、時間が経つごとに内容が変更されるようになりました。
なお、{time}
となっている部分がlet time;
の中身をリアルタイムで表示してくれています。
CRUDを実装する
では続いて、Svelte + Laravel
でCRUD
(追加・表示・変更・削除)を実装してみましょう。
DBまわりを準備する
まずモデル&マイグレーションですが、Livewire
の記事と同じものを使います。詳しくは以下を参考にしてください。
参考ページ: Laravel + Livewire で CRUD を実装してみる 〜 よりシンプルを求めて 〜:DBまわりを用意する
実際のテーブルはこうなります。
コントローラーをつくる
Livewire
と違い、ajax
などのメソッドが必要になるので、コントローラーを用意します。
以下のコマンドを実行してください。
php artisan make:controller ArticleController
するとファイルが作成されるので、中身を次のように変更してください。
app/Http/Controllers/ArticleController.php
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
public function index()
{
return view('article.index');
}
public function list()
{
return Article::select('id', 'title', 'content')->paginate(10);
}
public function store(Request $request)
{
// バリデーションは省略してます
$article = new Article();
$article->fill($request->only(['title', 'content']));
$result = $article->save();
return ['result' => $result];
}
public function update(Article $article, Request $request)
{
// バリデーションは省略してます
$article->fill($request->only(['title', 'content']));
$result = $article->save();
return ['result' => $result];
}
public function destroy(Article $article)
{
// バリデーションは省略してます
$result = $article->delete();
return [ 'result' => $result];
}
}
ビューをつくる
先ほどのコントローラーの中でセットしたビューをつくります。
resources/views/article/index.blade.php
<html>
<head>
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
</head>
<body>
<div id="app" class="p-5"></div>
<script src="{{ asset('js/app.js') }}"></script>
</body>
</html>
ルートをつくる
そして、ルートです。
routes/web.php
use App\Http\Controllers\ArticleController;
Route::get('article', [ArticleController::class, 'index']);
Route::get('article/list', [ArticleController::class, 'list']);
Route::post('article', [ArticleController::class, 'store']);
Route::put('article/{article}', [ArticleController::class, 'update']);
Route::delete('article/{article}', [ArticleController::class, 'destroy']);
Svelte の部分をつくる
では、メインのSvelte
部分です。
今回作成するのは以下3ファイルです。
- App.svelte: ページの基本になる部分。この中から以下2つのコンポーネントを呼び出します
- ArticleList.svelte: 記事データの一覧を表示します。今回はシンプルなページネーションをつけます
- ArticleInput.svelte: 登録・変更するための入力フォームです
では、1つずつご紹介します。
resources/js/svelte/components/App.svelte
<script>
import ArticleList from "./article/ArticleList.svelte";
import ArticleInput from "./article/ArticleInput.svelte";
let params = {
id: '',
title: '',
content: ''
};
let articleList; // コンポーネント内のメソッドを実行するために必要
const onEdit = e => {
params = e.detail.article;
};
</script>
<div>
<h1 class="text-3xl mb-4">
Svelte のCRUDサンプル
</h1>
<div class="grid grid-cols-2 gap-7">
<div>
<ArticleList bind:this={articleList} on:article-edit={onEdit}></ArticleList>
</div>
<div>
<ArticleInput params="{params}" on:article-saved={articleList.getArticles}></ArticleInput>
</div>
</div>
</div>
この中でArticleList
とArticleInput
を呼び出しています。
また、重要なのがbind:this={articleList}
の部分で、これはArticleList.svelte
内にあるメソッドgetArticles()
を呼び出すために必要になります。(Svelte
が初見なのでもっとスマートな方法があるのかも・・・)
resources/js/svelte/components/article/ArticleList.svelte
<script>
import { onMount, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
let articles = [];
let page = 1;
let hasPrevPage = false;
let hasNextPage = false;
export const getArticles = () => {
const url = `/article/list?page=${page}`;
axios.get(url)
.then(response => {
articles = response.data.data;
hasPrevPage = response.data.prev_page_url !== null;
hasNextPage = response.data.next_page_url !== null;
});
};
const onEdit = article => {
dispatch('article-edit', { article: article });
};
const onDelete = article => {
if(confirm('削除します。よろしいですか?')) {
const url = `/article/${article.id}`;
axios.delete(url)
.then(response => {
if(response.data.result === true) {
getArticles();
}
});
}
}
const onMovePage = mode => {
if(mode === 'next') {
page++;
} else if(mode === 'prev') {
page--;
}
getArticles();
}
onMount(() => {
getArticles();
});
</script>
<div>
<div class="pb-4 text-sm">
(ArticleList)
</div>
<table class="w-full text-sm mb-5">
<thead>
<tr>
<th class="border p-2">タイトル</th>
<th class="border p-2">本文</th>
<th class="border p-2">操作</th>
</tr>
</thead>
<tbody>
{#each articles as article}
<tr>
<td class="border px-2 py-1">{article.title}</td>
<td class="border px-2 py-1">{article.content}</td>
<td class="border px-2 py-1 text-right">
<button
type="button"
class="bg-yellow-500 text-yellow-50 rounded p-2 text-xs" on:click={onEdit(article)}>
変更
</button>
<button
type="button"
class="bg-red-600 text-red-50 rounded p-2 text-xs" on:click={onDelete(article)}>
削除
</button>
</td>
</tr>
{/each}
</tbody>
</table>
{#if hasPrevPage === true}
<button
type="button"
class="bg-blue-500 text-blue-50 rounded p-2 text-xs" on:click={() => onMovePage('prev')}>
前へ
</button>
{/if}
{#if hasNextPage === true}
<button
type="button"
class="bg-blue-500 text-blue-50 rounded p-2 text-xs" on:click={() => onMovePage('next')}>
次へ
</button>
{/if}
</div>
この中で重要なのは、「独自イベント」を用意している部分です。
イベントを用意するには、以下のようにイベント機能を呼び出し、
const dispatch = createEventDispatcher();
そして、以下のように実行します。(article-edit
がイベント名、そして第2引数は送信するデータです)
dispatch('article-edit', { article: article });
すると、呼び出し元であるApp.svelte
で以下のようにイベントを受け取ることできるようになります。
ArticleList bind:this={articleList} on:article-edit={onEdit}></ArticleList>
resources/js/svelte/components/article/ArticleInput.svelte
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let params = { // 親コンポーネントからアクセスできるように props 化
id: '',
title: '',
content: '',
};
let resultMessage = '';
let isModeCreate = false;
let isModeEdit = false;
// Vue で言うところの computed
$: {
isModeCreate = (params.id === '');
isModeEdit = (params.id !== '');
}
const onSubmit = () => {
if(confirm('送信します。よろしいですか?')) {
let url = '';
let additionalParams = {};
if(isModeCreate) {
url = '/article';
} else if(isModeEdit) {
url = `/article/${params.id}`;
additionalParams = { _method: 'PUT' };
}
const data = Object.assign({}, params, additionalParams);
axios.post(url, data)
.then(response => {
if(response.data.result === true) {
dispatch('article-saved');
resultMessage = '保存が完了しました!';
setTimeout(() => { // 3 秒後にメッセージをクリア
resultMessage = '';
}, 3000);
params = {
id: '',
title: '',
content: '',
};
}
});
}
}
</script>
<div>
<div class="pb-4 text-sm">
(ArticleInput)
</div>
<div>
{#if resultMessage !== ''}
<div class="text-green-700 p-3 bg-green-300 rounded mb-3">
{resultMessage}
</div>
{/if}
</div>
<div class="mb-3">
<label for="title">タイトル</label>
<br>
<input id="title" type="text" class="border w-full p-1" bind:value="{params.title}">
</div>
<div class="mb-4">
<label for="content">本文</label>
<br>
<textarea id="content" rows="7" class="border w-full p-1" bind:value="{params.content}"></textarea>
</div>
{#if isModeCreate}
<button type="submit" class="bg-purple-700 text-purple-50 p-2 rounded" on:click={onSubmit}>登録する</button>
{:else if isModeEdit}
<button type="submit" class="bg-blue-700 text-blue-50 p-2 rounded" on:click={onSubmit}>変更する</button>
{/if}
</div>
この中で重要なのが、以下のparams
をセットしている部分です。
export let params = {
id: '',
title: '',
content: '',
};
ここにはexport
が使われていますが、これは親コンポーネントからアクセスするためです。
そのため、親コンポーネントであるApp.svelte
では、以下のようにparams
をセットしておけば、どこからでも子コンポーネントであるArticleInput
内のparams
を変更できるようになります。
<ArticleInput params="{params}" on:article-saved={articleList.getArticles}></ArticleInput>
どこからでも
params
にアクセスできます
const onEdit = e => {
params = e.detail.article; // 子コンポーネントの内容が変更になる
};
テストしてみる
では、実際にテストしてみましょう
まずはブラウザで「https://******/article」へアクセスします。
はい
ちょっと画像が小さいですが、ArticleList
とArticleInput
を表示することができました。
では、ページネーションの「次へ」をクリックしてみましょう。
すると・・・・・・
2ページ目の内容が表示されました
では、次にフォームに以下のように入力し、新規登録してみましょう。
どうなったでしょうか・・・・・・??
はい
新しいデータが一覧に表示されました。
では、変更ボタンをクリックしてみます。
すると、空の入力フォームに登録内容がセットされました。
では、内容を以下のように変更して保存してみましょう。
どうなるでしょうか・・・・・・?
はい
内容が変更されました。
では、最後に削除です。
分かりやすいようにページを1ページまで戻して3行目のデータを削除してみましょう。
すると・・・・・・
はい
「タイトル3」のデータが削除されました。
すべて成功です。
企業様へのご提案
今回ご紹介したようにSvelte
はLaravel
と連携して利用することもできます。
もしLaravel + Svelte
の開発をご希望でしたらぜひお問い合わせよりご連絡ください。
お待ちしております。m(_ _)m
ちなみに: 使ってみた感想
ちなみにSvelte
を使ってみた感想をまとめてみました。
完全に「個人の感想です」というやつですが、もし良ければ参考にしてみてください。(今回初見なので、もっとスマートな書き方があるかもですが、その場合はぜひ教えてください。m(_ _)m)
いいなと思ったところ
- 可読性がいい
- $: で一気にリアクティブな変数をつくれるのは良い
- Vue との設計思想が共通する部分があるので移行しやすい
- ネイティブな JavaScript っぽく書けるので、よくあるフレームワークの「くせ」は少ない
- Vue ではよく使う this が不要
- コンパイルが想定してたよりも高速だった
気になったところ
- Laravelとの連携がそこまでシンプルには感じなかった
- Blade のように PHP のパラメータをシンプルに埋め込めない
- イベントにパラメータをつける場合、「() => onTest(‘parameter’)」というように書かないといけない
- ページネーションは自作しないといけない(Livewire だとそのまま使える)
- うーん、でもやっぱり毎回ビルドするのは好きじゃない…
- ページ移動部分は Svelte 側に書くか、Laravel 側の route を使うかの2択ですが、どちらにしても少し工夫が必要(個人的には SPA 的にはしたくないんですよね…)
おわりに
ということで、今回はLaravel + Svelte
の使い方をまとめてみました。
個人的には、「そこまでシンプルではないかな」という結論になりましたので、今回の記事テーマとしては少し残念な結果になってしまいました。
ただ、Laravel
との連携がそれほどシンプルではないだけで、Svelte
自体はシンプルにコードを書くことができると思います。(新しいアプローチも素晴らしいですよね)
なお、この辺りは個人の好みで違うと思うので、ぜひ皆さん自身で試してみてください。
ではでは〜
「久石譲さんがつくった
天外魔境Ⅱの曲、いいですね」