時間でいうと25hで設計・実装・デプロイまでできたのでメモとして残します
完成gif
- こんな感じで図書を管理する上で必要となるであろう各機能を実装しました。
- URL載せていたのですがアクセス増えて怖くなったのでURLは消しました。。
- 執筆する中でバグをチラホラ見つけてしまいましたが、一旦書き切ります・・・
モバイルもホラ(レイアウトは考慮が必要)
経緯(動機)
- 弊社では業務で使用する図書を経費購入できる制度があることもあり、社内図書がかなり溜まってきていました。
- 図書の管理は、現状スプレッドシートで管理しており、これが特に破綻していると言うことはなかったのですが、
モバイルから触りたいなぁ
という個人的な都合がありました。 - そこで簡単な図書管理アプリケーションのデモを作ってみようと思い立ちました。
- で、自分的に手応えがあったらこれでいきませんかと提案してみよう・・・
技術選定
-
モバイルから触りたい
要求を考えるとNuxtのPWA(プログレッシブWebアプリ)モードが一番早そう
だと思いました。(モバイルアプリ風にホーム画面にも追加できるし) - 結果、Nuxt.js(PWAモード) + firebaseにしました。理由は前述した要件に加えて
Nuxtに慣れていたこと
やUIコンポーネントが豊富
、とかcreate-nuxt-appでセットアップが早い
とかありましたが一番はfirebaseの各機能で開発スピードを上げたかった
からです(使ったことないけど)。 - 結果的に3日で動くものができたので選定自体は間違ってなかったと思いました。
- 最終的には主に以下の技術を使って作成しました
使用技術
- Nuxt.js:2.11.1
- TypeScript:3.5.3
- firebase
- firebase Authentication
- Firebase Cloud firestore(DB)
- Firebase cloud functions
- ※デプロイはどうしようかと考えましたが
Nuxt.js を Express のミドルウェアとして動作させてみよう
と思いfirebase cloud functions でデプロイしてみることにしました。
使用技術を何に使っているのか
- 画面
- Nuxt.js を用いて PWAとSSR(サーバサイドレンダリング) を実現
- TypeScript を用いて静的型付け(オートコンプリート機能他)
- サーバサイド・DB・認証
- firebase Cloud functions
- デプロイに使用
- firebase Cloud firestore
- 今回データの永続化の方法として
Firebase Realtime Database
を使用しています。こちらのデータベースはNoSQL (非リレーショナル) データベース
という方式を採用しており、様々なメリデメがありますが、今回のような小規模開発には向いていると思い採用しました。RDBMS
との違いは以下などを参照ください。 - RDBMS と NoSQL を徹底比較!特徴からそれぞれのメリット・デメリットまで、わかりやすく解説!
- 今回データの永続化の方法として
- firebase Authentication
- 認証
#設計
- とてもシンプルな構成なので、設計は頭のなかでやりました。(通勤時間:30*5=2.5h)
- 貸出・返却のフローを考慮すると
slackに通知送ったりするのも必要?
とか考えましたが、デモということで一旦CRUDが確認できる最低限の以下の機能を実装することにしました。
機能(とりあえずCRUDできる最低限)
- ログイン(かんたんログイン含む)
- ユーザ作成
- 図書一覧
- 文字列検索
- 貸出・返却申請
- 新規図書登録
開発工数
- 設計
- 通勤電車の中で 2.5h(0.5h * 5 日)
- 実装
- 20h
- テスト・デプロイ(次回はJest使ってE2Eテストやりたい)
- 機能に対する打鍵テストのみ:1h
- cloud functions を用いたデプロイ:1.5h
メッセージについて
-
画面にて表示するメッセージは
nuxt-i18n
を使用してlocales/各言語.json
に格納されているファイルを参照して出力するよう工夫しました。 -
実はこれについては昔Qiitaに執筆していましたNuxt.jsでメッセージの外出し&国際化対応
国際化 -
使うときのソースだけ貼ります
hoge.vue
import { i18n } from '~/plugins/nuxt-i18n'
this.message = i18n.tc('error.E-0006')
this.messageFlg = true
- 引数を入れたい時のメッセージ用に mixin を作成して対応(common-message.ts)
hoge.vue(引数を入れたい場合)
import { i18n } from '~/plugins/nuxt-i18n'
import CommonMessage from '~/mixins/common-message'
this.message = this.editMessage(i18n.tc('info.I-0003'), [
'ユーザ登録',
'ログイン'
])
デプロイ
- SSRモードなのでFirebase hostingだけだと動きません。
- リアルタイムリクエストに対してHTMLを返却するサーバを用意する必要があります。
- Firebase hosting ではサーバーはたてられないのでcloud functions を追加で使用します
- cloud functoinsは実行環境として
NodeJS
が用意されているためブラウザからのリクエストをcloud functions に集め、そのリクエストに応じたHTML を返却するサー バーを立てることでNuxt でのSSR 対応Web サービスを実現します
フロー
- 1.ブラウザからのhttpリクエストはすべてcloud functions に集めます。
- 2.cloud functions 側で動いているサーバーからNuxt によって生成されたHTML を返却します。
- 3.返却後のHTML のリクエスト(静的ファイル)は、firebase hosting によってhosting しておきます。たとえば、アプリ上で使用している画像やJS ファイルなどです
デプロイ準備としてビルド用のシェルを作り実行
- 後述のfunctionsの実装で使用するnuxtプロジェクトをビルドするシェルを作成しました。
- 後々CIに組み込む予定です。
build.sh
# 前回ビルドで作成されたフォルダ群を一度全て削除
rm -rf .nuxt public functions/nuxt
# publicフォルダを作成
mkdir public
# nuxtをbuildする
npm run build
# 各種フォルダのコピー
cp -R .nuxt functions/nuxt
cp -R .nuxt/dist/client public/assets
# MEMO:読み込みたい画像があればapp/staticもコピーする
# cp -R app/static/* public/assets
-
sh build.sh
で実行します
$ sh build.sh
functions実装
- functionsの初期化(省略)
- サーバサイドのコードを書く(これだけTypeScript化できていません。。。)
-
require('nuxt')
部分は↑でビルドしたNuxtプロジェクトを読み込んでいます - あとはexpress のミドルウェア関数としてnuxt.render を登録することで、nuxt のレンダリングしたhtml を返却するようにしてます
functions/index.js
const functions = require('firebase-functions')
const express = require('express')
const { Nuxt } = require('nuxt') // ビルドしたnuxtプロジェクト
const app = express()
const config = {
dev: false,
buildDir: 'nuxt',
publicPath: '/assets/'
}
const nuxt = new Nuxt(config)
app.use(async (req, res, next) => {
await nuxt.ready()
nuxt.render(req, res, next)
})
exports.ssr = functions.https.onRequest(app)
-
firebase deploy --project nuxt-library
でデプロイ完了です。
$ firebase deploy --project nuxt-library
良かったこと
- TypeScriptのオートコンプリート(メソッド名とかを補完してくれるやつ)はフロントエンド開発のストレスをかなり軽減してくれました。
- firebase Authenticationは非常に簡単に認証機能を作れたので採用して良かったです。
- Nuxtは業務で触っていたので苦労なく実装できましたが、デプロイがとても勉強になりました。
難しかったこと
- ログイン情報の永続化はベストプラクティスが確立されていないようで迷いました。
vuex-persistedstate を使い実現させました。(以下を多分に参考にさせていただきました)nuxt.js を使う時に localStorage で store を永続化する- 紆余曲折あり
universal-cookie
を使い cookie に保存することにしました。 -
src\store\authenticate\actions.ts
にてログイン時に cookie に保存。 -
src\middleware\auth-cookie.ts
にて cookie から取得してます。 - NoSQLは初めましてでしたが、中・大規模のアプリケーションのメインサーバではなかなか使わないんじゃないかな?と思いました。