使用技術
- Angular9
- AngularMaterial
- Firebase (Auth, Database, Storage, Functions)
- Algolia
- Stripe
- GitHub Actions
アプリの概要
どんなアプリ?
URLがたくさん貼られたNAVERまとめのような記事が、シンプルなUIで作れます。
ページ紹介
トップページ
ボタンをイラストで飾ることでAngularMaterialのUIキット感を抑えています。
未ログインの場合は、ログインを促すダイアログを表示されます。
記事の作成
アイコンボタンでURLの入力欄を増やしたり、減らしたり出来ます。
各記事ページ下部から編集や削除も行えます。
また、入力されたURLから、自作のAPIによってOGPとFaviconを取得します。
記事の一覧
「自分の投稿」と「いいねが多い投稿」を一覧表示出来ます。
いずれのページも無限スクロールを用いた追加読み込みで、1度に読み込む記事の数を抑えています。
【アプリ開発の進捗】
— お茶マル🍵 (@uotya) January 31, 2020
無限スクロールを実装しました😎
実装方法としては、最下部までスクロールされるたびに配列に記事のデータを追加して、それらを全て表示するイメージ。これで1000件投稿があっても表示出来ますね!#to_camp 52日目 pic.twitter.com/F97tEEljkj
記事の検索
Firestoreには全文検索の機能が無いので、公式ドキュメントでも推奨されている外部サービスのAlgoliaと連携することで実装しています。
開発者へ寄付
Stripeを使って課金機能も実装しました。
顧客データをStripe側に保管しているので、2回目以降の決済にはカード情報の登録は不要です。
その他の機能
- PWA対応
- TwitterAPIでアバターの自動同期
- アバターの変更 (トリミング含む)
- データベースの定期バックアップ
- ダイナミックレンダリングによるOGP対応
- アカウントの削除
ディレクトリ構成
この画像だけでは分かりづらいとは思うのですが、ログイン・CRUD・検索・課金を実装したアプリのファイル規模の参考程度にはなるかと思います。
- モジュールはページ単位で作成
- DRY原則に則ってミニマムなコンポーネントを複数箇所で使い回す
- 複数のModuleで重複したインポートをしない
苦労した実装
開発に着手したのは12月上旬ですが、当時はHTMLとSassとJavaScript(Progateでかじった程度)の知識しかありませんでした。
AngularとFirebaseをキャッチアップしながら開発したので、もちろん詰まる点ばかりで、しばらくは暗黒の森を彷徨うような気持ちで開発していました。
その中でも、特に苦戦した実装をピックアップしました。
データベース設計
画像のように、記事のドキュメントの中にユーザー名やアバターのURLを含めてしまうと、名前やアバターが変更された際にそのユーザーが投稿したすべての記事を更新しなければなりません。
Firestoreのリクエスト数を減らすためにも「記事」と「ユーザー」のデータは別々のコレクションで管理することにしました。
保存先を分けることはAngularFireの基本が理解出来ていれば、何ら難しいことではないのですが、別コレクションにある「記事のデータ」と「ユーザーのデータ」を取得して1つのObservableに統合するにはRxJSの解像度の高い理解が必須だったため、苦戦しました。
ちなみにこの実装は、同じように詰まった人のためにこちらの記事で解説しています。
いいね機能
いいねは想定外の不具合に見舞われたこともあり、実装手順を理解してから、実際に実装が終わるまで1番長かったです。
大まかな処理フローは以下のような感じです。具体的に詰まった点を紹介しておきます。
- フロントで表示用の変数をカウントアップorダウン
- Serviceでいいねを押した記事のIDを保存or削除
- CloudFunctionsで記事データ内のいいね数をカウントアップorダウン
記事データ内にいいね数のフィールドを持たせているのに、フロントでも表示用の変数を使う理由としては、Functionsを伴う処理は数秒のラグが生じるためです。
べき等処理
FunctionsでFirestoreトリガーを使うと、ごく稀に2回以上発火してしまうことがあり、公式ドキュメントでもべき等処理を実装することが推奨されています。
カウントアップorダウンが2回以上発火されるといいね数が正確性が損なわれるので、いいねにはべき等処理を実装しました。
一覧表示をする際の不具合
いいね数をフロントの変数で管理するため、記事のデータは常に最新のものをキャッチし続けるのではなく、take(1)で一度だけ取得する実装にしました。
その際にこの手順を踏むと、キャッシュの影響で1記事しか一覧表示されないことがありました。
- 特定のドキュメントを取得する
- コレクションを取得して一覧表示する
対処法についてはAngularFireのIssueで議論されており、以下のようにgetメソッドを用いてサーバーから直接データを取得する方法で対処しました。
collection('users').get({ source: 'server' });
さいごに
今回のポートフォリオの制作を通じて、数日でCRUDアプリが作れるくらいの自力が付きました。
また、日本語情報の少ないAngularで開発したことによって、英語のドキュメントやIssueに対する抵抗感もさっぱり無くなりました。
挫折せずに最後まで楽しくやり切れたのは、間違いなく付きっきりで教えてくれたメンターのNinoさんのおかげです。
リポジトリのCollaboratorとして、コードレビューもして頂きながら開発しました。
本当にありがとうございました🙇♂️