LaravelにおいてAPIを作成し、フロント側はVue.jsのVue Routerを利用してSPA(Single Page Application)を実装してみます。
開発環境
Laravel Framework 7.2.0
作成するアプリケーションの概要
本記事では下記イラストのようなアプリケーションを作成します。なおバリデーションに関してはAPI側で必要最低限の処理しかおこなっていません。
下記関連ページでは、Vue Routerを利用せずに一つのURL(一つのコンポーネント)においてCRUD機能を実装しています。
Laravel 6 APIとVue.jsでSPA構築 最速入門
API側の実装
「http://localhost:8000/api/books/〜」のエンドポイントを持つAPIを作成します。
モデルとマイグレーションファイルを作成します。
1 |
$ php artisan make:model Book --migration |
以上でbooksテーブルに対応したモデル(app/Book.php)とマイグレーションファイルが生成されるので、それぞれ下記のように追記します(ハイライト部分)。
app/Book.php
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Book extends Model { protected $fillable = [ 'title', 'author', ]; } |
database/migrations/2020_03_21_020219_create_books_table.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateBooksTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('books', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('title'); $table->string('author'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('books'); } } |
マイグレーションを実行します。
1 |
$ php artisan migrate |
–apiを付けてコントローラーファイルを作成します。
1 |
$ php artisan make:controller BookController --api |
api.phpに下記コードを追記します。/api/books/〜というエンドポイントになります。
routes/api.php
1 |
Route::apiResource('/books', 'BookController'); |
設定されているルート情報を確認します。
1 2 3 4 5 6 7 8 9 10 11 12 |
$ php artisan route:list +--------+-----------+------------------+---------------+---------------------------------------------+--------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+-----------+------------------+---------------+---------------------------------------------+--------------+ | | GET|HEAD | / | | Closure | web | | | GET|HEAD | api/books | books.index | App\Http\Controllers\BookController@index | api | | | POST | api/books | books.store | App\Http\Controllers\BookController@store | api | | | GET|HEAD | api/books/{book} | books.show | App\Http\Controllers\BookController@show | api | | | PUT|PATCH | api/books/{book} | books.update | App\Http\Controllers\BookController@update | api | | | DELETE | api/books/{book} | books.destroy | App\Http\Controllers\BookController@destroy | api | | | GET|HEAD | api/user | | Closure | api,auth:api | +--------+-----------+------------------+---------------+---------------------------------------------+--------------+ |
作成したコントローラーファイルにすでに用意されている index() / store() / show()/ update() / destroy() のメソッドを利用します。下記のように追記および編集します(ハイライト部分)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Book; use App\Http\Requests\StoreBook; class BookController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { return Book::all(); } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(StoreBook $request) { Book::create($request->all()); } /** * Display the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function show($id) { return Book::find($id); } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param int $id * @return \Illuminate\Http\Response */ public function update(StoreBook $request, $id) { $update = [ 'title' => $request->title, 'author' => $request->author ]; Book::where('id', $id)->update($update); } /** * Remove the specified resource from storage. * * @param int $id * @return \Illuminate\Http\Response */ public function destroy($id) { Book::where('id', $id)->delete(); } } |
バリデーションのためのフォームリクエストクラスを作成し、生成されたファイルを下記のように編集します(ハイライト部分)。
1 |
$ php artisan make:request StoreBook |
app/Http/Requests/StoreBook.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class StoreBook extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'title' => 'required|max:10', 'author' => 'required|max:10', ]; } } |
以上でAPI側の実装は終わりです。
Vue.jsのVue RouterによるSPAの実装
Laravel 6~でVue.jsをはじめて利用する方は下記ページもご覧下さい。
Laravel 6 Vue.jsを利用する [axios][Laravel Mix]
まずlaravel/uiパッケージをインストールします。
1 |
$ composer require laravel/ui |
Vue.jsで構築するためのファイル等を生成します。
1 |
$ php artisan ui vue |
package.jsonの内容に依存したパッケージおよびvue-routerをインストールします。
1 |
$ npm install && npm install vue-router |
Vueコンポーネント(後述)関連のファイルを編集/保存した際には「npm run dev」で再度コンパイルする必要がありますが、watchでそれら編集/保存を監視しコンパイルを自動化することができます。
1 |
$ npm run watch |
サーバーを起動させます。
1 |
$ php artisan serve |
まずSPAの土台としてのビュー(テンプレート)を作成します。index.blade.phpをを作成し下記のように記述します。
resources/views/index.blade.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!doctype html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="UTF-8"> <title>SPAサンプル</title> <meta name="csrf-token" content="{{ csrf_token() }}"> <script src="{{ asset('js/app.js') }}" defer></script> </head> <body> <div id="app"> <router-view /> </div> </body> </html> |
13行目
Vue Routerによるコンポーネントが表示されます。
routes/web.phpに下記を記述します。
1 2 3 |
Route::get('{any}', function () { return view('index'); })->where('any', '.*'); |
2行目のindexは上記で作成したindex.blade.phpを設定しています。どのURLにアクセスしてもこのindex.blade.phpが土台となり、各URLに対応したVue.jsのコンポーネントが表示されるようになります。
resources/js/app.jsを下記のように編集します。コメントは除外してあります。
resources/js/app.js
1 2 3 4 5 6 7 8 9 10 11 |
require("./bootstrap"); window.Vue = require("vue"); import Vue from "vue"; import router from "./router"; const app = new Vue({ el: "#app", router: router }); |
router.jsファイルをresources/jsディレクトリ以下に作成し、下記のように編集します。
resources/js/router.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import Vue from "vue"; import VueRouter from "vue-router"; Vue.use(VueRouter); import index from "./components/index.vue"; import add from "./components/add.vue"; import edit from "./components/edit.vue"; const router = new VueRouter({ mode: "history", routes: [ { path: "/books", name: "index", component: index }, { path: "/books/add", name: "add", component: add }, { path: "/books/:id/edit", name: "edit", component: edit } ] }); export default router; |
24行目
: でパターンを記述しています。コンポーネント側では $route.params.id によって受け取ることができます。
webpack.mix.jsに下記コードを追記します。
webpack.mix.js
1 2 3 |
mix.js("resources/js/app.js", "public/js") .js("resources/js/router.js", "public/js") .sass("resources/sass/app.scss", "public/css"); |
最後にVue.jsの各コンポーネント(index/add/edit)を作成します。
resources/js/components/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<template> <div> <router-link :to="{name:'add'}">新規追加</router-link> <ul> <li v-for="book in books" :key="book.id"> {{ book.id }}/{{ book.title }}/{{ book.author }} <router-link :to="{name: 'edit', params: { id: book.id }}">編集</router-link> <button @click="deleteBook(book.id)">削除</button> </li> </ul> <p v-if="message">{{ message }}</p> </div> </template> <script> export default { data() { return { message: "", books: {} }; }, created: function() { this.getBook(); }, methods: { getBook() { axios .get("/api/books/") .then(response => { this.books = response.data; }) .catch(error => { this.message = error; }); }, deleteBook(id) { axios .delete("/api/books/" + id) .then(response => { this.getBook(); this.message = ""; }) .catch(error => { this.message = error; }); } } }; </script> |
resources/js/components/add.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
<template> <div> <router-link :to="{name:'index'}">トップ</router-link> <form @submit.prevent="addBook"> <div> <label>タイトル</label> <input type="text" v-model="title" /> </div> <div> <label>著者</label> <input type="text" v-model="author" /> </div> <button>追加</button> </form> <p v-if="message">{{ message }}</p> </div> </template> <script> export default { data() { return { message: "", books: {}, title: "", author: "" }; }, methods: { addBook() { axios .post("/api/books/", { title: this.title, author: this.author }) .then(response => { this.$router.push({ name: "index" }); }) .catch(erorr => { this.message = erorr; }); } } }; </script> |
resources/js/components/edit.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
<template> <div> <p>idが{{ book.id }}の編集フォーム</p> <form @submit.prevent="updateBook"> <div> <label>タイトル</label> <input type="text" v-model="book.title" /> </div> <div> <label>著者</label> <input type="text" v-model="book.author" /> </div> <button>編集する</button> <button @click="updateCancel">キャンセル</button> </form> <p v-if="message">{{ message }}</p> </div> </template> <script> export default { data() { return { message: "", book: {} }; }, created() { axios .get("/api/books/" + this.$route.params.id) .then(response => (this.book = response.data)) .catch(erorr => console.log(error)); }, methods: { updateCancel() { this.$router.push({ name: "index" }); }, updateBook() { axios .put("/api/books/" + this.book.id, { title: this.book.title, author: this.book.author }) .then(response => { this.$router.push({ name: "index" }); }) .catch(erorr => { this.message = erorr; }); } } }; </script> |