Laravel5.7でVueを利用した開発をしてみる


## やりたいこと

* Laravel + vue + axiosの連携
* Laravel mixを利用したワークフローの確認

[React版](https://qiita.com/zaburo/private/fbcdd73d8d707357c25f)もいちおう書きました。

## やること

サンプルとして下記のようなTodo管理システムを作ります。

![スクリーンショット 2018-10-07 13.57.19.png](https://qiita-image-store.s3.amazonaws.com/0/55188/1e15c212-2929-d582-388b-c0d37f6a14be.png)

[こちらの記事](https://tonyo.design/program/vue-js/todo-list_with_laravel_vue_axios_no1/)が特に参考になりました。ありがとうございます。

## 前提知識

vueの開発環境はwebpackやCLIを利用して行うことが普通ですが、LaravelにはLaravel Mixというwebpackを簡単に取り扱うためのラッパーがあります。動作は、webpack.mix.jsに下記のように定義されています。

```js
mix.js('resources/js/app.js', 'public/js')
   .sass('resources/sass/app.scss', 'public/css');
```

resources/js/app.jsをいじってnpm run devを実行するとコンパイルしてpublic/js（これをViweから指定して利用）に出力してくれます（cssもしかり）。

## 環境の準備

### 環境

いちおうnodeだけ。

```bash
node -v
v8.12.0
```

### Laravelのインストールと.env

これは割愛。composer create-projectでインストール。MySQL用に.envを編集。

### node関連ライブラリのインストール

必要なものはpackage.jsonに書かれているので、npm installでインストールするだけ。

```bash
npm install
```

## データの準備

todoを管理するテーブル、モデル。デモデータを用意しておく。

### テーブルの作成

migrationファイル作成します。

```bash
php artisan make:migration create_todos_table
```

今回はtitleカラムだけ追加。

```diff
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTodosTable extends Migration
{
    public function up()
    {
        Schema::create('todos', function (Blueprint $table) {
            $table->increments('id');
+           $table->string('title');
            $table->timestamps();
        });
    }
    
    public function down()
    {
        Schema::dropIfExists('todos');
    }
}

```

マイグレーション実行。

```bash
php artisan migrate
```

### モデルの作成

artisanコマンドで。生成されたファイルは編集しません。

```bash
php artisan make:model Todo
```

### テストデータの挿入

無くてもいいですが、表示確認のために少し入れておきます。

```bash
php artisan tinker
Psy Shell v0.9.8 (PHP 7.2.8 — cli) by Justin Hileman
>>>
>>> $title = new App\Todo;
=> App\Todo {#2898}
>>> $title->title = '電話をかける';
=> "電話をかける"
>>> $title->save();
=> true
>>> $tile = new App\Todo;
=> App\Todo {#2904}
>>> $title->title = 'メールをする'
=> "メールをする"
>>> $title->save();
=> true
```

## すべてのリクエストをwelcome.blade.phpに飛ばす

SPAなでページ遷移は必要ない。
ルーティングはVue側で行うので全てのWebリクエスト（API除く）を１つのページ（ここではwelcome）に集約します。

>なお、今回はvue-routerとかは利用しません。

```php:web.php
Route::get('/{any}', function(){
    return view('welcom');
})->where('any','.*');
```

## 集約先のView（welcome.blade.php）

既に存在しているwelcome.blade.phpをひとまず下記のように編集します。Bootstrap4のサンプルをベースに最低限のマークアップをしています。

ディレクティブの埋め込みは後で行うとして、ひとまずLaravelの動作に最低限必要な設定をしています。

やってることは下記の３つ

* \<meta name="csrf-token" content="{{ csrf_token() }}">
* \<link rel="stylesheet" href="{{ asset('css/app.css') }}">
* \<script src="{{ asset('js/app.js')}}"></script>

csrf対策のコードの埋め込み。laravel mixがコンパイルする先のファイルの指定(CSSとJS)。

```diff:welcom.blade.php
<!doctype html>
<html lang="ja">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
+   <meta name="csrf-token" content="{{ csrf_token() }}">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- CSS -->
+   <link rel="stylesheet" href="{{ asset('css/app.css') }}">

    <title>vue test</title>
</head>

<body>
    <div id="app">
        <div class="container">
            <h3 class="mt-5">Todo 管理システム</h3>

                <div class="form-group mt-4">
                    <label for="todo">新規Todo</label>
                    <input type="text" class="form-control" id="todo">
                </div>

                <button type="submit" class="btn btn-primary">登録</button>

            <table class="table mt-5">
                <thead>
                    <th>ID</th><th>タスク</th><th>完了</th>
                </thead>
                <tbody>
                    <tr>
                        <td>1</td>
                        <td>タスクがでる</td>
                        <td><button class="btn btn-secondary">完了</button></td>
                    </tr>
                    <tr>
                        <td>2</td>
                        <td>タスクがでる</td>
                        <td><button class="btn btn-secondary">完了</button></td></tr>
                </tbody>
            </table>
        </div>
    </div>

    <!-- avaScript -->
+<script src="{{ asset('js/app.js')}}"></script>
</body>
</html>
```

## タスク一覧機能

では、始めにタスクの一覧機能を実装します。
上記ではスタティックに記述していましたが、そこをVue.jsを利用して動的に動作するようにします。

### ルートの追加

api.phpに一覧表示用のルートを定義します。

```php:api.php
Route::group(['middleware' => 'api'], function(){
    Route::get('get', 'TodoController@getTodos');
});
```
### コントローラー

ルートで指定したControllerを生成します。

```bash
php artisan make:controller TodoController
```

ルートで指定した機能を実装します。配列を戻せばそのままJSONにしてくれるので手抜き。

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Todo;

class TodoController extends Controller
{
    public function getTodos()
    {
        $todos = Todo::all();
        return $todos;
    }
}
```

### app.jsをいじる

タスク一覧を格納するtodos配列を定義し、/api/getにリクエストした結果を格納しています。
ページを閲覧した際に実行したいので、created()で呼び出ししています。

```js:resources/js/app.js
const app = new Vue({
    el: '#app',
    data: {
        todos: []
    },
    methods: {
        fetchTodos: function(){
            axios.get('/api/get').then((res)=>{
                this.todos = res.data
            });
        }
    },
    created(){
        this.fetchTodos();
    }
});
```

### vディレクティブの追加

取得したtodosをv-forで表示します。埋め込み位置は\<tr></tr>のところです。

```diff:welcome.blade.php
<!doctype html>
<html lang="ja">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- CSS -->
    <link rel="stylesheet" href="{{ asset('css/app.css') }}">

    <title>vue test</title>
</head>

<body>
    <div id="app">
        <div class="container">
            <h3 class="mt-5">Todo 管理システム</h3>

                <div class="form-group mt-4">
                    <label for="todo">新規Todo</label>
                    <input type="text" class="form-control" id="todo" placeholder="タスクを入力してください">
                </div>

                <button type="submit" class="btn btn-primary">登録</button>

            <table class="table mt-5">
                <thead>
                    <th>ID</th><th>タスク</th><th>完了</th>
                </thead>
                <tbody>
+                   <tr v-for="todo in todos" v-bind:key="todo.id">
+                       <td>@{{todo.id}}</td>
+                       <td>@{{todo.title}}</td>
+                       <td><button class="btn btn-secondary">完了</button></td>
+                   </tr>
                </tbody>
            </table>
        </div>
    </div>

    <!-- avaScript -->
<script src="{{ asset('js/app.js')}}"></script>
</body>
</html>
```

>v-bind:keyはv-forでは入れること推奨。

### コンパイルと確認

npm run devすることで裏でコンパイルが走り、js, cssを出力してれます。

```bash
npm run dev
php artisan serve
```


## タスクの追加機能

続いてタスクを追加する機能を実装します。

### ルート

まずはルートの追加から。

```diff:api.php
Route::group(['middleware' => 'api'], function(){
    Route::get('get', 'TodoController@getTodos');
+   Route::post('add', 'TodoController@addTodo');
});
```

### コントローラー

続いてコントローラー。追加して、追加した結果を一覧のときと同じ感じで返すだけ。

```diff:TodoController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Todo;

class TodoController extends Controller
{
    public function getTodos()
    {
        $todos = Todo::all();
        return $todos;
    }

+   public function addTodo(Request $request)
+   {
+       $todo = new Todo;
+       $todo->title = $request->title;
+       $todo->save();

+       $todos = Todo::all();
+       return $todos;
+   }
   
}
```

### js

addTodoを追加します。

```diff:app.js
const app = new Vue({
    el: '#app',
    data: {
        todos: [],
+       new_todo: ''
    },
    methods: {
        fetchTodos: function(){
            axios.get('/api/get').then((res)=>{
                this.todos = res.data;
            });
        },
+       addTodo: function(){
+           axios.post('/api/add',{
+               title: this.new_todo
+           }).then((res)=>{
+               this.todos = res.data;
+               this.new_todo = '';
+           });
+       }
+   },
    created(){
        this.fetchTodos();
    }
});
```

### vディレクティブ追加

inputにv-modelを設定し、ボタンにv-on:clickを追加し、addTodo()をコールします。

```diff:welcome.blade.php
<!doctype html>
<html lang="ja">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- CSS -->
    <link rel="stylesheet" href="{{ asset('css/app.css') }}">

    <title>vue test</title>
</head>

<body>
    <div id="app">
        <div class="container">
            <h3 class="mt-5">Todo 管理システム</h3>

                <div class="form-group mt-4">
                    <label for="todo">新規Todo</label>
+                   <input type="text" class="form-control" id="todo" v-model="new_todo">
                </div>

+               <button type="submit" class="btn btn-primary" v-on:click="addTodo">登録</button>

            <table class="table mt-5">
                <thead>
                    <th>ID</th><th>タスク</th><th>完了</th>
                </thead>
                <tbody>
                    <tr v-for="todo in todos" v-bind:key="todo.id">
                        <td>@{{todo.id}}</td>
                        <td>@{{todo.title}}</td>
                        <td><button class="btn btn-secondary">完了</button></td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>

    <!-- avaScript -->
<script src="{{ asset('js/app.js')}}"></script>
</body>
</html>
```

### コンパイルと確認

```bash
npm run dev
php artisan serve
```

## タスクの削除機能

最後にタスクの完了（削除）機能を追加します。

### ルート

ルートを追加。

```diff:api.php
Route::group(['middleware' => 'api'], function(){
    Route::get('get', 'TodoController@getTodos');
    Route::post('add', 'TodoController@addTodo');
+   Route::post('del', 'TodoController@deleteTodo');
});
```

### コントローラー

続いてコントローラー。

```diff:TodoController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Todo;

class TodoController extends Controller
{
    public function getTodos()
    {
        $todos = Todo::all();
        return $todos;
    }

    public function addTodo(Request $request)
    {
        $todo = new Todo;
        $todo->title = $request->title;
        $todo->save();

        $todos = Todo::all();
        return $todos;
    }

+   public function deleteTodo(Request $request)
+   {
+       $todo = Todo::find($request->id);
+       $todo->delete();

+       $todos = Todo::all();
+       return $todos;
+   }
 
}
```

### js

app.jsにdeleteTodo()を追加。

```diff:app.js
const app = new Vue({
    el: '#app',
    data: {
        todos: [],
        new_todo: ''
    },
    methods: {
        fetchTodos: function(){
            axios.get('/api/get').then((res)=>{
                this.todos = res.data;
            });
        },
        addTodo: function(){
            axios.post('/api/add',{
                title: this.new_todo
            }).then((res)=>{
                this.todos = res.data;
                this.new_todo = '';
            });
        },
+       deleteTodo: function(task_id){
+           axios.post('/api/del',{
+               id: task_id
+           }).then((res)=>{
+               this.todos = res.data;
+           });
+       }
    },
    created(){
        this.fetchTodos();
    }
});
```

### vディレクティブ

完了ボタンにv-on:clickを設定し、deleteTodoをコール。引数としてtodo.idを渡します。

```diff:welcome.blade.php
<!doctype html>
<html lang="ja">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- CSS -->
    <link rel="stylesheet" href="{{ asset('css/app.css') }}">

    <title>vue test</title>
</head>

<body>
    <div id="app">
        <div class="container">
            <h3 class="mt-5">Todo 管理システム</h3>

                <div class="form-group mt-4">
                    <label for="todo">新規Todo</label>
                    <input type="text" class="form-control" id="todo" v-model="new_todo">
                </div>

                <button type="submit" class="btn btn-primary" v-on:click="addTodo">登録</button>

            <table class="table mt-5">
                <thead>
                    <th>ID</th><th>タスク</th><th>完了</th>
                </thead>
                <tbody>
                    <tr v-for="todo in todos" v-bind:key="todo.id">
                        <td>@{{todo.id}}</td>
                        <td>@{{todo.title}}</td>
+                       <td><button class="btn btn-secondary" v-on:click="deleteTodo(todo.id)">完了</button></td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>

    <!-- avaScript -->
<script src="{{ asset('js/app.js')}}"></script>
</body>
</html>
```

### コンパイルと確認

```bash
npm run dev
php artisan serve
```

## 今後やること

* バリデーション
* 各種エラー処理
* ページネーション