何となく 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 を使ったログイン、記事作成、コメント投稿、なども追加していきたい。
(あくまでイメージです、このチュートリアルではCSSやSeedを入れてないためこうはなりません)
ちなみにログインが必要な時は、JWTというトークンベースのステートレスな認証を使うと良さげ。本記事は単純化を目指しているので、下記の記事で解説した。
アプリ作成
この辺はいつも通り
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
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
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
}
php artisan migrate
Routing
Web
<?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
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 を作成して
Route::group(['middleware' => 'api'], function() {
    Route::get('articles',  'ArticleController@index');
});
などとしても良いだろう。
フロントエンド
vue-router を使う。クライアントサイドでルーティングするためのVue.js公式ライブラリ。
       "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 の代替という認識
// ...
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 と設定すればよいです。
とあります。
また、GitHubでもこのコードはちらほら見かけます。例)
window.Vue.prototype.$http ... の書き方を採用すると、 Vue.use(...) なども window.Vue.use(...) としなくてはならないので、不自然なのかなと思います。
そもそも window はグローバルオブジェクトで、ブラウザ側からも参照したい値を格納するのに使うものかと思います。
例: window.Laravel.csrfToken = {{ csrf_token() }}
なので、このままにしておきます。
ESLint側の設定を変更するか、 window オブジェクトを使っていないファイルに記述するべきかと思います。
ちゃんと window が何だか知る良い機会になりました。重ねてお礼申し上げます。
メインの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を表示
記事詳細画面を作るならば
{ path: '/articles/:id', component: require('./components/Articles/Show.vue') },
などというルーティングを設定しても良いだろう。
View(HTML)
<!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
まずは静的なコンポーネント
<template>
    <nav class="navbar navbar-default">
        <li>
            <router-link  to="/about">About</router-link>
        </li>
    </nav>
</template>
<template>
    <div>
        This page describe who we are.
    </div>
</template>
いよいよ Vue が Laravel に対してリクエストを送る。
正確には、
- Vue が axios を使って Laravel にリクエストを送る
- その結果をコンポーネントのデータに入れる
- View が更新される
<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つ以外も。
