こんにちは、 2018年モバイルファクトリーアドベントカレンダー 12/7担当の id:yunagi_n です。
はじめに
個人的な趣味で、 Firebase と Nuxt.js でブログを作っています。
そのことについて、いろいろ話します。
なお、会社で開発しているアプリ・プロジェクトとは一切関係ありません。
前提
以下の環境で開発、動作確認を行っています。
- Firebase (Blaze プラン)
- Node.js 8.12.0
- Windows Subsystem for Linux (WSL) Ubuntu 16.04
- Nuxt.js 2.3.4
- TypeScript 3.2.1
- Firebase と Nuxt.js のプロジェクトは作成済み
本題のその前に
なぜ nuxt generate
で生成した静的ファイルを使わないのか、についてです。
あくまで個人的な思いなのですが、
- 記事を Git 管理したくない
- 記事を publish するために、いちいち (たとえ CI であろうとも ) ビルド & デプロイを行いたくない
という理由がありました。
とくにビルド時間というのはそこそこかかるので、やらなくても良いのならば、やらないに越したことはないです。
作るブログ
ブログとして最低限の機能のみの実装です。
Firestore の制約上、記事の検索は出来ないので要件に含めていません。
- 記事を投稿できる
- URL は ID ではなく、
yyyy/MM/slug
形式で自由に設定できる- 例:
2018/12/my-favorite-music
- 例:
- 記事にはカテゴリを複数個付けられる
- URL は ID ではなく、
- カテゴリで絞り込みが出来る
- 投稿月で絞り込みが出来る
- 投稿数が分かる
カテゴリ名 (投稿記事数)
、yyyy/MM (投稿記事数)
の形式で表示する
実装
Firestore の設計
Cloud Firestore は NoSQL データベースのため、 RDB のような SELECT COUNT
を用いたカウントは行えません。
また、読み取ったドキュメント数によって課金が行われるだけではなく、読み取る数が多くなるとレスポンスも悪化するので、
一度の操作で読み取るドキュメント数は、できる限り抑えるべきです。
そのため、投稿数などは次のようなドキュメントに記録しました。
// カテゴリー export interface Category { name: string; count: number; }
ドキュメントへの記事数の記録は、後述する Cloud Functions の Cloud Firestore トリガー を使って行います。
ブログ記事は単純な、以下のドキュメントにしました。
export interface Entry { slug: string; title: string; body: string; created_at: Date; // 実際は秒数が降ってきます categories: string[]; }
Cloud Functions
Cloud Functions では、記事の投稿・更新・削除をトリガーに集計を行う処理と、ページの描画処理を登録します。
例えば記事を投稿したときの処理は
import * as firebase from "firebase-admin"; import * as functions from "firebase-functions"; module.exports = functions.runWith({ memory: "256MB", timeoutSeconds: 30 }).firestore.document("entries/{entryId}").onCreate(async (snapshot) => { const entry = snapshot.data() as Entry; for (let name of entry.categories) { const category = await firebase.firestore().collection("categories") .where("name", "==", name) .limit(1) .get().docs.shift(); if (category) { // すでにあればカウントアップ await category.ref.update({ count: (category.data() as Category).count + 1 }); } else { // 無ければ作る await admin.firestore().collection("categories").doc().set({ name, count: 1 }); } } });
となります。
更新・削除も同様の処理を記述していけば OK です。
Nuxt.js と Cloud Functions での SSR は以下の通り。
buildDir
プロパティは、うっかりソースコードからの相対パスで書いてしまうと、何も返ってこなくなります。
import * as express from "express"; import * as functions from 'firebase-functions'; import { Nuxt } from "nuxt"; const app = express(); const nuxt = new Nuxt({ dev: false, buildDir: "./lib/.nuxt", // package.json がある場所からのパス build: { publicPath: "/assets/" } }); async function handleRequest(req, res) { return await nuxt.render(req, res); } app.use(handleRequest); module.exports = functions.runWith({ memory: "256MB", timeoutSeconds: 20 }).https.onRequest(app);
Firebase Hosting
firebase.json
を編集して、 Firebase Hosting のリライトルールを変更します。
基本的には全てのリクエストを Cloud Functions の関数を呼び出すように設定します。
{ "hosting": { "public": "dist/client", "ignore": [ "firebase.json", "**/.*", "**/node_modules/**" ], "rewrites": [ { "source": "**", "function": "render" } ] } }
Nuxt.js
npx create-nuxt-app
で作ったものをそのまま使います。
プリセットにある Tailwind CSS を使うと、CSS が苦手でも悩まされることなく UI を作れるのでオススメです。
Nuxt.js の Firebase の初期化は、 plugins/firebase.ts
で行います。
これは Firebase の初期設定ページに表示されているものをそのままコピペしてきます。
import * as firebase from "firebase"; if (!firebase.apps.length) { const config = { apiKey: "xxxxxxxxxx", authDomain: "xxxxxxxxxx.firebaseapp.com", databaseURL: "https://xxxxxxxxxx.firebaseio.com", projectId: "xxxxxxxxxx", storageBucket: "xxxxxxxxxx.appspot.com", messagingSenderId: "1234567890" }; firebase.initializeApp(config); } export { firebase };
そして、 nuxt.config.js
の plugins
プロパティに plugins/firebase.ts
へのパスを追加しておきます。
plugins: [ "~/plugins/firebase.ts" ]
Firestore からデータを持ってきて、 SSR もしくはページ遷移時に取得するには、 asyncData
メソッドを使用します。
ファイル名・ディレクトリ名を /_year/_month/_slug.vue
のようにすることで、 引数として context
が渡されるので、
その中の params
プロパティ経由で、 year
, month
, slug
といったパラメータを受け取ることが出来ます。
// SFC の script 部分のみ import { Component, Vue } from "nuxt-property-decorator"; import { firebase } from "~/plugins/firebase"; @Component export default class extends Vue { public async asyncData({ params }) { // const { year, month, slug } = params; でパラメータ取得できます。 const entry = await firebase.firestore()...; // 記事取得 return { entry }; } }
asyncData
メソッドの返り値は data
メソッドの返り値とマージされるので、あとは通常の Vue SFC のように
テンプレートや他のメソッドなどを書くだけで OK です。
ブログサイドメニューにあるような、カテゴリ一覧や月別アーカイブは、 store でデータを取得しました。
// 必要な部分のみ const actions = { // ページ遷移でサイドメニュー更新しなくても良いので nuxtServerInit メソッドを使っています async nuxtServerInit ({ dispatch }, context) { await dispatch("fetchArchives"); // ... } }; export default () => new Vuex.Store({ actions, // ... })
あとは、この値を Getter や store
プロパティ経由で取得・設定することで、サイドメニューの表示が実装できます。
まとめ
簡単にですが、 Firebase と Nuxt.js を使うことで、個人でもブログのような Web アプリケーションを作る事を紹介しました。
明日は @umaaaaa さんの記事です!