Nuxt.jsでfirestoreのデータ取得をSSRで行うには?~firebase-adminを使わない編~


目的(purpose)

前回、firestoreのデータをサーバーサイドで取得するのにfirebase-adminを使っていました。
今回、firebase-adminを使わなくてもできるってことに今更気づいたのでまとめていきます。
また、前回、前々回と実装していなかったログイン後のタグまたはwindows.location.hrefを使ったページ遷移時でもログイン状態を保つ処理も一緒にまとめていきます。
今回も前回と同じでfirebaseのAuthenticationに既にアカウントがあることが前提となっています。
アカウント追加の仕方は下の記事を参照してください。


開発環境(Development environment)

OS: Windows10 home
エディタ: Visual Studio Code
言語: Typescript


ディレクトリ構成(Directory structure)

sample/
     ├ @types/
     │      ├ js-cookie.d.ts
     │      └ jwt-decode.d.ts
     │
     ├ middleware/
     │       ├ verifyLoggedIn.ts
     │       ├ verifyNotLoginYet.ts
     │
     ├ pages/
     │    ├ auth/
     │    │   └ signin.vue
     │    ├ memberOnly/
     │    │   └ index.vue
     │    └ index.vue
     │
     ├ server/
     │      └ index.ts
     │
     ├ store/
     │    ├ modules/
     │    │     └ user.ts
     │    └ index.ts
     │
     ├ .env
     │
     └ nuxt.config.js

※ 今回使用しているファイルのみ表示しています。


ページ画面の作成(website screen)

ここではlocalhostで開いたときの最初の画面、サインインする画面、アカウント持っている会員だけが開ける画面の三つを作成していきます。


pages/index.vue

<template>
  <section>
    <div>
      <nuxt-link to="/membersOnly">会員専用ページ</nuxt-link>
      <nuxt-link to="/auth/signin">サインイン</nuxt-link>
    </div>
  </section>
</template>

  • 特に解説する部分はありません。


pages/auth/signin.vue

<template>
  <form @submit.prevent="signIn">
    <input id="usernameTxt" type="email" v-model="email" placeholder="メールアドレス">
    <input id="passwordTxt" type="password" v-model="password" placeholder="パスワード">
    <button type="submit">サインイン</button>
  </form>
</template>

<script lang="ts">
import Vue from 'vue'
import { mapActions } from 'vuex'
import axios from 'axios'

export default Vue.extend({
  data () {
    return {
      email: '',
      password: '',
    }
  },
  middleware: 'verifyLoggedIn',  // 1
  methods: {
    ...mapActions('modules/user', ['saveUserData']),
    signIn () {
      axios.get('/server/signin', {
        params: {
          email: this.email,
          password: this.password
        }
      })
      .then ((response) => {
        this.saveUserData(response.data)  // 2
        this.$router.push('/membersOnly')  // 3
      })
    },
  }
})
</script>

  • 1: もし既にサインインしている状態でもう一度サインインの画面を開こうとしても会員専用ページに飛ばすための部分です。
  • 2: 後にも解説しますが、サインインしたときに取得したユーザーのデータをstoreとcookieに保存します。

  この処理によって画面をリロードしてもサインイン状態を保つことができます。

  • 3: データを保存し終えて無事サインインできたら会員専用ページに遷移させます。


pages/membersOnly/index.vue

<template>
  <section>
    会員専用ページ
    <div>
      <button @click="signOut">サインアウト</button>
    </div>
    <button @click="getUserData">usersコレクションのデータを取得する</button>
    <br>
    {{ viewData }}
  </section>
</template>

<script lang="ts">
import Vue from 'vue'
import { mapActions } from 'vuex'
import axios from 'axios'

export default Vue.extend({
  data () {
    return {
      viewData: {}
    }
  },
  middleware: 'verifyNotLogInYet',  // 4
  methods: {
    ...mapActions('modules/user', ['removeUserData']),
    async getUserData () {
      axios.get('/server/users')  // 5
      .then((response) => {
        this.viewData = response.data
      })
    },
    async signOut () {
      await axios.get('/server/signout')  // 6
      await this.removeUserData()  // 7
      this.$router.push('/auth/signin')
    },
  }
})
</script>

  • 4: まだサインインしていないユーザが会員専用ページに行こうしようとしても強制的にサインインページに遷移させる部分です。
  • 5, 6: firestoreのデータ取得をserverMiddlewareを使ってapi処理として取得するのでここではaxiosを使って呼び出します。
  • 7: サインインするときに保存したデータを削除することでサインアウト後、会員専用ページに入れないようにしています。


middlewareの実装(Implement middleware)

次にmiddlewareの実装をしていきます。
serverMiddlewareではありません。middlewareです!
middlewareは公式によると特定のページまたはいくつかのページがレンダリングされる前に実行される関数を用意できる場所みたいです。


middleware/verifyLoggedIn.ts

import { Context } from '@nuxt/types'

export default (context: Context) => {
  if (context.store.getters['modules/user/loggedIn']) {  // 1
    return context.redirect('/membersOnly')
  }
}

  • 1: サインインする際にデータを保存していますが保存先の一つがstoreです。

storeのgettersから既にサインインしているかどうかを確認し、していたら専用ページに飛ばすようにしています。


middleware/verifyNotLoginYet.ts

import { Context } from '@nuxt/types'

export default (context: Context) => {
  if (!context.store.getters['modules/user/loggedIn']) {  // 2
    return context.redirect('/auth/signin')
  }
}

  • 2: 先ほどと逆でサインインしていなかったらサインイン画面に飛ばします。


serverMiddlewareを実装する(Implement serverMiddleware)

serverMiddlewareを実装していきます。
ここではサインイン、サインアウト、データの取得の三つの機能を用意します。


server/index.ts

import { Request, Response } from 'express'
import firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/firestore'

require('dotenv').config({ path: './.env' })  // 1

const config = {
  apiKey: process.env.API_KEY,
  authDomain: process.env.AUTH_DOMAIN,
  databaseURL: process.env.DATABASE_URL,
  projectId: process.env.PROJECT_ID,
  storageBucket: process.env.STORAGE_BUCKET,
  messagingSenderId: process.env.MESSAGING_SENDER_ID,
  appId: process.env.APP_ID,
  measurementId: process.env.MEASUREMENT_ID
}

const firebaseApp = !firebase.apps.length ? firebase.initializeApp(config) : firebase.app()
const auth = firebaseApp.auth()
const firestore = firebaseApp.firestore()

const express = require('express')
const app = express()

// サインイン
~~~~~~~~~~
処理を記載
~~~~~~~~~~

// サインアウト
~~~~~~~~~~
処理を記載
~~~~~~~~~~


// データ取得
~~~~~~~~~~
処理を記載
~~~~~~~~~~

module.exports = {  // 2
  path: '/server/',
  handler: app,
}

  • 1: serverMiddlewareはnuxt.config.jsに定義している環境変数を読み込むことはできません。

そのためserverMiddleware内にrequire('dotenv').config({ path: './.env' })をして環境変数を使用します。
API: env プロパティ - NuxtJS

  • 2: http:~~/server/~を呼び出すことでserverMiddleware内の各apiを呼び出すことができます。


server/index.ts (サインイン)

// サインイン
app.get('/signIn', (req: Request, res: Response) => {
  const email = req.query.email
  const password = req.query.password

  auth.signInWithEmailAndPassword(email, password)
  .then(async (response: any) => {
    const token = await response.user.getIdToken()  // 3
    const info = {
      token: token,
      user: response.user
    }
    res.send(info)  // 4
  })
})

  • 3: signInWithEmailAndPasswordで取得できるresponseの中にはuid登録したメールアドレス等が入っています。これをトークンにして呼び出し側に送り後にcookieに保存します。
  • 4: 呼び出し側に値を返しているのですが返ってきたresponse内のdataキーに格納されています。


server/index.ts (サインアウト)

// サインアウト
app.get('/signOut', async (req: Request, res: Response) => {
  await auth.signOut()
  res.end()
})

  • サインアウトのみしています。データは返さないのでres.end()で終えています。


server/index.ts (データ取得)

// データ取得
app.get('/users', async (req: Request, res: Response) => {
  await firestore.collection('users').get()
    .then((query: any) => {
      const items: any[] = []
      query.forEach((doc: any) => {
        const userData = {
          id: doc.id,
          data: doc.data()
        }
        items.push(userData)
      })
      console.log(items)
      res.send(items)
    })
})

  • firebaseの公式ドキュメント通りにusersコレクション内のデータをまとめて取得しています。


次にサインインしたときに取得したユーザのデータをstore, cookieに保存していきます。
storeに保存することでpagesやcomponentsをまたいだデータの共有等がやりやすくなります。
しかし、store内に保存したデータは画面をリロードしたりすると簡単に消えてしますためその対策としてcookieにトークンを保存しておきます。


store/modules/user.ts

import Cookies from 'js-cookie'  // 1

export const state = () => ({
  uid: null,
  email: null
})

export const getters = {
  loggedIn (state: any) {  // 2
    return !!state.uid
  }
}

export const actions = {
  async saveUserData ({ commit }: any, userInfo: any) {
    Cookies.set('userToken', userInfo.token)  // 3
    commit('setUid', userInfo.user.uid)
    commit('setEmail', userInfo.user.email)
  },

  async removeUserData({ commit }: any) {
    Cookies.remove('userToken')  // 4
    commit('setUid', null)
    commit('setEmail', null)
  },
}

export const mutations = {
  setUid (state: any, uid: string) {
    state.uid = uid
  },
  setEmail (state: any, email: string) {
    state.email = email
  },
}

  • 1: cookieに保存するためにjs-cookieを使っていますが、私の環境だとただimportしただけでは認識してくれませんでした。なので下のファイルも用意しました。


@types/js-cookie.d.ts

declare module 'js-cookie'

  • 2: middlewareで用意した。サインインしているかしていないかで遷移するページを制御していましたが、ここを基準に処理しています。
  • 3, 4: 先ほども書きましたがstore内のデータは簡単に消えてしまうのでcookieに保存し、サインアウトするときは削除しています。


store/index.ts

import jwtDecode from 'jwt-decode'  // 5
const cookieparser = require('cookie')

export const actions = {
  nuxtServerInit ({ commit }: any, { req }: any) {
    if (!req.headers.cookie) return
    const parsedCookie = cookieparser.parse(req.headers.cookie)

    const userInfo = parsedCookie.userToken
    if (!userInfo) return

    const infomation = jwtDecode(userInfo)
    if (!infomation) return
    commit('modules/user/setUid', infomation.user_id)
    commit('modules/user/setEmail', infomation.email)
  }
}

  • 5: jwt-decodeの場合もimportしただけでは認識しなかったため下のファイルを用意しました。


@types/jwt-decode.d.ts

declare module 'jwt-decode'


serverMiddlewareを追加する(add serverMiddleware)


nuxt.config.js

modules: [
    '@nuxtjs/axios',
  ],
  dotenv: {
    filename: '.env'
  },
  axios: {
  },
  serverMiddleware: [
    '~/server'                   // <= 追加する
  ],

  • 上記のように追加します。また、npm install axiosをする必要があるかもしれないのでインストールしておいてください。


最後に(Finally)

今回は前回と違い、firebase-adminを使わずfirebaseモジュールのみでサインイン、データ取得をしてみました。冗長な部分または、間違いがあるかもしれません。発見しだい後々修正していくつもりです。
ここまで見て頂きありがとうございます。