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

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

More than 1 year has passed since last update.

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

つくるもの

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

アートボード 1.png
こんな感じで一連の動きを学習用に作ってみました。

環境

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

サーバサイド

Laravelインストール

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

$ laravel new

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

設定

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

.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リソースファイルの骨組みが作成されます。

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

マイグレーション

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

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のプロパティにない項目を作成したり、少し手を加えたりなどもできます。

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で格納されるので、いろいろといじったりも簡単です。

コントローラー

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

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();
}

// 1データ表示
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に以下を記述するだけです。

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/ディレクトリに以下を作成します。

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に振られます。

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ファイルに必要な内容を記述します。

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ファイル作成

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ファイル作成

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ファイル作成

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ファイル作成

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

shin1kt
webまわり php,html,css,javascript laravel,vue.js.jquery,wordpress,cakephp,yii,ec-cube他
http://www.katacom.jp/a/
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