Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

何となく LaravelVue.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

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

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

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

アプリ作成

この辺はいつも通り

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 モデルを作成する

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');
    }
}
app/Models/Article.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
}
php artisan migrate

Routing

Web

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

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 を作成して

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

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

フロントエンド

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

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

Webpack を起動

npm run watch

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

axios を使う

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

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 と設定すればよいです。

とあります。

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

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

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

例: window.Laravel.csrfToken = {{ csrf_token() }}

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

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

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

メインの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 を表示

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

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

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

View(HTML)

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

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

resources/assets/js/components/Layouts/Navbar.vue
<template>
    <nav class="navbar navbar-default">
        <li>
            <router-link  to="/about">About</router-link>
        </li>
    </nav>
</template>
resources/assets/js/components/About.vue
<template>
    <div>
        This page describe who we are.
    </div>
</template>

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

  • Vue が axios を使って Laravel にリクエストを送る
  • その結果をコンポーネントのデータに入れる
  • View が更新される
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: createdmounted との違いは何だろう?

↓ 追記

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つ以外も。

acro5piano
React.js / TypeScript / DevOps エンジニア。 Fastlane のコントリビューター。 Terraform と Ansible のファン。 https://twitter.com/acro5piano
https://github.com/acro5piano
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした