【v2対応】Nuxt.jsとFirebaseを組み合わせて爆速でWebアプリケーションを構築する


Firebase Advent Calendar 2017 21日目の記事です。

フリーランスでフロントエンドを中心にエンジニアをやっているpotato4dです。
普段はVue.jsを中心に、案件を進めたりコミュニティに関わったりしていますが、今回はそんなVue界隈で今アツいフレームワークであるNuxt.jsとFirebaseを組み合わせて、SPA + SSRにAuthと Firestore を組み合わせたアプリケーションを高速で作る方法を、サンプルとあわせてご紹介します。

  • 2019/10/16 追記
    • このサンプルは Firestore が存在しない Nuxt v1.x + RTDB 時代のコードを愚直に移行している ので全体的に資料が古くなっています。
    • インフラ構成については順次更新していますが、特にデータストア操作周りについては できるだけ参考にしないでください
  • 2019/07/02 追記
    • VuexFire をやめて自前実装に変更しました
    • トップページが SSR されるようになりました
    • (本文の追従はまだです)
  • 2019/03/08 追記
  • 2019/02/03 追記
    • Firebase RTDB から Cloud Firestore へと移行したため、本文について該当箇所の置換を行いました。

今回のサンプル

先にサンプルの完成品から。

Nuxt.js + Firebase で作っており、Vuex でグローバルな状態を管理、 Firestore と連携し、 Firebase Auth で Google アカウントと認証、 SSR によって投稿はレンダリングされ、ホスティングは Firebase Hosting と Firebase Functions を組み合わせてサーバーレスで SSR をするという構成に仕上がっています。

元々が Realtime Database しかない時代のアプリケーションのため、Firestore 層については参考にしないでほしいですが、サーバーレス SPA のデプロイ方法やプロジェクトの作り方としては参考程度にどうぞ。

GitHub(Starしてね): https://github.com/potato4d/nuxt-firebase-sns-example
Demo: https://nuxt-firebase-sns-example.potato4d.me/

Nuxt.jsとは

Vue.jsを普段使っていないかたですと聞きなれないかと思いますが、Nuxt.jsはVue.js製のフロントエンドフレームワークとなっており、SSR対応をはじめとして、ルーティングの自動生成などを行ってくれる非常に優秀な開発ツールキットとなっています。

Nuxt.jsが提供する機能としては、主に以下のようなものがあります。(公式サイトより引用)

  • Vueファイルで記述できること
  • コードを自動的に分割すること
  • サーバーサイドレンダリング
  • 非同期データをハンドリングするパワフルなルーティング
  • 静的ファイルの配信
  • ES6/ES7のトランスパイレーション
  • JSとCSSのバンドル及びミニファイ
  • Head要素の管理
  • 開発モードにおけるホットリローディング
  • SASS, LESS, Stylus などのプリプロセッサのサポート
  • HTTP/2 push headers ready
  • モジュール構造で拡張できること

Nuxt.js自体の基本については今回は省くこととしますので、気になるかたは手前味噌となりますが、私が以前紹介しているスライド・エントリをご覧いただくと円滑かと思います。

Nuxt.jsとFirebaseを組み合わせてできること

そんなNuxt.jsを利用することで、フロントエンドの開発は

  • ルーティングを自分で作成する必要がなく
  • Vuexのストアはオートローディングされ
  • SSRはデフォルトで対応、自由に付け外しができる

という非常に効率が良く、また気軽に実装できる環境が整いましたが、依然としてバックエンドは自分で開発する必要がありました。

また、商用サービスならまだしも、個人サービスでSSRサーバーを建てるNuxtとAPI用のアプリケーションサーバー両方を構築することは、インフラ費も、メンテナンスコストもどうしても増大し、気軽にモノを作ることはできるのはlocalStorageの範囲に限られてしまいます。

そこで今回はその問題を解消するため、Firebaseの Cloud Firestore と認証(Auth)を利用して、「ユーザー認証と永続化」の領域をmBaaSに委譲してしまい、Nuxtでアプリケーションをロジックをメインで書いていく構成を実践してみました。

こうすることで、バックエンドはFirebaseのFirestoreのRuleの記述とAuthも初期設定のみで済んでしまいますので、非常に簡単に構築でき、フロントエンドはVueの強力なサポートのもと、Nuxtを利用することで高い効率で開発が可能となります。

実際、今回のサンプルについては、データの操作周り(Store)のコードは全て含めても80行程度となっており、効率よく実装できているさまがわかるかと思います。

今回の記事は、そんな Nuxt.js + Firebase の組み合わせにおいて、それぞれを少し触ったことがあるかたを対象に、開発におけるコツをご紹介いたします。

Nuxt.js と Firebase での開発のコツ

ここからは実際の開発においてのハマリポイントやオススメのソリューションをご紹介します。

プロジェクトの初期化

  • 2019/10/16 追記
    • 2018/09 の Nuxt.js v2 以降は create-nuxt-app の利用に一本化されています。

Nuxt.jsのプロジェクトを作成する時は、Nuxt.js 公式の create-nuxt-app を利用しましょう。

$ npm i -g create-nuxt-app

create-nuxt-app を利用することで、必要最低限のプロジェクト構造を素早く作ることができます。

アプリケーションのデータの永続化

アプリケーションのデータの永続化は Firebase の Cloud Firestore を利用すると良いでしょう。
Firebase の Firestore は基本的に無料で提供されており、また、リアルタイムな送受信が可能であること、サーバーの管理の必要が無いこと、 MongoDB などの NoSQL 型データベースで一般的な KVS 形式の JSON ストアを提供しています。

Firebase の Firestore は、 JSON 形式のデータを簡単に取り扱うという点では便利ですが、 Firestore の機能を存分に活用したい、リアルタイムな送受信を行いたいというモチベーションが生まれた時に少し問題があります。
Firestore は、一度参照したデータ構造に対して、独自のイベントシステムのもとデータが送られてきます。
これは、JavaScriptの普遍的なイベント駆動を踏襲している Vue においては、気軽に使うには状態を管理しづらいところがありますので、 Vue.js 謹製の vuefire あるいは vuefire の開発者による vuexfire を利用するのが良いでしょう。

Nuxtの場合は、Vuexストアが非常に扱いやすくなるように作られていますので、基本的には vuexfire の利用をおすすめします。

vuexfire は非常にシンプルであり、 Firebase の Firestore の領域のバインディングのみを行ってくれます。
シンプルな例ですと、以下のように記述することで、 Firestore の /posts/ の中身の切り替えをリアルタイムで Vuex Store 内の state.posts に反映してくれます。

受信だけを考える場合は、これ以上特に追加の設定を必要とせず、データの挿入/編集/削除全てに対してリアルタイムで追従してくれます。

import Vue from 'vue'
import Vuex from 'vuex'
import firebase from 'firebase'
import { firebaseMutations, firebaseAction } from 'vuexfire'
const db = firebase.database()
const postsRef = db.ref('/posts')

Vue.use(Vuex)

return new Vuex.Store({
  state: {
    posts: []
  },
  mutations: {
    ...firebaseMutations
  },
  actions: {
    INIT_POSTS: firebaseAction(({ bindFirebaseRef }) => {
      bindFirebaseRef('posts', postsRef)
    })
  }
}

2019/10/16 追記

RTDB ではあまり実用的ではなかったセキュリティルールが、Firestore では大幅に強化されています。

今後セキュリティルールについての項目を追記予定です。

OAuth と認証情報の永続化

Web アプリケーションの開発において、多くの場合認証・認可は必須の機能となります。ですが、古き良き Rails の devise のように、運営者が自分で認証情報を管理することは非常にコストのかかる作業です。

セキュリティにつねに気を払う必要がありますし、データが漏洩した場合を考慮すると、運営者には相応のリスクがあります。

ですが、 Firebase Authentication を利用することで、 Firebase を IDaaS とした認証と、Firestore との組み合わせによる認可を実現できます。

Firebase Auth 自体は多くのサービスプロバイダの OAuth のラッパーとなっており、 Twitter, Facebook, GitHub, Google などの事業者や、匿名ログインやメールアドレスでの認証も対応しています。

一番かんたんな Google 認証は、以下のような数行のコードで記述が可能です。

OAuthサンプル
import firebase from 'firebase'
const provider = new firebase.auth.GoogleAuthProvider()
firebase.auth().signInWithRedirect(provider)

今回の例では、以下のような形で Firebase のラッパーと Auth のラッパーを別に作成し、アプリケーションから気軽に認証を呼び出し、Firebase 単体でユーザーの同一性確認を可能としました。

~/plugins/firebase.js
import firebase from 'firebase'

if (!firebase.apps.length) {
  firebase.initializeApp(
    {
      apiKey: process.env.APIKEY,
      authDomain: process.env.AUTHDOMAIN,
      databaseURL: process.env.DATABASEURL,
      projectId: process.env.PROJECTID,
      storageBucket: process.env.STORAGEBUCKET,
      messagingSenderId: process.env.MESSAGINGSENDERID
    }
  )
}

export default firebase

また、 Auth についても、 firebase.auth().onAuthStateChanged() というメソッドが認証情報の取得および変更検知を担いますが、これはコールバックがデフォルトとなっていますので、 util.promisify を利用するか、以下のように Promise ラッパーを自作すると、メインで記述するコードでは
Promise として扱うことができ、 async/await で呼び出せるため便利です。

~/plugins/auth.js
import firebase from '~/plugins/firebase'

function auth () {
  return new Promise((resolve, reject) => {
    firebase.auth().onAuthStateChanged((user) => {
      resolve(user || false)
    })
  })
}
export default auth

迷ったら認証はGoogleアカウントで

余談ですが、迷ったら認証はGoogleアカウント認証を利用することをおすすめします。 Google 認証であれば、専用トークンが不要であり、管理画面からボタン一つで有効化できて楽です。

その上、Google アカウントは二段階認証を設定している人が多いことを考えると、運営として心理的な負担も軽めで嬉しいですね。

特に Twitter については、 OAuth アプリケーションの締め出しが厳しくなっていますので、控えたほうが良いのは勿論、ほかのサービスについてもそれなりに取得に手間がかかるため、OAuthなら何でも良いなら Google が手軽でしょう。

リアルタイムなコンテンツと SSR

リアルタイムなコンテンツと SSR の併用となるとなかなか難しいところがありますが、ある程度の割り切り方をして進めるのが一番幸せな方法でしょう。

今回は、リアルタイム性の高いコンテンツであるトップのタイムラインついては、SSRのメリットが特に無いため、はじめのレンダリング時は空になるようにしました。
逆に、単一の投稿ページにおいては、 Firebase Firestore の機能の一つ、一度だけ取得し、リアルタイムな変更は行わない .once メソッドを利用して取得することで、 SSR を可能としています。

この切り分けかたによって、 SSR がほぼ必要ない vuexfire 層と、必要となる Firebase Firestore once 層に分けることができます。

勿論、 .once を利用すると Firestore の恩恵のリアルタイムな部分はまったく受けられなくなりますが、逆に言えばアクセス後に投稿が削除される。といったおかしな挙動のもとにもならないため安心できますし、余計な懸念点を増やさないという意味では、自身のアプリケーションの SSR 戦略を見直す上では非常に良い選択肢であるといえるでしょう。

どうしても SSR をした上でリアルタイムなコンテンツを取得したいという場合は、 Vuex の初期ステートに .once 結果を代入後、 vuexfire と連携する。というアプローチを取ることをおすすめします。

SSR しない選択肢とリアルタイムではないデータベースの取り扱い

ここまでは主に Nuxt.js の SSR の側面と Firebase のリアルタイムなデータベースを中心に紹介しましたが、これらを取り扱うにあたって覚えておくと良いことがあります。

無理にSSRしない

また、 Nuxt.js を利用しているからとといって、無理に SSR をする必要もありません。
Nuxt.js には、 mode: 'spa' による SPA 機能、そして、静的サイト生成のための generate 機能がありますので、 SSR をしないといけない状況では無ければ、静的サイトとして最終的に書き出してしまうと、 AWS S3, GitHub Pages, Firebase Hosting, Netlify などの静的サイトホスティングサービスで、無料 or 格安でホスティングができるので、 「SSRしない」 という選択肢も常に考えておきましょう。

SSR はコストのかかる行為です。
そして、 Nuxt.js は、 SSRだけではなく、 prefetch や Vuex ストアのオートローディング、 SPA のルーティングの自動生成など、 SSR を利用せずとも有効活用できるシステムが豊富に揃っています。
目を引く機能ではありますが、 SSR に囚われすぎないことは重要なポイントの一つとなります。

nuxt.config.js にこの一行を追加するだけで SSR をやめて SPA モードになります

無理にリアルタイムなデータの取扱をしない

また、 Firebase の Firestore についても同様です。
確かにリアルタイムな NoSQL のデータベースと聞くと、使ってみたくはなりますが、その一方で、多くの場合リアルタイム通信というものは非常に考えることが多くなる概念です。

Firebase については、先程ご紹介した .once() をはじめとして、 Firestore を 認証付きの CRUD 可能な Web API のように利用することができる側面もあります。むしろ、一般的な Web アプリケーションを構築したい場合は殆どこちらでこと足りるでしょう。

Nuxt.js の SSR と同じくらいインパクトがありますが、同じくらい必要であるケースが限られる技術ではありますので、こちらも 「リアルタイムには通信しない」 という選択肢も常に考えておきましょう。

おわりに

最近は Nuxt.js で開発を行う機会が非常に多く、その強力な機能を Firebase と組み合わせることでより効率的な開発ができるかと思い、ご紹介いたしました。

簡単な CRUD であれば、もはや SPA + Firebase で作ってしまえるような時代はきていますので、その恩恵を存分に享受しながら、効率よくアプリケーションを開発していきたいところです。

Twitterなどでは「Railsみたいで明確」という声も上がるほど、非フロントエンドのかたにもわかりやすいフレームワークである Nuxt.js を、無料ではじめられてかつ高機能、 Web アプリケーションにも使える優良な mBaaS サービス Firebase と連携することで、より便利に使っていきましょう。