framework
SPA
vue.js
laravel5
Lighter

SPAフレームワークLighterの紹介

Lighterとは

Lighterは僕が開発したLaravelとVue.jsベースのSPAフレームワークです。

なぜ作ったか

最近LaravelとVue.jsでSPAを作っています。
その中で「Laravelとの通信の部分が増えてきてコードが見にくくなっている」と思いました。
もうちょっとリーダブルなコードがかけないものか...

せや、フレームワークつくろ。
(現時点ではフレームワークと言っていいのかわかりません。)

Lighterの紹介

実際にLighterでTODOアプリを作りながら解説していきます。
良ければ一緒に進めてみてください。

※注意:この記事では基本的にyarnを使っていきます。

ダウンロードとセットアップ

こちらからzipでダウンロードしてください。
ダウンロードしたら解凍してください。
ターミナルかコマンドプロンプトで解凍したフォルダに移動してください。
そこで最初に

composer install

を実行してください。
次にnpmを使っている方は

npm install
npm run lighter:setup

を実行してください。
yarnを使っている方は

yarn
yarn run lighter:setup

を実行してください。

サーバーの起動

HMRサーバーとPHPサーバーの起動を行います。

最初にHMRサーバーの起動。

npmを使っている方は

npm run hot

を実行してください。
yarnを使っている方は

yarn run hot

を実行してください。

次にPHPサーバーの起動

新しいターミナルかコマンドプロンプトを開き

php artisan serve

を実行してください。

これでhttp://127.0.0.1:8000/を開き
スクリーンショット 2018-07-11 10.59.08.png
が表示されていたら成功です。

TODOアプリの作成

機能としては最低限の
1. タスクの一覧表示
2. タスクの作成
3. タスクの削除今回は長くなってしまうので作りません。
4. タスクの並び替え今回は長くなってしまうので作りません。
4. タスクの編集
5. タスクの「完了・未完了」の切り替え
にします。
見た目は最近気にっているvuetifyを使います。

設定

今回のTODOアプリはデータベースが必要なので1つ作っておいてください。
名前はご自由に。
まず.envファイルを開いてください。
ここでデータベースの設定をしてください。
僕の場合はMAMPを使っているので

.env
APP_NAME=LighterTodo
APP_ENV=local
APP_KEY=# App key here
APP_DEBUG=true
APP_URL=http://localhost

LOG_CHANNEL=stack

DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_SOCKET=/Applications/MAMP/tmp/mysql/mysql.sock
DB_DATABASE=lighter_todo_demo
DB_USERNAME=root
DB_PASSWORD=root

BROADCAST_DRIVER=log
CACHE_DRIVER=file
SESSION_DRIVER=file
SESSION_LIFETIME=120
QUEUE_DRIVER=sync

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

Vuetifyのインストール

yarn add vuetify

でインストールします。

次にresources/assets/js/app.jsを開いてください。
最初は

app.js
import Vue from 'vue'

import router from './router'
import store from './store'
import Lighter from './lighter'

import '../sass/app.scss'

Vue.component('app', require('@/layouts/App'))

const app = new Vue({
    router,
    store,
}).$mount('#app')

こんな内容になっていると思います。
これを

app.js
import Vue from 'vue'

import Vuetify from 'vuetify' //Vuetifyのインポート
import '../../../node_modules/vuetify/dist/vuetify.min.css' //VuetifyのCSSの読み込み

import router from './router'
import store from './store'
import Lighter from './lighter'

import '../sass/app.scss'

Vue.use(Vuetify) //Vuetifyを有効化

Vue.component('app', require('@/layouts/App'))

const app = new Vue({
    router,
    store,
}).$mount('#app')

このようにします。
これでVuetfiyが使える状態になりました。

MaterialDesignIconsを読み込む

resources/views/app.blade.phpを開いてください。
内容を以下のようにしてください。

app.blade.php
<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="https://fonts.googleapis.com/css?family=Material+Icons" rel="stylesheet">

    <title>Lighter</title>

    @include('Lighter.HeadScript')
</head>
<body>
<div id="app">
    <app></app>
</div>
<script src="{{mix('js/app.js')}}"></script>
</body>
</html>

不要なマイグレーションの削除

まず、LaravelAuthのマイグレーションを削除します。
database/migrationsフォルダを空にしてください。

Taskモデルとtasksテーブルの作成

php artisan make:model Task -mを実行しTaskモデルとtasksテーブルのマイグレーションを作成します。
database/migrationstasksテーブルのマイグレーションが作成されていることを確認してください。
'tasks`テーブルのマイグレーションの内容を以下のようにします。

<?php

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

class CreateTasksTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('order');
            $table->string('name');
            $table->boolean('is_finished');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('tasks');
    }
}
  1. orderカラムはタスクの順番
  2. nameカラムはタスクの名前
  3. is_finishedカラムはタスクの完了・未完了(trueなら完了、falseなら未完了)

次にTaskモデルの準備をします。
app/Task.phpを以下のようにします。

Task.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    protected $table = 'tasks';
}

全体のレイアウトを決める

resources/assets/js/layoutsフォルダを見てください。
このフォルダはアプリケーションのレイアウトコンポーネントを入れるフォルダです。
基本的なレイアウトはApp.vueファイルに記述します。
App.vueを以下のようにします。

App.vue
<template>
    <v-app>
        <v-toolbar color="blue" dark>
            <v-toolbar-title>
                Lighter TODO
            </v-toolbar-title>
        </v-toolbar>
        <v-content>
            <v-container>
                <router-view></router-view>
            </v-container>
        </v-content>
    </v-app>
</template>

<script>
    export default {
        name: "app",
    }
</script>

<style scoped>
</style>

TaskListページの作成

resources/assets/js/pagesフォルダを見てください。
このフォルダはアプリケーションのページコンポーネントを入れるフォルダです。
初期にはExample.vueファイルが入っています。
このファイルは不要なので削除します。

次にTaskList.vueファイルを作成してください。
試しに内容を以下のようにしてみます。

TaskList.vue
<template>
    <div>
        <h1 class="display-3">TaskList</h1>
    </div>
</template>

<script>
export default {
    name: 'TaskList',
}
</script>

<style>

</style>

最後にルーターに登録します。
resources/assets/js/routerフォルダを見てください。
このフォルダにはindex.jsroutes.jsファイルがあります。
index.jsにはルーターの定義が予め書いてあります。
routes.jsにはルートの定義を書きます。

routes.jsを開き、以下のようにします。

routes.js
const routes = [
    {
        path: '/',
        component: require('@/pages/TaskList')
    }
]

export default routes

そして
スクリーンショット 2018-07-11 12.55.42.png
このように表示されてれば正常です。

CreateTaskページの作成

resouces/assets/js/pagesフォルダにCreateTask.vueファイルを作成してください。
内容は以下のようにしてください。

CreateTask.vue
<template>
    <div>
        <h1 class="display-3">CreateTask</h1>
        <v-card>
            <v-card-text>
                <v-text-field label="Name" v-model="name"></v-text-field>
                <v-btn color="primary" @click="create">Create</v-btn>
            </v-card-text>
        </v-card>
    </div>
</template>

<script>
export default {
    name: 'CreateTask',
    data () {
        return {
            name: '',
        }
    },
    methods: {
        create () {

        }
    }
}
</script>

<style>

</style>

http://127.0.0.1:8000/createにアクセスし

スクリーンショット 2018-07-11 13.16.15.png
このように表示されていれば正常です。

Sparkの簡単な説明

LighterにはSparkという機能があります。
SparkはLaravelとの通信をより分かりやすくするためのラッパーです。
例えば今回のtasksテーブルに対応するSparkを作ると以下のようなプログラムでtasksテーブルにレコードをインサートすることができます。

Tasks.create({
    name: 'Create a project.'
})

セキュリティー面はLaravel側で行うのでフロント側では最低限のバリデーションをもたせます。

TasksSparkの作成

resources/assets/js/sparksフォルダを見てください。
このフォルダに各通信やテーブル操作のsparkを作成していきます。

今回はtasksテーブルに対応するsparkを作るのでTasks.jsを作成します。

まずSparkimportします。

Tasks.js
import Spark from '../lighter/spark'

次にTasksオブジェクトを作ります。

Tasks.js
import Spark from '../lighter/spark'

var Tasks = new Spark('tasks') // Tasksオブジェクトの作成

このようにSparkはクラスになっていて、コンストラクタにルートプレフィックス(後記述)を渡します。

対応するルートの作成

Sparkは各操作に対応するルートにリクエストすることでデータを取得します。
routes/web.phpを開いてください。

中身が以下のようになっていると思います。

web.php
<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::prefix('local_api')->group(function () {
    /*
     * Local API route here.
     */
});

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

この

Route::prefix('local_api')->group(function () {
    /*
     * Local API route here.
     */
});

の部分にルートを定義します。
また、先程のSparkコンストラクタのルートプレフィックスをベースにルートを作ります。
なので先程のtasksだとtasksというプレフィックスでルートをグループ化します。
そして基本はpost通信でリクエストされるのでpostルートを定義することになります。
そのためTaskコントローラーを作る必要があります。

php artisan make:controller TaskControllerでコントローラーを作成してください。
app/Http/Controllers/TaskController.phpを開き内容を以下のようにします。

TaskController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TaskController extends Controller
{
    public function create (Request $request)
    {
        $name = $request->get('name');

        $task = new \App\Task;
        $task->order = \App\Task::all()->count();
        $task->name = $name;
        $task->is_finished = 0;
        $task->save();

        return \Lighter\Helper\Lesponse(true, [], 'Created the task.');
    }
}

create関数のreturn \Lighter\Helper\Lesponse(true, [], 'Created the task.');を見てください。
これのLesponse関数はLighter responseの略で引数はLesponse(処理が成功したか, 返すデータ, メッセージ)です。

再びweb.phpを開いてこのTaskControllerを対応させましょう。

web.phpを以下のようにします。

web.php
<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::prefix('local_api')->group(function () {
    Route::prefix('tasks')->group(function () {
        Route::post('create', 'TaskController@create');
    });
});

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

次にTasks.create関数でtasksテーブルにインサートするようにします。

Tasks.jsを開いて以下のようにします。

Tasks.js
import Spark from '../lighter/spark'

var Tasks = new Spark('tasks')

Tasks.create = async function (name)
{
    return await this.request('create', {name})
}

export default Tasks

Tasksオブジェクトにcreate関数を追加しました。
このcreate関数の中のthis.request('create', {name})で通信を行います。
先程のweb.phpで定義したtasks/createnameを送っています。

では実際にこのTasks.create関数を試してみます。

resources/assets/js/app.jsを以下のようにします。

app.js
import Vue from 'vue'

import Vuetify from 'vuetify'
import '../../../node_modules/vuetify/dist/vuetify.min.css'

import router from './router'
import store from './store'
import Lighter from './lighter'

import Tasks from './sparks/Tasks'
window.Tasks = Tasks //グローバルに使えるように

import '../sass/app.scss'

Vue.use(Vuetify)

Vue.component('app', require('@/layouts/App'))

const app = new Vue({
    router,
    store,
}).$mount('#app')

デベロッパーツールを開いてください。
コンソールでTasks.create('test')を実行してください。

以下のようにtasksテーブルに追加されているはずです。

スクリーンショット 2018-07-11 14.50.48.png

これを利用してCreateTaskページに組み込んでみます。

CreateTaskページの実装

resources/assets/js/pages/CreateTask.vueを以下のようにします。

CreateTask.vue
<template>
    <div>
        <h1 class="display-3">CreateTask</h1>
        <v-card>
            <v-card-text>
                <v-text-field label="Name" v-model="name" :error-messages="nameErrors"></v-text-field>
                <v-btn color="primary" @click="create">Create</v-btn>
            </v-card-text>
        </v-card>
    </div>
</template>

<script>
import Tasks from '@/sparks/Tasks'

export default {
    name: 'TaskList',
    data () {
        return {
            name: '',
            nameErrors: []
        }
    },
    methods: {
        async create () {
            this.nameErrors = []

            if (this.name.length === 0) {
                this.nameErrors = ['Name is required!']
                return
            }

            await Tasks.create(this.name)

            this.$router.push('/')
        }
    }
}
</script>

<style>

</style>

http://127.0.0.1:8000/createを開きNameを適当に入力しCREATEボタンを押してみてください。
タスクが追加されているはずです。
5こぐらい適当に作ってみてください。

TaskListページの実装

TaskListには全タクスの情報が必要なのでSparkから持ってこられるようにします。
まずコントローラーを編集します。

TaskController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TaskController extends Controller
{
    public function create (Request $request)
    {
        $name = $request->get('name');

        $task = new \App\Task;
        $task->order = \App\Task::all()->count();
        $task->name = $name;
        $task->is_finished = 0;
        $task->save();

        return \Lighter\Helper\Lesponse(true, [], 'Created the task.');
    }

    public function all () //追加
    {
        return \Lighter\Helper\Lesponse(true, \App\Task::all(), 'Got all tasks.');
    }
}

次にweb.phpも以下のように変更します。

web.php
<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::prefix('local_api')->group(function () {
    Route::prefix('tasks')->group(function () {
        Route::post('create', 'TaskController@create');
        Route::post('all', 'TaskController@all'); //追加
    });
});

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

最後にSparkに実装します。
resources/assets/js/sparks/Tasks.jsを以下のようにしてください。

Tasks.js
import Spark from '../lighter/spark'
import alac from '../lighter/alac'

var Tasks = new Spark('tasks')

Tasks.create = async function (name)
{
    return await this.request('create', {name})
}

Tasks.all = async function (name)
{
    var spark = this
    var tasks = await this.request('all').then(alac.resultData)
    var models = []

    tasks.forEach(task => {
        models.push(spark.getModel(task))
    })

    return models
}

export default Tasks

all関数を追加してます。
all関数の中身を解説すると最初にvar spark = thisとあります。
これは後ほど配列をforEachで回す際に使うためです。
次にvar tasks = await this.request('all').then(alac.resultData)でデータを取得します。
alacとはaxiosのラッパーでAjax Local API Clientの略です。alac.resultData関数でレスポンスを扱いやすく加工します。
次にvar models = []配列を宣言しています。この中にレコードを使いやすくしたModelが入ります。
次にtasksforEachで回し各要素をspark.getModel関数に渡し、それをmodelsに追加します。
最後にmodels配列を返して終了です。

これを使ってTaskListを実装します。

resources/assets/js/pages/TaskList.vueを開いて以下のようにします。

TaskList.vue
<template>
    <div>
        <h1 class="display-3">TaskList</h1>
        <v-list>
            <template v-for="task in tasks">
                <v-list-tile>
                    <v-list-tile-action>
                        <v-checkbox v-model="task.is_finished"></v-checkbox>
                    </v-list-tile-action>
                    <v-list-tile-title>
                        {{task.name}}
                    </v-list-tile-title>
                </v-list-tile>
                <v-divider></v-divider>
            </template>

        </v-list>
    </div>
</template>

<script>
import Tasks from '../sparks/Tasks'

export default {
    name: 'TaskList',
    data () {
        return {
            tasks: [],
        }
    },
    async mounted () {
        this.tasks = await Tasks.all()
    },
}
</script>

<style>

</style>

とりあえず表示すると
スクリーンショット 2018-07-11 15.38.05.png
こうなります。

しかしまだ保存機能がついていません。
Sparkにはデフォルトで簡単にレコードを保存する機能がついています。
その機能を使用するためにTaskControllerweb.phpを編集します。

app/Http/Controllers/TaskController.phpを開き以下のようにしてください。

TaskController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TaskController extends Controller
{
    public function create (Request $request)
    {
        $name = $request->get('name');

        $task = new \App\Task;
        $task->order = \App\Task::all()->count();
        $task->name = $name;
        $task->is_finished = 0;
        $task->save();

        return \Lighter\Helper\Lesponse(true, [], 'Created the task.');
    }

    public function all ()
    {
        return \Lighter\Helper\Lesponse(true, \App\Task::all(), 'Got all tasks.');
    }

    public function update (Request $request)
    {
        $task = \App\Task::find($request->get('id'));
        $task->name = $request->get('name');
        $task->is_finished = $request->get('is_finished');
        $task->save();

        return \Lighter\Helper\Lesponse(true, [], 'Saved the task.');
    }
}

routes/web.phpを以下のようにしてください。

web.php
<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::prefix('local_api')->group(function () {
    Route::prefix('tasks')->group(function () {
        Route::post('create', 'TaskController@create');
        Route::post('all', 'TaskController@all');
        Route::post('update', 'TaskController@update');
    });
});

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

こうすることでレコードを簡単に保存できるようになります。
コンソールを開いてください。

まずvar tasks = await Tasks.all()を実行します。
次にvar task = tasks[0]で最初のタスクを取り出します。
次にtask.name = 'task0'を実行し、nameを変更します。
最後にtask.save()で保存完了です。

実際に確かめると
スクリーンショット 2018-07-11 15.49.08.png
nametask0に変わっています。
これを利用し保存機能をつけます。

resources/assets/js/pages/TaskList.vueを以下のようにしてください。

TaskList.vue
<template>
    <div>
        <h1 class="display-3">TaskList</h1>
        <v-list>
            <template v-for="task in tasks">
                <v-list-tile>
                    <v-list-tile-action>
                        <v-checkbox v-model="task.is_finished" @input="save(task)"></v-checkbox>
                    </v-list-tile-action>
                    <v-list-tile-title>
                        {{task.name}}
                    </v-list-tile-title>
                </v-list-tile>
                <v-divider></v-divider>
            </template>

        </v-list>
    </div>
</template>

<script>
import Tasks from '../sparks/Tasks'

export default {
    name: 'TaskList',
    data () {
        return {
            tasks: [],
        }
    },
    async mounted () {
        this.tasks = await Tasks.all()
    },
    methods: {
        save (task) {
            task.save()
        }
    },
}
</script>

<style>

</style>

http://127.0.0.1:8000/を開き、適当なタスクのチェックボックスを入れて再読込してください。
スクリーンショット 2018-07-11 15.53.53.png
ちゃんと反映されています。

編集機能の作成

resources/assets/js/pages/TaskList.vueを以下のようにしてください

TaskList.vue
<template>
    <div>
        <h1 class="display-3">TaskList</h1>
        <v-list>
            <template v-for="task in tasks">
                <v-list-tile>
                    <v-list-tile-action>
                        <v-checkbox v-model="task.is_finished" @input="save(task)"></v-checkbox>
                    </v-list-tile-action>
                    <v-list-tile-title>
                        {{task.name}}
                    </v-list-tile-title>
                    <v-list-tile-action>
                        <v-btn color="success" @click="openEditDialog(task)">Edit</v-btn>
                    </v-list-tile-action>
                </v-list-tile>
                <v-divider></v-divider>
            </template>

        </v-list>

        <v-dialog v-model="editDialog" max-width="500">
            <v-card>
                <v-card-title><h1>Edit</h1></v-card-title>
                <v-card-text>
                    <v-text-field label="Name" v-model="editName" :error-messages="editNameErrors"></v-text-field>
                </v-card-text>
                <v-card-actions>
                    <v-btn flat @click="editDialog = false">Close</v-btn>
                    <v-spacer></v-spacer>
                    <v-btn color="primary" @click="applyEdit">Save</v-btn>
                </v-card-actions>
            </v-card>
        </v-dialog>
    </div>
</template>

<script>
import Tasks from '../sparks/Tasks'

export default {
    name: 'TaskList',
    data () {
        return {
            tasks: [],
            editDialog: false,
            editTask: null,
            editName: '',
            editNameErrors: [],
        }
    },
    async mounted () {
        this.tasks = await Tasks.all()
    },
    methods: {
        save (task) {
            task.save()
        },
        openEditDialog (task) {
            this.editTask = task
            this.editName = task.name
            this.editNameErrors = []
            this.editDialog = true
        },
        applyEdit () {
            this.editTask.name = this.editName
            this.editTask.save()
            this.editDialog = false
        }
    },
}
</script>

<style>

</style>

やっていることは先程ほぼ同じです。
Editボタンを押すとダイアログが開き、Nameを入力しSaveを押すと保存されます。

長くなりましたがこれで完成です。

今後の課題

今後の課題として
1. ドキュメントの作成
2. CLIツールの作成
などがあります。

最後に

今回作成したTODOアプリはgithubで公開しています。

https://github.com/lighter-framework/lighter-qiita-todo-demo

良ければアドバイスや意見をいただけると幸いです。