vue.js
vue-router
ElementUI
vue-localstorage

最近流行りのフロントエンド構成(実装編)

こんにちわ
グローバルセンス株式会社のskanehiraです。

久しぶりの投稿になってしまいましたが、
以前こちらの記事で紹介した構成を使って、簡単なTodoアプリを作ってみようと思います。

フロントだけで完結できるものなので、結局axiosを使用していないが、
それについては別記事に書く予定です。

出来上がったモノ

こちらに上げておきますた。
表示・追加・更新・削除は一通り出来ます。
エラー処理はしていないので、変な動きするかもですがサンプルなのでご容赦下さい。

今回はサンプルなのでDB使わず、
データはブラウザ側のLocalStorageに保存するようにしています。

vueのコンポーネントからLocalStorageの操作をしたいので、
vue-localstorageを使用しています。

全体の構成はこんな感じになっています。

├── app
│   └── src
│       ├── components
│       │   ├── addTodo
│       │   │   ├── addTodo.html
│       │   │   └── addTodo.vue
│       │   ├── editTodo
│       │   │   ├── editTodo.html
│       │   │   └── editTodo.vue
│       │   ├── header
│       │   │   ├── header.html
│       │   │   └── header.vue
│       │   ├── todoList
│       │   │   ├── todoList.html
│       │   │   └── todoList.vue
│       │   └── top
│       │       └── top.vue
│       └── js
│           ├── app.js
│           └── routes.js
├── package-lock.json
├── package.json
├── public
│   ├── index.html
│   └── js
│       └── bundle.js
└── webpack.config.js

使い方

シンプルなので説明するまでもないが一応…

  • Todo追加 image.png

タスクの概要と詳細を書いて追加するだけです。
image.png

追加されたTodoは一覧で見れます。
image.png

DetailはTodoの詳細を表示します。
image.png

EditはTodoの更新ができます。
追加画面とほぼ同じです。
image.png
image.png

Deleteはそのまま削除します。
image.png

構成について

今回作成したコンポーネントは以下の5つになります。

  • addTodo
    Todoの追加を行う画面
    一応簡単なvalidationも軽く実装
  • editTodo
    Todoの編集画面
    追加画面と基本的に同じ
  • header
    ヘッダと書いてありながら実質メニュー
  • todoList
    追加したTodoの一覧
    更新・詳細表示・削除はこの画面から行う
  • top
    一番最初に表示される画面
    無くてもいいが、とりあえず作った感じ

そして、こちらのファイル達は、重要な役割を担っています。

  • app/src/js/routes.js
    v-routerのルーティング処理を実装
    どのpathでどのコンポーネントを呼び出すかをここで定義
  • app/src/js/app.js
    各種コンポーネント、css、ライブラリを読み込み、vueインスタンスを生成する処理を実装
    初期化処理などはここにおく
  • public/js/bundle.js
    ビルド済みのjsファイル
    こいつがあれば問題なし
  • public/index.html
    ビルド済みのbundle.jsを読み込み
    あとはvue-routerでコンポーネントの描写に必要なディレクティブを定義するくらい
    あんまり、やることがない…

ざっくり説明するとこんな感じになります。
これ以上分かりやすい説明が思いつかないので、実際ソースを見ながら説明します。

とりあえずindex.html

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>sample Todo app</title>
</head>

<body>
  <main id="app">
    <headers></headers>
    <router-view></router-view>
  </main>
  <script src="./js/bundle.js"></script>
</body>
</html>

スリムところかやせ細って骨くらいしか残っていないですね。
それが良いですが。

mainタグにidを付けています。
これはvueインスタンスを生成する時にどのタグにbindするかを指定する必要があるためです。
詳しくはvueの公式を見て下さい。

<headers>はヘッダ(メニュー)の部分になります。
※headerというディレクティブがすでに存在するから複数形にせざるを得ない…

<router-view>はvue-routerを使うと時のお作法なので、気にせず覚えれば良いと思います。
これがないと各画面が描写されないのでご注意を。
詳しくは(ry

<headers><router-view>を別々に分けたのはヘッダを固定するためです。
メニューを各画面に実装すると非効率なので、別コンポーネントに切り出しました。

app.js

app.js
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import locale from 'element-ui/lib/locale/lang/en';
import VueRouter from 'vue-router';
import VueLocalStorage from 'vue-localstorage';

import header from '../components/header/header.vue';
import routes from './routes';

Vue.use(VueRouter);
Vue.use(ElementUI, { locale });
Vue.use(VueLocalStorage);

const router = new VueRouter({
    routes: routes
});

const app = new Vue({
    el: '#app',
    components: {
        'headers': header,
    },
    router,
});

vue-router、ElementUI、vue-localstorageを各コンポーネントで使用したいので、
Vue.useでそれらをVueインスタンスに追加しています。

あとは基本的にライブラリのお作法の処理なので割愛します。

routes.js

import TodoList from '../components/todoList/todoList.vue';
import AddTodo from '../components/addTodo/addTodo.vue';
import EditTodo from '../components/editTodo/editTodo.vue';
import Top from '../components/top/top.vue';

export default [
        { path: "/", component: Top },
        { path: "/TodoList", component: TodoList },
        { path: "/AddTodo", component: AddTodo },
        { path: "/EditTodo/:index", component: EditTodo },
]

こちらもシンプルで、各コンポーネントをimportして、
ルーティング情報を定義しているだけです。
画面を増やす時はこちらの定義も追加しないと画面遷移しないがよく忘れます。。

Todo一覧

大体の流れが分かるtodo一覧画面に焦点を絞って説明します。
他の画面は、リポジトリを見て頂ければと思います。

todoList.html
<div>
    <el-card shadow="never" style="margin-right: 30%; margin-left: 30%">
        <template>
            <el-table :data="todoList" style="width: 100%">
                <el-table-column prop="title" label="Todo" width="120px"></el-table-column>
                <el-table-column label="Operations" fixed="right" align="center">
                    <template slot-scope="scope">
                        <el-button size="mini" @click="handleDetail(scope.row)">Detail</el-button>
                        <el-button size="mini" @click="handleEdit(scope.$index)">Edit</el-button>
                        <el-button size="mini" type="danger" @click="handleDelete(scope.$index)">Delete</el-button>
                    </template>
                </el-table-column>
            </el-table>
        </template>
    </el-card>

    <el-dialog :title="todo.title" :visible.sync="centerDialogVisible" width="30%" center>
        <pre>{{todo.detail}}</pre>
        <span slot="footer" class="dialog-footer">
            <el-button type="primary" @click="centerDialogVisible = false">OK</el-button>
        </span>
    </el-dialog>
</div>

ElementUIをガッツリ使用しています。
ポイントの部分をかい摘んで行きます。

  • <el-table>
    :dataでtodoの配列を指定すれば、
    あとはライブラリ側でうまい具合にテーブルを作ってくれます。

  • <el-table-column>
    propは各オブジェクトのプロパティを抽出して表示します。

  • <template slot-scope="scope">
    各オブジェクトのデータを個別に扱いたいときはscopeを使用します。
    scope.rowがtodoオブジェクトです。

  • <el-button>
    @clickはボタンを押下した時に呼び出すメソッドを指定します。
    scope.rowやらscope.$indedを引数として渡しています。
    @clickについてvueの公式サイトを見て下さい。

  • <el-dialog>
    Detailを押下した時に表示するダイアログです。
    :visible.syncで指定している変数がtrueなら表示、falseなら非表示という仕組みです。

todoList.vue
<template src="./todoList.html"></template>

<script>
export default {
    data() {
        return {
            todo: {
                title: "",
                detail: "",
            },
            todoList: [],
            centerDialogVisible: false,
        }
    },
    created() {
        this.todoList = this.getTodoListFromStorage();
    },
    methods: {
        getTodoListFromStorage() {
            let data = this.$localStorage.get("todoList");
            // nullの場合空配列を返却
            if (!data) {
                return [];
            }
            // jsonを返却
            return JSON.parse(data);
        },
        setTodoListToStorage(data) {
            this.$localStorage.set("todoList", JSON.stringify(data));
        },
        handleDetail(data) {
            this.todo = data;
            this.centerDialogVisible = true;
        },
        handleEdit(targetIndex) {
            this.$router.push("/EditTodo/" + targetIndex);
        },
        handleDelete(targetIndex) {
            // callbackから直接thisにアクセス出来ないため、ローカル変数経由でアクセスする
            let todoList = this.todoList;

            // 配列から対象レコードを除去
            todoList.some(function(currentData, index) {
                if (index == targetIndex) todoList.splice(index, 1);
            });

            // ストレージ更新
            this.setTodoListToStorage(this.todoList);
        }
    }
}
</script>
  • <template>
    相棒のhtmlを指定しています。
    vueはあくまでもロジックを実装する場所なので、htmlを切り出しました。

  • data()
    画面で使用するデータをreturnしています。
    returnまではお作法なのです。

  • getTodoListFromStorage()
    LocalStorageからデータを取得しています。
    LocalStorageに保存できるデータは文字列なので、
    保存時はオブジェクトから文字列に変換、
    取得時はJSONにパースする必要があります。

  • handleDetail()
    Detailボタンを押下時に呼ばれるメソッド。
    単純に表示するデータをセットして、フラグをtrueにするだけです。

  • handleEdit()
    Todo編集画面に遷移するだけです。
    app.jsでVue.use(VueRouter)をしたのはthis.$routeを使用できるようにするためです。

  • handleDelete()
    配列から対象データを削除してLocalStorageを更新しているだけです。
    1点注意なのが、someで指定しているcallbackからはthisにアクセスできないので、
    一度ローカル変数を挟む必要が有るようです。
    知らなかった…

最後に

ソースを見つつ、かい摘んで説明しましたが、あんまりぴんと来ない方もいるかと思います。
なので自分で実装してみるのが一番良いと思います。

実装時にぼくが見ていたものはこちらになりますので、
もし改造してみたいとか、作ってみたいとかがあれば参考になればと思います。

雑感

簡単なTodoアプリでも意外と時間がかかりましたが、
もっと短時間で作れるように精進します。。

vue.jsは本当に便利なので、これからもメインで使っていきたいと思っています。
UIコンポーネントは…ちょっとこちらが気になっています。
いずれ使ってみようと思います。

次回の記事は、今作っているサービスについて何か書けたらと思います。