laravelでVue.jsをつかってみた

#はじめに
当初はネイティブJSで実装を行う予定でしたが、laravelがvueをサポートしているということを知り良い機会だと思い取り入れて見ました。

#想定・前提
- vue理解のため導入箇所を絞ってシンプルに導入（コンポーネントのみ）
- ルーティングはlaravel側で行う（SPAはなし）
- バージョンは下記の通り

```
laravel: 5.7
vue: 2.5.7
```

#関連ファイルの準備
今回は、予めlaravelのblade側で実装していたテーブルをvueでコンポーネント化し、Twitterでよくみかける無限スクロールっぽい実装を行なっています。

##bladeの設定
- 共通テンプレート`resources/views/layouts/layout.blade.php`

```html:layout.blade.php
<!doctype html>
<html lang="ja">
    <head>
        @include('includes.partials.page_meta')
        <link href="{{ mix('/css/common.css') }}" rel="stylesheet" type="text/css">
    </head>
    <body>
        <div id="app" class="container">
            @yield('content')
            @include('includes.footer')
        </div>
        <script src="{{ mix('/js/app.js') }}" defer></script>
    </body>
</html>
```

- ページ固有テンプレート`resources/views/pages/page.blade.php`

```html:page.blade.php
@extends('layouts.layout')

@section('pageMetaTags')

@stop

@section('pageHeadStyles')
<link href="{{ mix('/css/page.css') }}" rel="stylesheet" type="text/css">
@stop

@section('content')

<div class="contents">
    <div class="wrap">
        <demo-component
            results="{{ empty($demo['results']) ? json_encode('') : json_encode($demo['results']) }}"
            :limit="5"
        >
        </demo-component>
    </div>
</div>

@stop
```
axiosを使わなくても、bladeから直接データを直接渡せるということなのでコントローラー側には手を入れていません。

```
[results] => Array
    (
        [0] => Array
            (
            [data1] => Array
                (
                    [date] =>
                    [name] => 
                    [item1] => 
                    [item2] => 
                )
            [data2] => Array
```

コントローラーから受け取るデータが上記のような場合だと、`Object`では渡せないため`json_encode`を使用してJSON形式の文字列にしてコンポーネント側に渡し配列に変換が必要でした。


##webpackの設定
```js:webpack.mix.js
let mix = require('laravel-mix');

mix.disableNotifications()
    .options({
        postCss: [
            require('autoprefixer')({
                grid: true,
                browsers: ['ie >= 9', 'Edge >= 12', 'ff >= 50', 'chrome >= 56', 'Safari >= 7', 'ios_saf >= 8', 'Android >= 4']
            })
        ]
    })
    //.js('resources/assets/js/common.js', 'public/js')
    .js('resources/assets/js/app.js', 'public/js');

if (!mix.inProduction()) {
    mix.webpackConfig({
        devtool: 'source-map'
    });
    mix.sourceMaps();
}
if (mix.inProduction()) {
    mix.version();
}
```

もともと共通用のスクリプトを書いていた`common.js`はvueを使用するにあたり`app.js`にimportさせています。

##インスタンスの設定
```js:app.js
window.Vue = require('vue');

Vue.component('demo-component', require('./components/demoComponent.vue'));

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

##コンポーネントファイルの設定
```html:demoComponent.vue

<template>
    <div v-bind:class="{'table-scroll': wrapFlg}">
        <table>
            <thead>
            <tr>
                <th>Date</th>
                <th>Name</th>
                <th>Item1</th>
                <th>Item2</th>
            </tr>
            </thead>
            <tbody v-if="records.length > 0">
                <tr v-for="record in records">
                    <td>{{ record.date }}</td>
                    <td>
                        <a :href="'/detail/' + record.id">{{ record.name }}</a>
                    </td>
                    <td>{{ record.item1 }}</td>
                    <td>{{ record.item2 }}</td>
                </tr>
                <tr v-show="loader">
                    <td colspan="4">
                        <i class="icon-loader"></i>
                    </td>
                </tr>
            </tbody>
            <tbody v-else>
                <tr>
                    <td colspan="4">
                        <span>データはまだありません。</span>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</template>

<style scoped>
    .table-scroll {
        overflow-x: scroll;
    }
    .icon-loader {
        display: inline-block;
        background-image: url(/img/loader.gif);
        background-repeat: no-repeat;
        background-size: contain;
        width: 30px;
        height: 30px;
        vertical-align: middle;
    }
</style>

<script>
    export default {
        props: {
            results: {
                type: String,
                default: ''
            },
            wrapFlg: {
                type: Boolean,
                default: true,
            },
            limit: {
                type: Number,
                default: 10
            }
        },
        data () {
            return {
                timer: null,
                startPos: 0,
                records: [],
                loader: false
            }
        },
        created: function () {
            for (let cnt = 0; cnt < this.limit; cnt++) {
                if (this.parseResults[cnt]) {
                    this.records.push(this.parseResults[cnt]);
                }
            }
        },
        mounted: function () {
            window.addEventListener('scroll', this.infiniteScroll);
        },
        beforeDestroy: function () {
            window.removeEventListener('scroll', this.infiniteScroll);
        },
        computed: {
            parseResults: function () {
                return JSON.parse(this.results);
            }
        },
        methods: {
            async fetch () {
                if (this.parseResults[this.records.length]) {
                    this.loader = true;
                    if (this.timer === null) {
                        this.timer = setTimeout(function () {
                            this.records.push(this.parseResults[this.records.length]);
                            this.timer = null;
                            this.loader = false;
                        }.bind(this), 500);
                    }
                }
            },
            getElementRect: function () {
                return this.$el.getBoundingClientRect();
            },
            infiniteScroll: function () {
                const {top, height} = this.getElementRect();
                const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
                const targetTop = top + scrollTop;
                if (window.innerHeight + scrollTop > targetTop + height) {
                    if (scrollTop > this.startPos) {
                        this.fetch();
                    }
                }
                this.startPos = scrollTop;
            }
        }
    }
</script>
```
#つかってみて
はじめてvueを使ってみましたが、思いのほか全体の設計を変えずに導入できました。
templateも`v-bind`などvue独自のものを除けば、比較的bladeの記述に近い形になるのと日本語マニュアルにある程度網羅されているので、普段laravelに触れている方は気軽に始めることができるのかと思いました。

また、自由度が高くフレームワーク依存が少ないので後からの切り離しも楽かと。