Laravel5.6とVue.jsで簡単なシングルページアプリケーション

Laravel5.6とVue.js（Laravel Mix）をつかって基本的なシングルページアプリケーションをつくってみました。。のでメモ。

# つくるもの

- Webアプリによくある一連の動き「一覧、登録、表示、更新、削除」（CRUD的なやつ）ができるアプリケーション
- ひとつの基本HTMLだけで各画面遷移も実行される、いわゆるシングルページアプリケーション（SPA）として稼働する
- シングルページアプリケーションだけど、URLは画面ごとに振られる
- バリデーションとかエラー処理とか考えずに、ひとまず動くものをつくる

![アートボード 1.png](https://qiita-image-store.s3.amazonaws.com/0/73833/e1eb24fb-5dc9-9e35-82ce-7b0bf9b0ccbc.png)
こんな感じで一連の動きを学習用に作ってみました。


# 環境
- Laravel5.6
- Homestead　(vagrantでLaravel環境が整う便利なやつ)

# サーバサイド

## Laravelインストール

インストールしたいディレクトリを作成して、そこに移動後以下のコマンドを実行

```bash

$ laravel new
```

`laravel new myapp`とかでディレクトリもつくれるんですが、Homesteadだと最初からディレクトリがある場合のほうが多いと思うのでディレクトリ内で `laravel new` をしました。



## 設定

しばらく待つとインストールが完了してます。
インストールディレクトリ直下の`.env` が環境設定ファイルです。
とりあえず、ここのデータベースだけ設定します。

```text:.env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=DB名
DB_USERNAME=homestead
DB_PASSWORD=secret
```

## APIエンドポイント作成

フロントエンドはVue.jsでつくるので、サーバサイドではエンドポイントだけ作る感じです。

- 記事のテーブル名は　**topics**　とする
- 記事のタイトルは　title フィールド
- 記事本文は　content フィールド


| メソッド | URI | 内容　| パラメータ |
|:---------|:-------|:-------|:---------|
| GET | /api/topics | 一覧データの取得
| GET | /api/topics/[id] | 指定したIDのデータを一つだけ取得
| POST | /api/topics | 新規登録 | `title`,`content`
| PUT | /api/topics/[id] | 指定したIDのデータを更新 | `title`,`content`
| DELETE | /api/topic/[id] | 指定したIDのデータを削除


## 必要なファイルの作成

以下のコマンドで、マイグレーションファイル、モデル、コントローラ、APIリソースファイルの骨組みが作成されます。

```bash

$ php artisan make:model Topic -mr
$ php artisan make:resource Topic
```

## マイグレーション

データベーステーブルをPHPから定義するためのファイルです。
ここに構造を記述してコマンド実行すれば、テーブルが出来上がります。
上記のコマンド実行により、`database/migrations/` 内にファイルが作成されているので、テーブル構造を記述します。
今回は単純にtextカラムのみ設定しましたが、いろいろなカラムタイプや修飾子が使用できます。

```PHP:database/migrations/****_**_**_******_create_topics_table.php

public function up()
{
    Schema::create('topics', function (Blueprint $table) {
        $table->increments('id');
        $table->text('title');
        $table->text('content');
        $table->timestamps();
    });
}
```
参考）：
https://readouble.com/laravel/5.6/ja/migrations.html#columns


## APIリソース

APIエンドポイントで返される項目を設定します。
LaravelのモデルであるEloquentを元にして、どの項目がAPIとして返されるかを設定します。
Eloquentのプロパティにない項目を作成したり、少し手を加えたりなどもできます。

```php:app/Http/Resources/Topic.php

public function toArray($request){
    return [
      'id' => $this->id,
      'title' => $this->title,
      'content' => $this->content,
      'date' => $this->created_at->format('Y-m-d H:i:s'),
    ];
}
```
- id,タイトル、内容、作成日付 を返すことにします。
- date項目については、フォーマットを指定しています。Laravelの日付フィールドはCarbonで格納されるので、いろいろといじったりも簡単です。

## コントローラー
上記「必要なファイルの作成」の項目で実行したコマンドにより、コントローラーの骨組みも作成されています。
一通りのアクションの骨組みに内容を記述していきます。

```php:app/Http/Controllers/TopicController.php
//APIリソースを使用
use App\Http\Resources\Topic AS TopicResource;

//~~~ 以下、各メソッドに記述 ~~~~

// 一覧表示
public function index() {
    return TopicResource::collection(Topic::all());
}

// 保存
public function store(Request $request) {
    $topic = new Topic;
    $topic->title = $request->input('title','');
    $topic->content = $request->input('content','');

    $topic->save();
}

// １データ表示
public function show(Topic $topic) {
    return new TopicResource($topic);
}

// 更新
public function update(Request $request, Topic $topic) {
    $topic->title = $request->input('title','');
    $topic->content = $request->input('content','');

    $topic->save();
}

// 削除
public function destroy(Topic $topic) {
    $topic->delete();
}
```

とりあえず必要なメソッドは上記だけです。
最低限必要なことしか書いてないので、当たり前ですが、非常に短いコードで、APIエンドポイントが実装できます。

## ルーティング

コントローラーを書いたらルーティングを記述します。
`routes/api.php`に以下を記述するだけです。

```php:routes/api.php
Route::resource('topics', 'TopicController');
```

`api.php`は、/api 以下にルーティングするためのファイルで、APIなどのルーティングに使います。

これで、コントローラの各メソッドがURIに割り振られます。
上記の「APIエンドポイント作成」のところで書いたエンドポイントができあがりました。


# フロントエンド

## Laravel Mix　インストール
LaravelにはLaravel Mixというフロントエンド開発環境があります。
webpackを使いやすくラッピングした感じのものですが、とても簡単にVue.jsの開発環境が構築できます。
Laravelのインストールディレクトリに`Package.json`があるので`npm install`するだけでVue.jsの開発に必要な環境が整います。

Laravelのインストールディレクトリのトップで

```
$ npm install
```

## Vue Router インストール
続いて、Vue.jsでページ遷移やルーティングを制御するためのライブラリ`vue-router`をインストールします。

```
$ npm install --save-dev vue-router
```

これで最低限必要な環境のインストールは終わりです。



## Laravel側のテンプレート設定

今回は、シングルページアプリケーションとして、ページ遷移等も全部Vue.jsで行います。
Laravel側の仕事としては、基本となるHTMLを出力してCSSやJSを読み込むだけ。となります。

なので、viewのテンプレートは一つだけ。
`resources/views/`ディレクトリに以下を作成します。

```html:resources/views/layouts.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>サンプルApp</title>
    <link href="{{ mix('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <div id="app">
        <navbar></navbar>
        <router-view></router-view>
    </div>

<script src="{{mix('js/app.js')}}"></script>
</body>
</html>
```
そして、ルーティングします。
`routes/web.php`の中身を全部消して以下に置き換えます。
そうすることで、先程作成した`routes/api.php`のAPIエンドポイントのURI以外のすべてアクセスは `layouts.blade.php`に振られます。

```php:routes/web.php
// Route::get('/', function () {
//     return view('welcome');
// });

Route::get('/{any}', function() {
    return view('layouts');
})->where('any', '.*');
```
これで、あとはフロント側を書いていくだけです。

## Vue.jsとVue Routerの基本設定

まずは、インストールした　`Vue Router`の基本設定

今回つくるページのURIは以下となります。

| URI | 内容
|:---|:---
| / | タイトルを一覧表示
| /[id] | 指定したIDの内容を表示
| /create | 新規作成用のフォーム

Laravel Mixの環境構築用JSファイルに必要な内容を記述します。

```js:resources/assets/js/app.js
require('./bootstrap'); //最初からあります。bootstrap関連の読み込み

window.Vue = require('vue'); //Vue.jsの読み込み

import VueRouter from 'vue-router'; // Vue Routerの読み込み
Vue.use(VueRouter); // Vue.jsで、Vue Routerを使うように設定

// vue-routerによるルーティング設定
const router = new VueRouter({
    mode: 'history',
    routes: [
        { path: '/', component: require('./components/list.vue'), name: 'list' }, // ルートでアクセスしたら、List.vueを表示
        { path: '/create', component: require('./components/Form.vue'), name: 'create' }, // createにアクセスしたらForm.vueを表示
        { path: '/:id', component: require('./components/Detail.vue'), name: 'detail' }, // id番号でアクセスしたらDetail.vueを表示
    ]
});

// Vueのコンポーネント
Vue.component('navbar', require('./components/Navbar.vue')); //ページ上部にメニューバーを表示させたいので、Navbar.vueを登録


// Vue.jsの実行
const app = new Vue({
    router
}).$mount('#app');
```

## 各ページ用のファイルを作成

### 一覧表示のVueファイル作成

```vuejs:resources/assets/js/components/List.vue
<template lang="html">
  <div class="container">
    <div class="list-group">
      <router-link v-for="( item, key, index ) in items" :key="key" :to="{ name: 'detail', params: { id: item.id } }" class="list-group-item">
        {{item.title}}
        <button class="btn" @click.stop.prevent="onDelete(item.id, key)">削除</button>
      </router-link>
    </div>
  </div>
</template>

<script>
export default {
  data: function() {
    return {
      items: null
    }
  },
  mounted: function() {
    this.getItems();
  },
  methods: {
    getItems: function() {
      axios.get('/api/topics')
      .then( (res) => {
        this.items = res.data.data;
      });
    },
    onDelete: function(id, key) {
      axios.delete('/api/topics/' + id)
      .then( () => {
        this.$delete(this.items, key);
      })
    }
  }
}
</script>

```

## 詳細表示のVueファイル作成

```vuejs:resources/assets/js/components/Detail.vue
<template lang="html">
    <div>
        <div class="card" v-if="item">
            <div v-if="updated" class="alert alert-primary" role="alert">
                更新しました
            </div>
            <div class="card-body">
                <div v-if="!editFlg">
                    <h1 class="card-title">{{item.title}}</h1>
                    <div class="card-text">{{item.content}}</div>
                </div>
                <form v-else>
                    <div class="form-group">
                        <input type="text" name="title" id="TopicTitle" class="form-control" v-model="item.title">
                    </div>
                    <div class="form-group">
                        <textarea name="content" id="TopicContent" class="form-control" v-model="item.content"></textarea>
                    </div>
                </form>
            </div>
            <div class="card-footer">
                <time>{{item.date}}</time>
                <button class="btn btn-light text-right" v-if="!editFlg" @click="(editFlg = true)">編集</button>
                <button class="btn btn-light text-right" v-else @click="onUpdate">更新</button>
            </div>

        </div>
    </div>
</template>

<script>
export default {
    data: function( ) {
        return {
            item: null,
            editFlg: false,
            updated: false,
        }
    },
    mounted: function() {
        this.getItem();
    },
    methods: {
        getItem: function() {
            axios.get('/api/topics/' + this.$route.params.id)
            .then( ( res ) => {
                this.item = res.data.data;
            });
        },
        onUpdate: function() {
            axios.put('/api/topics/' + this.item.id, {
                title: this.item.title,
                content: this.item.content
            })
            .then( (res) => {
                this.editFlg = false;
                this.updated = true;
                console.log('update')
            });
        }
    }
}
</script>

<style lang="css">
.card-text {
    white-space: pre-wrap;
}
</style>

```

## 新規作成のVueファイル作成

```vuejs:resources/assets/js/components/Form.vue
<template lang="html">
    <div class="container">
        <div v-if="saved" class="alert alert-primary" role="alert">
        保存しました
        </div>
        <form>
            <div class="form-group">
                <label for="TopicTitle">タイトル</label>
                <input type="text" class="form-control" id="TopicTitle" v-model="title">
            </div>
            <div class="form-group">
                <label for="TopicContent">内容</label>
                <textarea class="form-control" id="TopicContent" rows="3" v-model="content"></textarea>
            </div>
            <button type="submit" class="btn btn-primary" @click.prevent="create">登録</button>
        </form>
    </div>
</template>

<script>
export default {
    data: function() {
        return {
            saved: false,
            title: '',
            content: '',
        }
    },
    methods: {
        create : function() {
            axios.post('/api/topics', {
                title: this.title,
                content: this.content,
            })
            .then((res) => {
                this.title = '';
                this.content = '';
                this.saved = true;
                console.log('created');
            });
        }
    }
}
</script>
```

##　メニューのVueファイル作成

```vuejs:resources/assets/js/components/Navbar.vue
<template lang="html">
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <ul class="navbar-nav mr-auto">
            <li class="nav-item"><router-link class="nav-link active" :to="{ name: 'list' }">一覧</router-link></li>
            <li class="nav-item"><router-link class="nav-link active" :to="{ name: 'create' }">新規作成</router-link></li>
        </ul>
    </nav>
</template>
```

## JS、CSSファイルの作成

上記のファイルを一式作成したら、以下のコマンドを実行

```
$ npm run dev
```

これで各Vueファイルがバンドルされて、`public/js/app.js` に書き出されます。
同時に、Laravel Mixに最初からはいっているbootstrapのCSSなども、`public/css/app.css`に書き出されます。

今回は、一括で作成して`npm run` をしましたが、実際の作業をおこなうときなどは


```
$ npm run watch
```

もしくは

```
$ npm run watch-poll
```
を実行することで、ファイル変更を監視して即時にバンドルしてくれるようになります。

以上で、一通り、必要最低限のSPAのウェブアプリが作成されました。

とても簡単に、エンドポイントとフロント側のJSが作成できるので、表から裏までまとめて楽しく開発できます。


# 参考
https://readouble.com/laravel/5.6/ja/
https://jp.vuejs.org/
https://router.vuejs.org/ja/
https://qiita.com/acro5piano/items/f33381fc60408abe9865