Laravel 5.4 + Vue 2.1 でSPAアプリケーション作成チュートリアル

何となく `Laravel` ＋ `Vue.js` の使い方が分かってきた。
自分の頭の整理を兼ねて、書き残しておく。

※ 追記
2019/02/26 現在では Laravel も Vue も若干古いバージョンですが、基本的な考え方は同じなので、参考になれば幸いです。

# 処理の流れ

- ブラウザ → Laravel:  SPAのViewをリクエスト（初回のみ）
- ブラウザ:  Vue を初期化、View （仮想DOM） を描画
- Vue → Laravel:  XHRでデータをリクエスト
- Vue → ブラウザ:  クライアントサイドでのルーティング

## Laravel の役割

- Webサーバー。起点となるSPAのViewを初回だけ返す。
- APIサーバー。サーバーサイドでデータを処理し、JSONを返す。

## Vue の役割

- Viewの描画。変数のバインド、繰り返し、イベント検知など。
- Router。 `vue-router` を使って、URLに対応する Vue Component を設定する。
- サーバーサイドにリクエスト。ここでは `axios` を使う。

Laravel には上記を実現する機構が Out of Box で整っている。


# 完成イメージ

ただ記事を読むだけの、ブログのようなものを作っていく。投稿は無い。

SPA なので、コンポーネント志向でモジュール分割することを前提にする。

今後、 `JWTAuth` を使ったログイン、記事作成、コメント投稿、なども追加していきたい。

![Screenshot_2017-03-07_11-06-41.png](https://qiita-image-store.s3.amazonaws.com/0/103885/23895970-d856-5080-19e5-28adc5a9855f.png)

（あくまでイメージです、このチュートリアルではCSSやSeedを入れてないためこうはなりません）

ちなみにログインが必要な時は、JWTというトークンベースのステートレスな認証を使うと良さげ。本記事は単純化を目指しているので、下記の記事で解説した。

https://qiita.com/acro5piano/items/eb29f13b82f386220460

# アプリ作成

この辺はいつも通り

```bash
composer create-project --prefer-dist laravel/laravel blog
cd blog

# 単純化のため、データベースドライバを mysql から sqlite に変える
perl -i -pe 's/DB_.+\n//g' .env
echo 'DB_CONNECTION=sqlite' >> .env
touch database/database.sqlite

php artisan serve
```

# サーバーサイド (Laravel)

## Model

記事を取ってくる `Article` モデルを作成する

```php:database/migrations/2017_03_05_064019_create_articles_table.php
<?php

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

class CreateArticlesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id')->references('id')->on('users')->unsigned()->index();
            $table->text('content');
            $table->string('title');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('articles');
    }
}
```

```php:app/Models/Article.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
}
```


```
php artisan migrate
```

## Routing

### Web

```php:routes/web.php
<?php

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

ブラウザの Routing は Vue が担当するので、不要。
Laravel `/api/*` 以外の全てのリクエストに対して、 `resources/views/app.blade.php` を返す。

`/` だけでいいじゃんと思うかもしれないが、それだと `/` 以外で画面を更新すると Laravel 側のルーティングエラーになってしまう。

よって全てのルーティングで設定しておかないといけない。

参考: https://laracasts.com/discuss/channels/vue/using-vue-router-with-laravel

### API

```php:routes/api.php
<?php

use Illuminate\Http\Request;

Route::group(['middleware' => 'api'], function() {
    Route::get('articles',  function() {
        $articles = Article::all()->take(5);
        return $articles;
    });
});
```

`GET /api/articles` で、記事5件をJsonで返す。

Controller は簡略化のために今は使わない。
アプリが成熟してきたら、Controller を作成して

```php:routes/api.php
Route::group(['middleware' => 'api'], function() {
    Route::get('articles',  'ArticleController@index');
});
```

などとしても良いだろう。

# フロントエンド

`vue-router` を使う。クライアントサイドでルーティングするためのVue.js公式ライブラリ。

```diff:package.json
       "lodash": "^4.17.4",
       "vue": "^2.1.10",
+++    "vue-router": "^2.3.0"
  }
}
```

```
npm install
```

## Webpack を起動

```
npm run watch
```

`npm run watch` で `Webpack` が起動し、アプリケーションの変更を検知してフロントエンドをビルドする。
もしここでエラーになったら、 `package.json` の `scripts` 以下のパスを適当に調整する。

## axios を使う

`エイクシオス` って発音するらしい。jQueryの `$.ajax` の代替という認識

```js:resources/assets/js/bootstrap.js
// ...

window.axios = require('axios')

window.axios.defaults.headers.common = {
    'X-CSRF-TOKEN': window.Laravel.csrfToken,
    'X-Requested-With': 'XMLHttpRequest'
}

Vue.prototype.$http = window.axios

// ...
```

こうしておくと、 Vue Component から `this.$http.get(....` みたいに使える。

（追記）
@foursue さんから、

> ESLintがエラーを検出してしまうので、こちらのほうが良いのではないでしょうか？
> `window.Vue.prototype.$http = window.axios`

という編集リクエストを頂きました。ありがとうございます。

これについてちょっと調べたのですが、公式アナウンスで

> Vue.prototype.$http = axios と設定すればよいです。

とあります。

- https://jp.vuejs.org/2016/11/03/retiring-vue-resource/

また、GitHubでもこのコードはちらほら見かけます。例）

- https://github.com/pespantelis/vue-typeahead/blob/9d22a63b28569c2f97543e9ddabcde432d1b9926/demo/main.js

`window.Vue.prototype.$http ...` の書き方を採用すると、 `Vue.use(...)` なども `window.Vue.use(...)` としなくてはならないので、不自然なのかなと思います。

そもそも `window` はグローバルオブジェクトで、ブラウザ側からも参照したい値を格納するのに使うものかと思います。

例： `window.Laravel.csrfToken = {{ csrf_token() }}`

なので、このままにしておきます。

ESLint側の設定を変更するか、 `window` オブジェクトを使っていないファイルに記述するべきかと思います。

ちゃんと `window` が何だか知る良い機会になりました。重ねてお礼申し上げます。

## メインのJSファイル

```js:resources/assets/js/app.js
import Vue from 'vue'
import VueRouter from 'vue-router'

require('./bootstrap')

Vue.use(VueRouter)

Vue.component('navbar', require('./components/Layouts/Navbar.vue'))

const router = new VueRouter({
    mode: 'history',
    routes: [
        { path: '/', component: require('./components/Articles/Index.vue') },
        { path: '/about', component: require('./components/About.vue') },
    ]
})

const app = new Vue({
    router,
    el: '#app'
})
```

ここで `vue-router` を定義しているので、 HTMLに書かれた `<router-view></router-view>` が下記のように動的に変わる。

- `/` -> `Articles/Index.vue` を表示
- `/about` -> `About.vue` を表示

記事詳細画面を作るならば

```js
{ path: '/articles/:id', component: require('./components/Articles/Show.vue') },
```

などというルーティングを設定しても良いだろう。

## View(HTML)

```php:resouces/views/app.blade.php
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Laravel</title>

        <link rel="stylesheet" href="{{ mix('css/app.css') }}"></script>

        <script>
            window.Laravel = {
                csrfToken: "{{ csrf_token() }}"
            };
        </script>
    </head>
    <body>
        <div id="app">
            <navbar></navbar>
            <div class="container">
                <router-view></router-view>
            </div>
        </div>
    </body>
    <script src="{{ mix('js/app.js') }}"></script>
</html>
```

- Laravel はこのファイルを返すだけ、つまりどのURLに対してもこの `app.blade.php` を返す
- `mix` は、 laravel-mix が生成するマニフェストファイルに従ったファイルを返す

## Vue Component

まずは静的なコンポーネント


```vue:resources/assets/js/components/Layouts/Navbar.vue
<template>
    <nav class="navbar navbar-default">
        <li>
            <router-link  to="/about">About</router-link>
        </li>
    </nav>
</template>
```

```vue:resources/assets/js/components/About.vue
<template>
    <div>
        This page describe who we are.
    </div>
</template>
```

いよいよ Vue が Laravel に対してリクエストを送る。
正確には、

- Vue が axios を使って Laravel にリクエストを送る
- その結果をコンポーネントのデータに入れる
- View が更新される


```vue:resources/assets/js/components/Articles/Index.vue
<template>
    <div v-for="article in articles">
        <h1>
            <router-link :to="'/articles/' + article.id">{{ article.title }}</router-link>
        </h1>
        <p>
            {{ article.content }}
        </p>
    </div>
</template>

<script>
    export default {
        created() {
            this.fetchArticles()
        },
        data() {
            return {
                articles: []
            }
        },
        methods: {
            fetchArticles() {
                this.$http.get('/api/articles')
                .then(res =>  {
                    this.articles = res.data
                })
            }
        }
    }
</script>
```

`created()` で、 Component が読み込まれた際に行う処理を書ける。
`data()` は、Viewに渡す変数を定義する。

TODO:

`router-link` のところは、もっとエレガントに書けるはず。
`this.$router.push` とかすれば良いのだろうが、長いので今はこのままにする。

c.f.) http://router.vuejs.org/en/essentials/navigation.html

TODO: `created` と `mounted` との違いは何だろう？

↓ 追記

`created` は、SPAアプリが最初に読み込まれたタイミングで一度だけ実行される。
`mounted` は、 `vue-router` によってコンポーネントがセットされる度に更新される。

・・・だと思う。本当に最初だけしかやらなくて良い処理は `created` に書くのが吉。

# 所感

以上は走り書きなので、抜け漏れなどあれば指摘お願いしますm(_ _)m

Laravel いいなと思ったのが、最初から 

- `Vue`, `axios` がインストール済み
- `Webpack` によるビルドが設定済み
- `routes/api.php`, `routes/web.php` のルーティング切り分け

となっているところです。

JSの管理を自前で行わずに、NodeJSに任せているところが潔くて好きです。

この辺の設計思想は、 `webpacker.gem` を導入した `Rails` とは真逆だなと思いました。

c.f. Rails5.1から導入されるwebpacker.gemは本当にRailsのフロントエンド開発に福音をもたらすのか?
http://qiita.com/yuroyoro/items/a29e39989f4469ef5e41

フルスタックにこだわり、Railに載せて開発させるRails。

自身はサーバーサイドに徹し、JSに多くを任せるLaravel。

どちらが今後の主流になっていくのか、みものですね。
もちろんこの2つ以外も。