LaravelとVue.jsを使って、子供向けタスク管理アプリを制作しました。

https://routinemaker.yuruknowledge-labo.com/

子供向け、と書いてますがむしろ
「うちの子向け」です。
なにせこの企画、発案こそ私ですが細かい要望は息子(12歳)が出しましたのでw

そんな我が子の無茶振りに親バカで応じてしまった制作記録です。

今回の目的

ウェブカツの卒業試験を受ける前に
何かしらLaravel+Vue.jsでアウトプットしておこうというのがきっかけ。

で、何を作る?となったときにいくつか候補があったのですが
ちょうど子供がこの夏から中等課程に進学(でいいのかな)し
やることが増えてエライコッチャになるのが目に見えてたんですね。

この辺はいずれ育児ブログ側に書きますが
そんなこんなでタスクを実行しやすくアプリ作ってみようか?と
息子に提案したところ、割とノリノリで要望を出してきたので
それをそのまんま作ってみることにしました(*´ω`*)

それ以外の目的はこんな感じ。

  • 公開する前提でのWebサービス開発をする
  • LaravelとVue.jsの扱いに慣れる
  • VPSでのデプロイに挑戦

開発環境

  • MacBook Pro (Retina, 15-inch, Mid 2015)
  • MacOS Mojave (10.14)
  • PHP(7.3)
  • Laravel(5.8)
  • Vue.js(2.5.17)
  • Laravel-mix
  • Homestead
  • VSCode
  • PhpStorm
  • Adobe XD (カンプ作成用)

今回はこれまでとすこし環境を変えています。

まずMAMPでの開発から仮想環境を使った開発 へ。
「なんか面白そう」という理由で作ってみた仮想環境でしたが
MAMPを使っていたときと特に遜色もなく、という感じでした。

エディタはこれまでVSCodeを利用していましたが
こちらも何となく気になったという理由でPhpStorm を選択。

無料お試し期間に作り終えることを目標としましたwが
思った以上に使い勝手が良かったので、有料版を契約する予定です。
これはいずれ記事にしますね。

制作過程

機能洗い出し

まずは実装する機能の洗い出し・・の前に
息子にどんなアプリがいいのかヒアリング。

「やることをセーブしておけて、やる順番を自由に変えることができて
あ、ボタン押したら順番がランダムになるのもいい!」

「終わったら母ちゃんのイラストが出てきて格好いい音楽が鳴って
You’ve completed!とかメッセージが出て褒めて貰えるの!」

「で、過去にどんな順番でやって
どの位時間がかかったかが見れるようにして
全部出来た日はできたねスタンプが押してあって
履歴から『この順番でする』って選んで実行できて・・」

まてまてまてまて

果てしなく広がる依頼人の要望(;´Д`)

・・あのな
一応8月の最終週には卒業試験を申し込むから
それまでに出来てなきゃいかんのだよ。
要望全部詰め込んだらどう考えても間に合わないですな!

ということで
「卒業試験が終わったら全ての機能を実装する」という約束で
まず最初は以下の機能を実装することになりました。

  • ユーザー登録(メールアドレス・Twitter)
  • ログイン・ログアウト
  • パスワードリマインダ
  • リスト新規登録
  • リスト編集
  • リスト削除
  • マイページにリスト一覧表示
  • 実行前にリスト内タスク一覧表示
  • 実行前にタスクの順番をドラッグ&ドロップで変更
  • 実行前にタスクの順番をランダムに変更
  • 現在実行中のタスクを表示する
  • タスクが終わったらボタンを押して次のタスクを表示
  • 途中でやめてマイページへ戻る
  • 全てのタスクを終えたら完了画面へ
    (完了音とオリジナルイラストは後日実装)
  • 履歴表示(履歴からタスク実行は後日実装)
  • 問い合わせフォーム

Twitter登録は公開することを前提としているため
付けていた方がちょっとアプリを試したいときに便利かなと。
うちの子まだ年齢的に使えませんけどね!orz

ワイヤーフレーム制作

いつも通り、コピー用紙にボールペンでごりごり。

今回のアプリは「iPodで使う」事を前提としているため
まずスマホ用画面のワイヤーフレームを作成。
その後PC用画面を作っています。

(この段階の作業時間: 30分+スキマ時間でちまちま)

DB設計とルーティング設計

Laravelの場合、usersテーブルは最初からひな形?が用意されているので
それをそのまま利用する事に。

それ以外にリストを保存するテーブルと、履歴を保存するテーブルを作り
それぞれユーザーIDで紐付けることにしました。
リストと履歴は紐付けちゃうと、元のリストを削除したときに
履歴が大変なことになりそうだったので紐付けしていません。

そしてルーティング設計。
先ほど出した機能とワイヤーフレームを元にひとまず設計。
開発しながら適宜変更していく方針です。

これまた全力で手書きw
こういう作業過程のメモってどの程度需要があるのか分かりませんが
記録のために置いておきます。

(この段階の作業時間: 30分)

デザインカンプ制作

XDで作成。
配色はネットで見つけた配色パターン集から息子が選択。

「色が多いとそれだけで気が散る」タイプの子なので
デザインもとにかくシンプルに、使いやすいけど気が散らないように
むしろ「つまらない」デザインを目指した結果がこれです。

こちらも先にスマホ用画面を作成し
それに併せる形でPC画面を作っています。

スマホ画面はなるべく文字での説明を減らしています。
パッと見て直感的に使える画面を目指しました。

何よりこれを最も使うであろう子供のデバイスが
よりによって「iPod」ですので・・画面ちっちゃすぎて
あまり説明を入れるとごちゃつくんですよねぇ(;´Д`)

実行中画面・完了画面にはヘッダーとフッターを付けません。
ヘッダーが付いてる→メニューボタンをいじれる→気がそれる
というのを防止するためです。

カンプが出来た段階で息子にチェックをお願いし
いくつか修正してこの形になりました。

(カンプ制作時間: 4時間半)

画面モック作成

ローカルで画面モックを作成。
この段階ではVSCodeを使っています。
PhpStormでも書けるのですが、VSCodeの方が速くて使いやすい印象です。

シンプルな画面だしそこまで時間はかからない・・と
何か毎回そう思って始めるんですが
結局CSSで結構時間を食うんですよねぇorz

今回もCSS設計はFLOCSSを採用しています。
大分慣れてきましたが、未だにutilityを使ったことが無いという・・

また今回初めてフォントサイズ指定にremを使っています。
これまではpx指定でしたが、試しにやってみようかと。
結果

どうせ変数使ってSCSSで書くから、あんまり変わらないんじゃね?

という結論に至りました(;´Д`)

ページネーションだけはLaravelの機能で付けてからCSSを書くことに。
またこの段階でレスポンシブ対応も終わらせています。

(画面モック作成9時間、レスポンシブ対応3時間半)

Laravelインストールと設定

Homesteadを利用してLaravelの新しいプロジェクトを作成。
.envファイルにDB設定を書き込み、localeとタイムゾーンを
Asia/Kuala_Lumpur に。

本番環境ではAsia/Tokyoにします。
VPSのタイムゾーン設定はAsia/Tokyoだし
現在時刻を表示する機能は付けてないから時差は気にしなくて良さそうだしね。

ちなみにこの段階で
「Homestaedに入ってるphpのタイムゾーンを確認し忘れていた」
お陰で、後々履歴画面の時間がおかしくなって叫ぶ羽目になります(´・ω・`)
ちゃんと設定しておこうね・・

https://laraveler.hatenablog.com/entry/2018/11/09/222400

https://gist.github.com/maxxscho/8e6a5e6378f969c6c8b6

ここまですんだらmigrationとseedingで
DBにテーブルを作り、ダミーデータを挿入します。
今回seedingは「ユーザー1人」「そのユーザーの履歴データ12個」を
準備しました。

そしてVue.jsとLaravel-mixをインストール。
Laravelに入っているcomposer.jsonファイルには
すでにLaravel-mixやVue、axiosなど、必要な物がセットされてますので
ターミナルに

npm install

これ一発で全てがインストールされます。
ホント便利ですねぇ(*´ω`*)

ちなみにLaravel-mixを使うと
SCSSファイルやVueファイルをさくっとひとまとめに(ビルド)してくれます。
なのでこの辺のファイルを使っているときは

npm run watch

をターミナルから打ち込んで、何か変更する度
すぐにビルドするようにしておきます。

なお、migrationを使って一度作ったテーブルに変更を加えるときは
doctrine/dbalを以下のコマンドで追加する必要があります。

composer require doctrine/dbal

ログイン機能作成

Laravelで一番感動したのがこれ。
ターミナルでコマンド一発打ち込むだけで
新規登録・ログイン・ログアウト・パスワードリマインダが
簡単に実装されてしまうってホント素晴らしい(*´ω`*)

php artisan make:auth

ページ表示用のルーティング設定

先にページ表示用のルーティングを設定し
view用のbladeファイルを作って
うまくページが表示できるかを確認。

その後bladeファイルに画面モックのコードを流し込んでいきます。

    Route::get('/mypage', 'RoutinesController@mypage')->name('routines.mypage'); //マイページ表示  

    Route::get('/routines/new', 'RoutinesController@new')->name('routines.new'); //新規作成画面表示  

    Route::get('/routines/{id}/edit', 'RoutinesController@edit')->name('routines.edit'); //編集画面表示  

    Route::get('/routines/{id}/prepare', 'RoutinesController@prepare')->name('routines.prepare'); //準備画面表示  

    Route::get('/routines/{id}/proceed', 'RoutinesController@proceed')->name('routines.proceed'); //実行中画面表示  

    Route::get('/routines/complete', 'RoutinesController@complete')->name('routines.complete'); //完了画面表示  

    Route::get('/histories', 'HistoriesController@show')->name('histories.show'); //履歴表示

現時点ではログインしなくても画面が見られる状態です。
全てのviewが完成した段階でログイン認証をかけます。

これでアドレスを入力すると各画面のモックが表示できるように。

(Laravelプロジェクト作成からここまでの作業: 4時間)

ログイン関連機能の修正

ログイン機能はコマンド一発で実装できましたが
このままだとログイン後に表示されるページがhome.blade.phpのままです。

今回はマイページに遷移させたいので
RedirectIfAuthenticatedミドルウェア を変更します。

    public function handle($request, Closure $next, $guard = null)  

    {  

        if (Auth::guard($guard)->check()) {  

            return redirect('/mypage'); //ここを変更  

        }  

  

        return $next($request);  

    }  

LoginControllerやRegisiterController を変えることでも対応出来るようです。

    protected function redirectTo()  

    {  

     //フラッシュメッセージ利用時はここに書く  

        session()->flash('scc_message', __("Registered!"));   

        return '/mypage'; //ここに遷移先を入れる  

    }

両方とも設定した場合はミドルウェアが優先されるようです。

layoutテンプレートを使ってBladeファイルを整理

現時点ではBladeファイルにそのまま画面モックを流し込んだ状態なので
ここから色々切り分けて見やすいコードに変えていきます。

Vue.jsを使うので全てをVueコンポーネントにして
Vue-routerで管理することも考えたのですが
今回は

  • layout/app.blade.php に全体のレイアウトを書く
  • headerとfooter、実行準備と実行中の画面はvueを使うので.vueファイルに書き出す
  • スマホ用フローティングメニューもvueコンポーネントに
  • その他はbladeファイルにそのまま書く

ということで、再利用しまくりのところとがっつりvueを使って
動きを付ける所だけvueコンポーネントを利用する
ことに。
bladeのコンポーネントは使いませんでした。

BladeファイルからVueファイルにログイン状態を渡す

今回悩んだところその1。

ヘッダーメニューの内容をログインの有無で変更したかったのと
ログイン時にはユーザー名とメールアドレスを表示したかったので
bladeファイルからログイン状態を渡すことに。

まずview表示用のコントローラーはこんな感じに。

//ドメイントップ  

Route::get('/', function () {  

    if(Auth::check()){  

        //ログインしている場合ユーザ情報を取得  

        $user = Auth::user();  

    }else{  

        //未ログインの場合は空の連想配列  

        $user = json_encode([""=>[]]);  

    }  

    return view('index', ['user' => $user]);  

});

未ログインの場合に空の連想配列を渡しておかないと
エラーが出てしまいます。

ログインしていないと見られないページはこんな感じで書いてます。

    //マイページ表示  

    public function mypage()  

    {  

        //ユーザ情報を取得  

        $user = Auth::user();  

        //自分のリストを取得  

        $routines = Auth::user()->routines()->get();  

        //ビュー表示  

        return view('routines.mypage', ['user' => $user, 'routines' => $routines]);  

    }  

    

bladeファイル側のヘッダー部分はこんな感じ。
v-bind

。

  

ログアウト用のルーティングも送ってあります。
これはvueファイル側で直接指定出来るんじゃないかな・・

これでheaderComponent.vueへ情報を送れましたので
受け取り側の設定をvueファイル側でします。

     export default {  

        name: "HeaderComponent",  

        data: function () {  

            return {  

          //省略  

            }  

        },  

        props: {  

       //ここで情報を受け取る  

            authcheck: Boolean,   

            logout: String,  

            user: Object  

        },  

        computed: {},  

        methods: {  

            //省略  

        }

後はv-ifを使って、authcheckの値次第で表示を変えるように
テンプレートを書けば完成。

    * __user:  
    
                        {{this.user['name']}} 


                
    * ||

                
    * [マイページ](</mypage>)

                
    * |

                
    * [履歴を見る](</histories>)

                
    * |

                
    * ログアウト
    
    
                    

            


       
            
                
    * [ログイン](</login>)

                
    * |

                
    * [新規登録](</register>)

            


        

今回ログアウト時にaxiosを使っています。
ログアウトした後トップページへ遷移するように書いたのですが
最初そのまま書いたら

ログアウトは出来ているけど表示がログイン時のヘッダーのまま

という事態が発生。

どうやらログアウト処理後一定時間をおいてから遷移しないと
いけなかったようです。
ということでsetTimeoutを設定 し、1秒後に遷移するようにしています。

   methods: {  

            //ログアウト機能  

            doLogout: function () {  

                //axios使ってlogoutをpost送信  

                axios.post(this.logout)  

                    .then(function (response) {  

                        console.log(response);  

                    })  

                    .catch(function (error) {  

                        console.log(error);  

                    });  

                //ログアウト後トップページへ遷移  

                //データ反映を待つので1000ms後  

                setTimeout(function () {  

                    window.location.href = '/'  

                }, 1000)  

                }  

            }  

        }

この
「axios使って非同期通信した後の処理は
setTimeout使って時間を置いてから発動させる」

パターンは、この後の処理で結構使いまくりました(*´ω`*)

フラッシュメッセージ実装

今回悩んだところその2。

vueでコンポーネント化するかどうか悩んだ結果
今回はlayout/app.blade.phpに直接書き込んだ上で
vueを使って1.2秒後に消えるようにしてみました。

また成功時とエラー時で表示を変えたかったので
2パターンのフラッシュメッセージを作成しています。

    @if (session('scc_message'))  

        


            __  



                

{{session('scc_message')}}


            


        


@endif  

    @if (session('err_message'))  

        


            __  



                

{{session('err_message')}}


            


        


@endif  

これをapp.jsで操作。
親のapp.jsに直接記入する形になり
保守性など考えるとvueコンポーネントにした方が良かったのかどうか
今でもちょっと悩んでいます。

data: {  

        isActive: true  

    },  

    props: {},  

    mounted: function () {  

        this.isActive = true  

        //マウント後1.2秒経ったらeraseMessageを呼ぶ  

        setTimeout(this.eraseMessage, 1200)  

    },  

    methods: {  

        //フラッシュメッセージを見えなくする  

        eraseMessage: function(){  

            this.isActive = false  

        }  

    }

最初、setTimeoutに直接メソッドを書いたら
「this.isActiveなんて無いがな!」と怒られましたw

そりゃそうだ、スコープ考えたらそこでは使えませんなww
てことで、methodに処理を書いたものを呼び出す 形に。
分かれば単純なことですが、これに2時間くらいかかってます・・

(ログイン関連機能、リスト実行以外のviewとvue機能実装 : 9時間)

リスト関連機能

リストの新規登録・編集・削除及びマイページに自分のリスト表示。
この辺はウェブカツで習ったことがそのまま使えたので
割とすんなり実装できました。

(リスト関連機能実装: 3時間半)

タスク実行準備〜完了までの機能

今回一番の難関であり、機能実装までロジックが決まらなかった部分です。
実際のコードはgithubに上げた物を最後にリンクしてありますので
そちらを見て頂くとして
今回は機能を実装するまでの大まかな流れを書いてみようと。

プロの方々がどういう思考手順を取っているかは分かりませんが
自分の場合、何かしら機能を付けるときには

  • やりたいことを詳細に書き出す
  • それを実装するためにどういう処理をすれば良さそうか考える
    (実装できるかどうかは置いといて)
  • 考えた物を元にググりながらコードを紙に書いておく
  • 実際にコードを書いて動くか答え合わせ

という手順を取ります。

今回の場合だとこんな感じ。

やりたいこと

  • 実行準備画面で、タスクの順番をドラッグ&ドロップで入れ替える
  • 実行準備画面で、タスクの順番をボタン一つでランダムに入れ替える
  • 実行ボタンを押すと、タスクが入れ替えた順に実行中画面で一つずつ表示される
  • 終わったボタンを押すと次のタスクが表示される
  • 中断ボタンを押すと中断してマイページに
  • 全てのタスクを終えて終わったボタンを押すと完了画面へ

考えた処理

  • 実行準備画面で表示されるタスクはroutinesテーブルから引っ張ってくる
  • そこからタスク順だけを抜き出した配列データを作る
  • 順番を入れ替えるとこのデータが連動するようにする
  • 実行ボタンを押すと、タスクだけの配列データとroutinesテーブルから引っ張ってきたリストタイトルをPOSTで送り、historiesテーブルへ新規保存
  • この時一緒に開始時刻も保存し、実行中画面へ遷移する
  • 実行中画面ではさっき保存したばかりのhistoriesテーブルのデータを使ってタスクを順番に表示
  • 中断ボタンを押すと、終了時刻とcompletion
    (更新)後マイページへ遷移
  • 全てのタスクを終えて終わったボタンを押すと、終了時刻とcompletion
    (更新)後完了ページへ遷移

結構細かく分けて書きます。
とにかく分解して一つの単位を小さくしていくと
案外簡単に処理を思いつくことが多いためです。

これを元にググったり本を見たりして
実際にどういうコードを書けば良いか調べていきます。

調べ物は本とスマホを使って横になって書いてるので
文字はいつも以上に乱雑ですww

時間的には5時間で実装とそれなりだったのですが
そこまでにググってる時間と参考にとひとりslackにメモった
アドレスの多さは結構なものでした。

診断テストを作ったときにも感じましたが
一見してこれ本当に出来るのか・・?と思うものも
内容を詳細に分解して把握するとすんなり実装できることが多いので
ぱっと見で出来ない!って諦めるのはもったいないなぁと。

そこで怯まずに冷静に分析していくのが大事ですね。
プログラミングに限らずの話だと思いますが。

・・と言うことを子供にも言うんだけど
中々分かっちゃくれないんだよなぁ(´・ω・`)

ドラッグ&ドロップでリスト順を並び替える

今回の悩みどころその3。

これはVue.Draggable を利用しました。

https://github.com/SortableJS/Vue.Draggable

で、これを使うところまでは良かったのですが
問題はどうやってこれで変えた順番をPOST送信で保存するの?の部分。

まずは準備画面表示時にvueコンポーネントへ
表示するリストのデータを渡します。

データを受け取る側はこんな感じ。
created フックを使い、受け取ったデータから
タスクだけが入った配列を作ります。

components: {  

            draggable,  

        },  

        data: function () {  

            return {  

                tasks: [], //加工したタスクだけ入った配列入れ  

            }  

        },  

 props: {  

            routine: Object,  

        },  

        created: function () {  

            //routinesテーブルからタスクのみの配列を作る  

            for (let i = 0; i <= 9; i++) {  

                let order = 'task' + i  

                if (this.routine) {  

                    this.tasks.splice(i, 0, this.routine)  

                } else {  

                    //タスクがnullの時は配列に入れない  

                    continue  

                }  

            }  

        },

これをv-for 使ってリストで表示。
ドラッグ&ドロップ出来る様にタグで囲むのですが
この時にタグに v-moder=“tasks”と
先ほどの配列とデータが双方向で連動するようにしておきます。

    * 
                                    {{item}}  
    
                                

                        
                    

                

これで順番が変わるとdata側のタスクの並びも変わるので
後はそれをaxios使ってPOST送信してあげればok。

このv-model使えばいける!にたどり着くまでにかなり時間がかかりました。
今回一番のアハ体験はここだと思うw

(タスク実行関連機能実装: 5時間)

Twitterログイン機能

Twitterログインに関してはこちらの記事をそのまま使わせて頂きました。
既にTwitterAPIは使ったことがあったのですんなり。

https://php-junkie.net/framework/laravel/socialite/

(Twitterログイン実装: 1h)

その他

問い合わせフォームはこちらを参考に実装。

https://www.bnote.net/blog/laravel_contact_form.html

その他、利用規約とかプライバシーポリシーとかを作って
手動でテストして完了としました。

(その他作業: 2時間)

デプロイ

これまでデプロイはレンタルサーバー(ヘテムル)を利用していましたが
ヘテムルだとLaravel5.8は動作要件を満たしてないようなので
思い切ってVPSを利用する事に。

使ったのはさくらVPS です。
初心者向けの初期設定が非常に充実していて、本当に助かりました(*´ω`*)

https://knowledge.sakura.ad.jp/serialization/understood-cats-vps/

大体この記事で初期設定は間に合いますが
ssh鍵の設定はこちらを参考にした方がいいです。

https://qiita.com/uhooi/items/137de4578534c8e7e7f2

今回はムームードメインを利用したので
ドメインの設定はこちらを参考にしました。

[