Vue.js
Webサービス
Firebase
個人開発
nuxt.js

Nuxtで毎日やりたいことを習慣づけるWebアプリ「コツコツ忍者」を作った🏃‍♀️【個人開発】

お久しぶりです。

以前、Nuxt.jsとFirebaseでchocottoというTwitterでお菓子と一緒にメッセージを送れるサービスを作ったジフォです。

先週、二十歳を迎えて成人しました🎉

今は高専五年で、編入試験を受けている真っ只中です。

適度にお酒を入れて、今後も個人開発頑張っていこうと思います!

さて、今回作ったWebサービスは「コツコツ忍者」というWebアプリです。

昔ばなしに、忍者は跳躍力向上のために毎日成長していく小さな木を飛び続けた、というお話があるのをご存知ですか?

ある能力を向上させたいのであれば毎日継続してやり続けることが大事である、ということですね。

自分が好きなことは続けられますが、好きではないけど上達したいことは続かないからなかなか上達しません。

そんな「上達したいけど続けられない」ことがある人のために、毎日やったことを記録できるWebアプリを作りました。

PWAなのでデバイスにインストールできるため、アプリを開くハードルも下がって毎日記録できるかなーと思います。

この記事では、コツコツ忍者で使った技術を紹介したいと思います。


コツコツ忍者で何ができる?

コツコツ忍者は、毎日のやった記録を簡単に付けられるWebアプリです。

一日にやる量とやる曜日を決めて「やること」を作ったら、あとはやったその日に「今日もやった」ボタンを押すだけで、何日連続でやっているかや通算でどのくらいやったかなどのデータを計算してくれます。

また、最大四週間前までのログを基にカレンダー式のログチャート(正式名称を知らない)も生成します。(やった量によって芝の色が変わります。)

ninja_task_log_chart.png

やったことを記録すると出てくるモーダルからシェアすると、OGP画像を生成して設定してくれます。

シェアするときに付いてくるURLに細工をしたので、飛ぶページは一緒だけどツイートによって表示されるOGP画像が変わります。この方法の説明はまた後ほど。

ninja_task_share.jpg

毎日やることに慣れてきたら、忍者の小さな木ように量を増やしてみるのもおすすめです。

今からでも腕立て腹筋を毎日やって夏の準備をしたいなどの毎日やり続けたいことがある方、コツコツ忍者ですぐに始めましょう!

https://ninja.g4rds.dev


コツコツ忍者で使った技術

コツコツ忍者はどういった技術に支えられているか、フロントエンドバックエンドともに紹介します。


Nuxt.js

nuxtjs-typo.png

コツコツ忍者はNuxt.jsで作られています。

今回のようなOGPを設定する必要があるWebサービスの場合、SSRは必須となってくるためVue.jsではなくNuxt.jsを選ぶ必要があります。

静的ページ生成機能もすごく良いので、ブログとか作るときにもおすすめです。(Vue系だけでもいろんな選択肢があるので色々比較してみてください。Gridsomeとか。)


Firebase / Google Cloud

Firebase_Logo_Standard_Lockup.png

コツコツ忍者はバックエンドにFirebaseを使っています。



  • Firebase Authentication

    ユーザー認証ができます。コツコツ忍者ではGoogleログインとTwitterログインを有効化しています。


  • Firestore

    ドキュメントデータベースです。Nuxt.jsでリアルタイム同期をする場合、サーバーサイドでオブザーバーを取得しないよう気を付ける必要があります。


  • Cloud Storage

    オブジェクトストレージです。生成したOGP画像を格納しています。


  • Firebase Performance Monitoring

    先日のGoogle I/O 19でWebのサポートが発表された、アプリのパフォーマンス計測をしてくれるサービスです。初回ペイントまで何秒かかっているかなどの数値を表示してくれます。

logo_lockup_cloud_rgb.png

NuxtのホスティングはGAEを使ってます。

公式ドキュメントではF2インスタンスが推奨されていますが、無料枠に収めたいのでF1にしてます。


CircleCI

circle-logo-horizontal-black.png

CircleCIは、ビルド・デプロイを自動でやってくれるCI/CDサービスです。

連携したGitHubリポジトリにコミットをプッシュすると、CircleCIでコンテナが立ち上がり、事前に定めておいた手順に沿ってビルド・デプロイしてくれます。

GAEへのデプロイは数分かかるので、masterにプッシュしてあげるだけで裏で勝手に更新してくれるというのは非常に便利で快適です。

セットアップの数分を使うか、何度も行うビルド・デプロイ作業に時間を費やすか。

コマンドでデプロイできるやつは簡単にセットアップできるので、おすすめです。しかも無料。


Tailwind CSS

tailwind.png

Tailwind CSSは先日ついにv1.0になりましたが破壊的変更が入っているため、コツコツ忍者ではアップデートせずにそのままv0.7を使っています。

TailwindはユーティリティファーストなCSSフレームワークで、コンポーネントは入っていません。

一度しか使わないクラスを作るのではなく、直接マークアップにスタイルを書くように用意されたクラスを追加することで、レスポンシブデザインを実装できるのが強みです。

ドキュメントも豊富で使いやすいので、英語が読めなくても何となく理解できると思います。


html2canvas

クライアント側でOGP画像を生成するときに使っています。


FontAwesome

値上げするよーの脅しにつられてProに課金してしまったので、コツコツ忍者にはFontAwesome Pro限定のアイコンがちりばめられています。

便利ですし、さらなるアイコンタイプ(duotone)の追加も予定されていますので、これからもお世話になります。


unDraw

undraw_code_typing_7jnv.jpg

unDrawはおしゃれなイラストを全部無料で商用利用できるサイトです。

イラストはコンテンツの量増しになるので、寂しく感じたら使うようにしてます。


OGP画像の生成、動的適用

コツコツ忍者では、やることを完了する度に生成される画像が異なるため、一つのページに複数のOGP画像を割り当てる必要があります。

Twitterなどのクローラは同じURLに対しては一週間ほど画像をキャッシュするため、違うURLにする必要があります。

そこで、やることの固有IDと、画像を生成した時のタイムスタンプを組み合わせて、それぞれの画像に一意なIDを割り振りました。

html2canvasで生成した画像をCloud Storageにアップロードする際、ファイル名を「YOyGuKC5gNdCr4NtW5oj_1560067092042.png」などのようにしてアップロードします。

画像のダウンロードURLを取得して、Firestoreのやることのドキュメントに、タイムスタンプからURLを取得できるようなマップを入れておきます。

{

"1560067092042": "https://firebasestorage.googleapis.com/hogehoge",
...
}

シェアするURLのクエリにタイムスタンプを設定しておけば、Twitterから「ninja.g4rds.dev/tasks/YOyGuKC5gNdCr4NtW5oj/?ogp=1560067092042」にアクセスされるので、NuxtのasyncDataでクエリのタイムスタンプを使ってドキュメントから画像URLを取ってきて、metaのogp:imageにそのURLを設定することで、ツイートによって違う画像が表示されるようにしています。

追記 (19-06-12 00:08)

ここで紹介している方法は画像をクライアントサイドで生成し、サーバーにアップロードするものです。

この場合、悪意のある第二者によって不正な画像をアップロードしOGP画像として適用することが可能となってしまいます。(Cloud Storageへアップロードするデータを置き換える、ドキュメントに登録する画像URLを書き換えるなど。)

これを良しとしない場合はサーバーサイドで画像の生成、保存、設定まで全て行う必要があります。

前回開発したchocottoはサーバーサイドでレンダリングしていますので、詳しくはchocottoの開発記事をご覧ください。


NuxtでのFirebase Authentication

SSRするNuxtではFirebase Authenticationで気を付けなければならないポイントがあります。

今度別の記事に詳しくまとめようと思いますが、この記事にはコツコツ忍者のログインフローを簡単に書いておこうと思います。


  1. ログインページ(/login)のログインボタンがクリックされる

  2. LocalStorageにログイン処理中であるサインを置く

  3. signInWithRedirectを呼ぶ(signInWithPopupはモバイル端末で動作しないことが多いです)

  4. ユーザーがプロバイダーのページでログインする

  5. ログインページ(/login)にリダイレクトしてくるので、mountedでgetRedirectResultを使ってログイン処理を完了する

ポイントとなるのはLocalStorageにサインを置いておくことです。(あ、サインはわかればなんでもいいです。{"loggingin":"true"}とか。)

/loginのmountedでLocalStorageを確認し、サインがあったら5の処理を行います。

getRedirectResultの処理に数秒かかる場合が多いので、「ログイン処理中だから待ってねー」と表示しなくてはなりません。

プロバイダーからリダイレクトしてきたとき以外は表示したくないので、それがわかるようにサインを残しておくということです。

また、コードがどこで実行されるのかということにも気を付ける必要があります。

LocalStorageはクライアントサイドでしか利用できません。

そのため、ログイン処理はすべてクライアントで実行するようにします。

created、asyncData、fetchはNuxtクライアントがルーティングしたとき以外はサーバーサイドで実行されるので、mountedで処理を行いました。


まとめ

自分がやってきた記録が数字とビジュアルで分かるというのはモチベーション維持にすごく効果的だと思います。

「毎日何かを勉強する」など、毎日継続してやっていることや、やりたいことがあったら、使っていただけると嬉しいです。

https://ninja.g4rds.dev

これからはしばらくFlutterを触ってみようかなと思います。

個人開発でWebサービスを収益化するには広告くらいしかなく、他のハードルが高いのに辛さを感じていましたが、アプリであれば簡単にサブスクでさえ作れるのが魅力的です。

Flutterは一つのソースでiOSもAndroidもビルドできるのがすごく良いですよね。

何か作ったらまた紹介させてください!

さて、余談ですが、今週の水木土に幕張メッセで開催されるAWS Summitに僕も参加する予定です。

初めてこういうイベントに行くのですごく楽しみです。

Twitterで会場の雰囲気とか感想とかつぶやこうと思いますので、よかったらフォローしてください。