Vue.js + vuexによるToDoアプリケーションの実装
前々回の記事でLaravel 5.4 + Vue.jsの開発環境を構築し、前回の記事でLaravel 5.4によるWeb APIを作成しました。
今回は、作成したWeb APIを使用したToDoリストアプリケーションを、Vue.jsを使って作成します。
PHP側の積み残し
はじめに、昨日のコントローラーを修正します。
もともと、ItemControllerのindex()メソッドは「Item::all()」メソッドで全てのアイテムを返していまし た。しかし、画面上に表示するのは未完了のアイテムだけです。そこで、index()メソッドを以下のように変更します。
<?php
public function index()
{
return response(Item::query()->where('checked', false)->get());
}
これで、未完了(checkd=false)のアイテムだけを取得できるようになりました。tests/Feature/ItemTest.php にも機能テストを追加しておきましょう。
<?php
public function testIndexReturnsOnlyUncheckedItems()
{
$item = Item::query()->find(1);
$item->checked = 1;
$item->save();
$response = $this->get('/api/items');
$response->assertStatus(200);
$this->assertCount(0, $response->json());
}
次に、ルーティングを変更し、新しいビューテンプレートを作成します。
routes/web.phpは以下のように、'/'へのアクセスでindexというテンプレートを表示するようにします。
<?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::get('/', function () {
return view('index');
});
次に、resources/views/index.blade.phpに以下の内容でファイルを追加します。
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>ToDo App with Laravel 5.4 + Vue.js</title>
<link rel="stylesheet" href="css/app.css"/>
<script type="text/javascript">
window.Laravel = window.Laravel || {};
window.Laravel.csrfToken = "{{csrf_token()}}";
</script>
</head>
<body>
<div id="app"></div>
<script src="js/app.js"></script>
</body>
</html>
以上でバックエンド側の準備は完了です。
完成図
Vue.jsアプリケーションの完成図を示します。
シンプルなToDoリストで、アイテムの一覧・追加・完了・削除の機能を持っています。最終的なファイル構造は以下のようになります。
resources/assets/js
├── api
│ └── items.js
├── app.js
├── bootstrap.js
├── components
│ ├── App.vue
│ ├── Item.vue
│ ├── ItemList.vue
│ └── NewItem.vue
└── store
├── action-types.js
├── index.js
└── mutation-types.js
3 directories, 10 files
コンポーネントの構成
このアプリケーションのコンポーネントの構成は以下のようになっています。
新しいアイテムを追加するための「NewItem」と、アイテムのリストである「ItemList」が並列になっています。
ItemListの中にはItemが入れ子になっています。
これら全体を包含するのが「App」コンポーネントです。
ここで問題になるのが、NewItemからItemListへのデータの受け渡しです。
新しいアイテムを追加したら、一覧の末尾に追加したいのですが、Vue.jsが提供する機能(propsによるデータの受け渡し)では、コンポーネントに親子関係がある場合にしかできません。
Appコンポーネントが全ての親になっているので、Appコンポーネントにデータを管理する役割をもたせれば何とかなりそうですが…。
と、このようにコンポーネント間通信が必要になった場合には、状態管理のためのライブラリであるvuexの導入を検討するとよいでしょう。
vuexは、以下のコマンドでインストールできます。
$ npm install --save-dev vuex
このアプリケーションでは、状態管理にはvuexを使用します。
アプリケーションのセットアップ
はじめに、不要なコンポーネントを削除します。
resources/assets/js/components から、Example.vue(と、Hello.vue)を削除しましょう。
次に、Vue.jsでvuexを使えるようにする設定を行います。
resources/assets/js/bootstrap.js で、「window.Vue」の定義の直後に以下のコードを追加します。
window.Vue.use(require('vuex'));
これでVue.jsがvuexを使えるようになります。次に、アプリケーションの初期化処理を記述したresources/assets/js/app.jsを見てみます。
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap');
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
const App = require('./components/App.vue');
const store = require('./store/').default;
const app = new Vue({
el: '#app',
store,
render: h => h(App)
});
ここでは、以下の仕事を行っています。
1. Appコンポーネントの読み込み
2. Storeオブジェクトの読み込み
3. Vueアプリケーションの初期化
4. Appコンポーネントの描画
AppコンポーネントとStoreについては 後述します。
Vueアプリケーションの初期化時に、Appコンポーネントの描画を実行させているのがポイントです。
Storeの実装
Fluxの仕組みを解説をすると長くなってしまうので、解説は省略します。
vuexのドキュメントがわかりやすいので、こちらを参照してください。
vuexでは、Vuex.Storeオブジェクトに、アプリケーションの横断的な状態(state)を閉じ込めます。
また、stateの変更(mutation)はミューテーターと呼ばれる関数を介して行う必要があります。
このように、情報の一元化 + アクセス方法の制限によって、アプリケーションがもつデータの管理を行いやすくなるのがvuexの利点です。
依存関係の少ないところから見ていくとわかりやすいので、まずはWeb APIとの通信を行う機能の実装から紹介します。
resources/assets/js/api/items.js
const axios = require('axios');
const API_URI = '/api/items';
export const ItemsAPI = {
getAllUnchecked(callback) {
axios.get(API_URI)
.then((response) => {
callback(response.data);
})
.catch((error) => {
console.log(error);
});
},
create(content, callback) {
axios.post(API_URI, {content: content})
.then((response) => {
callback(response.data);
})
.catch((error) => {
console.error(error);
});
},
check(id, callback) {
axios.patch(API_URI + '/' + id, {checked: true})
.then((response) => {
callback(response.data);
})
.catch((error) => {
console.error(error);
});
},
delete(id, callback) {
axios.delete(API_URI + '/' + id)
.then((response) => {
callback(response.data);
})
.catch((error) => {
console.error(error);
});
}
};
実行している処理がほとんど同じなので、冗長な感じですが、やってることは簡単で、Web APIへ問合せて、結果をコールバック関数に渡しているだけです。
次に、Storeオブジェクトの実装を示します。
const Vuex = require('vuex');
const {MUTATION} = require('./mutation-types');
const {ACTION} = require('./action-types');
const {ItemsAPI} = require('../api/items');
const state = {
items: []
};
const getters = {
uncheckedItems: (state) => state.items.filter(item => !item.checked)
};
const actions = {
[ACTION.GET_ITEMS] ({commit}) {
ItemsAPI.getAllUnchecked(items => {
commit(MUTATION.SET_ITEMS, items);
});
},
[ACTION.CREATE_ITEM] ({commit}, content) {
ItemsAPI.create(content, (item) => {
commit(MUTATION.ADD_ITEM, item);
});
},
[ACTION.CHECK_ITEM] ({commit}, id) {
ItemsAPI.check(id, () => {
// チェックされたアイテムをリストから削除
commit(MUTATION.REMOVE_ITEM_BY_ID, id);
});
},
[ACTION.DELETE_ITEM] ({commit}, id) {
ItemsAPI.delete(id, () => {
commit(MUTATION.REMOVE_ITEM_BY_ID, id);
});
}
};
const mutations = {
[MUTATION.SET_ITEMS]