以前窓の杜にてこのような記事が話題となりました。
無駄な会議のコストを可視化する秀逸なツールが……なぜか「Excel VBA」で登場
https://forest.watch.impress.co.jp/docs/serial/yajiuma/1252316.html
ぜひともWeb版が欲しいと思い、勢いでダーっと開発したものがこちら、「Meeting is Money」というサービスです!
Meeting is Money
https://meeting-is-money.web.app
GitHubでもソースを公開しています。
https://github.com/kuluna/MeetingIsMoney
Webアプリの開発は本当に久々(3年ぐらい)だったのでいろいろ思い出しながら技術的なところと良かったところや所感を述べたいと思います。
技術選定
- データベース Firebase Firestore
- ホスティング Firebase Hosting
- バックエンド Firebase Functions
- フロントエンド Angular9
- デザインフレームワーク BootStrap4.5
選定理由
Firebase
今更1から紹介するまでもないアプリ開発の強い味方、バックエンド周りのあらゆるユースケースをほとんどサポートしてくれるFirebaseを使いました。今回Firebaseのサービスで使ったのはFirestore、Hosting、Functionsの3つです。
Firestore
FirestoreはドキュメントベースのデータベースでMySQLのようなRDBとは少し違うデータベースとなっています。ColumnとRowを使うのではなくJSONをデータベースのよう扱えるものとして考えるとわかりやすいと思います。
Firebaseにはもう1つRealtime Databaseというものあるのですが、こちらは1つの大きなJSONをDBとして扱うのに対し、Firestoreは小さなJSONを階層構造で管理するといった雰囲気です。とりあえず始めるならFirestoreのほうが良いと思います。
Firestoreには値の変化をリアルタイムに受け取れる仕組みがあるのでトップページと会議ページに採用してみました。ずっと開き続けていると値が変わったタイミングでニュッっと表示が切り替わるのは見ていて楽しいです。
Hosting
Hosting自体は特筆して何かあるわけではないですが、無料ながらバージョン管理ができたりSPAモード(どんなパスからアクセスしてもまずルートのindex.htmlにリダイレクトしてくれる)があったりと使いやすくまとまっている印象です。
Functions
後述するセキュリティが主な理由でFirebase Functionsも使うことにしました。FunctionsはHTTPリクエストで発火するトリガーとFirebaseのサービスで発火するトリガーの2つが作れます。今回はどちらも使うことにしました。HTTPのほうは会議の作成や開始に、後者はFirestoreに書き込みがあったのをトリガーに合計のサマリーを計算するのに使いました。
上の画像のようにトップページに全ミーティングの合計が表示されるのですが、これを表示する専用のFirestoreドキュメントを1つ用意して参照するようにしています。というのもFirestoreの課金体系は読み/書き回数だからです。つまり集計のために10件のドキュメントにアクセスすれば10回カウントされてしまうのです。Firestoreの無料プランでは読み取りが50,000回/1日とかなり良心的な設定ですが、うっかりすると集計だけでかなりの数を稼いでしまいます。そのため都度集計するのではなく集計済みのSummaryドキュメントを用意し、会議終了のトリガータイミングでトランザクションをかけ結果を加算するということを行っています。再計算ではなくて加算がポイントです。
Firestoreのトランザクションとは「トランザクション中に別の箇所から書き込みがあった場合、別の箇所のほうはトランザクション終了後にもう一度再試行する」という動きになっているようです。またトランザクションは必ず読み取ってから書き込みをしなければならないというルールは意外と落とし穴だと思います。読み取りから再試行させるのでトランザクションを確保しているというやり方ですね。
export const calcSummary = functions.firestore.document('meet/{id}').onUpdate((change, context) => {
const newData = change.after.data() as Meet;
if (newData.actualEnd !== undefined) { // 会議終了の時だけここに値が入る
const summaryRef = db.doc('total/summary');
return db.runTransaction(async t => { // トランザクション開始
const total = (await t.get(summaryRef)).data() as Total; // まず読み取り
total.meetings += 1;
total.amounts += newData.total;
total.times += Math.ceil((newData.actualEnd!.toMillis() - newData.actualStart!.toMillis()) / 1000 / 60);
return t.update(summaryRef, { // その後書き込み
amounts: total.amounts,
meetings: total.meetings,
times: total.times
});
});
}
return;
});
Functionsは基本CORSの設定がされてませんが、Functions内でonCall関数を使う場合はCORSを気にしなくても良いらしいです。gRPCを内部で使ってるのでしょうか。余談ですがFunctionsもプロジェクト作成時にTypeScriptが選べるのが嬉しかったです。Functionsだと型定義する前に実装書いたほうが良い場面のほうが多いですが、ざっと書ききったあとのリファクタリング環境としてはやはり型がないと不安です。
export const generateMeeting = functions.https.onCall(async (data, context) => {
const body = data as MeetForm;
// まずは新規ドキュメント作成
const newMeet = db.collection('meet').doc();
await newMeet.set(convertModel(newMeet.id, body));
// IDを返す
return newMeet.id;
});
Angular
今の現場ではなく前の前の現場で少しだけ使ったことがある程度なのですが、そもそもWeb開発自体が3年ぶりなものに対し昨今のWebの進化はかなりの速度なため、3年前の知識からモダンな開発に追いつけるかが不安でした。
結論から言うとその心配は全くありませんでした。
3年ほど前のAngularのバージョンは4.x.xぐらいで、現在は9.x.xです。メジャーバージョンが5つも飛んでいるのですが、驚くほど破壊的変更はありませんでした。どちらかというとAngularが依存しているrxjsが5から6になったことによる破壊的変更のほうが強かった印象です。
rxjs6からはメソッドチェーンではなくpipe()関数の中でチェーンをつなぐようになりました。またネームスペースも変わったようです。この辺りは書いて覚えろってことですよね。
// rxjs5
this.route.params
.map(params => params['id'])
.do(id => this.id = id)
.mergeMap(id => this.getMeeting(id))
).subscribe(meeting => {
// hogehuga
});
// rxjs6
import { map, mergeMap, tap } from 'rxjs/operators';
this.route.params.pipe(
map(params => params['id']),
tap(id => this.id = id),
mergeMap(id => this.getMeeting(id))
).subscribe(meeting => {
// hogehuga
});
Webという進化が激しい環境の中でAngularというインターフェースが変わらないことの安心感はjQueryにも似たものがありました。データバインディングからルーティング、通信周りまでほとんど変わりはありませんでした。もちろん開発言語もAngular2からずっとTypeScriptのままです。
調べてみるとAngularも6, 7あたりで大きな変更があったようです。ですがAngularを使ってアプリ開発をする人向けへのインターフェース(Angular構文)は私が使った範囲では何一つ変わっていませんでした。つまりAngularという外側のインターフェースはそのままに内部だけを大きく変更したということになります。
Angular-CLIを使ってプロジェクト管理をするとwebpackの設定ファイルすらありません。以前いろんなブログでWebpack4になったことによる破壊的変更で阿鼻叫喚の嵐だったように見え、現場で使わない技術に対してこんな激しい波に私はついていけないなとその時感じました。Webpackの設定もCLIが内包しているので私たちはWebpackの使い方を知らなくても現在のベストプラクティスに基づいたプロダクションビルドが出来るように作られています。これこそが開発者が本当に欲しかったもので、私たちはWebpackのコンフィグをいじりたいのではなく、アプリの開発をしたいという本質にフォーカスできるのは本当に良いフレームワークだと感じます。このあたりはvue.jsもvue-cli3からwebpackをブラックボックス化したようですので、いい流れだと思います。
一方で悪いところも悪いままだなという印象です。やはりapp.module.tsのメンテナンス性の悪さはもう少しなんとかしてほしいと感じました。Angular構文は覚えられれば書けるものの、覚えるまでが大変というところも変わりなさそうです。
Bootstrap
デザインセンスのないエンジニアにとって神の存在であるBootstrap先輩に今回もお世話になりました。最近ではGoogleのマテリアルデザインというのもあるのですが、Bootstrapのほうが個人開発感が出るかなと思い逆にチープさを表に出すようにしました。
ところで個人開発で悩みの種の一つが画像やアイコンといったアセットです。これはライセンスも絡むので本当に気をつけなければならないものです。トップページをみると画像がたくさんあるように見えますが、これ全て絵文字です。絵文字もフォントサイズをここまで大きくすれば画像のように見え...なくもないかなと。とはいえ個人開発でもここまでアセットを雑にするのはもうちょっと頑張れと言わざるを得ないですね...。
セキュリティ
個人開発においてセキュリティは2の次3の次となったり、やりたくてもわからないといったことは多いと思います。私もそれほど詳しいわけではないので次の優先度で対策を講じることにしました。
- 他人に迷惑をかけないこと(他人のサーバー負荷を上げる・誰かのデータを消せる・ユーザーに不利益が出る)
まず外部のURLには極力アクセスしないようにしました。外部URL経由でよくないスクリプトがロードされてしまったり、無限ループバグを作ってしまった時にDOS攻撃のようなことがないよう気をつけました。結果外部サービスはFirebaseのみ、外部URL参照はBootstrapのみとしました。 - 自分に迷惑がかからないこと(サービス費用が青天井になる)
最悪自分にだけ迷惑がかかる分には特に問題はないので、考えられるところは事前に対策を考えるようにしました。Firebaseは有料プランにしない、Firestoreへのアクセス回数を減らす、APIキーは漏れても問題ないか調べるなどです。
Firebaseのセキュリティについてですが、今回Meeting is MoneyはGitHubにFirebaseのAPIキーを載せたままPushしています。いろいろ調べた結果APIキーが公開されても問題はないようにできることがわかりました。ここでいう「できる」とはFireStoreやRealtime Databaseへのアクセスはセキュリティルールによって制限を行うものという意味で、これを使わないのであればAPIキーはあってないようなものですし、逆に使うのであればセキュリティルールをしっかりしないと酷い目にあうぞということです。
参考: Firebase apiKey ってさらしていいの? ほんとに?
https://qiita.com/hoshymo/items/e9c14ed157200b36eaa5
今回のMeeting is Moneyでは読み込みは誰でもできるが、書き込みはFunctions経由でないとできないようにし、Functionsの中でバリデーションを行うような構図にしました。これで不正な書き込みリクエストがされるとすればFunctionsのバリデーションが甘いということになるので、私はFunctionsだけ気をつければ良いということになります。これによって個人開発で気をつけなければいけない領域を少なくすることができます。
まとめ
Firebase + 馴染みのあるフロント技術 であれば3年ほどのブランクがある人でも個人開発は難なくやっていけるということが改めてわかってよかったです。
特に本業ではない領域の技術を学ぶ時はその技術がどれだけ長生きするかという物差しも1つあると良いかもと思いました。