volpe’s diary

フリーランスじゃなくなったプログラマ volpe が日々便利だなぁと感じたことを中心に綴るブログです

Nuxt.js + Firebase で動的に初期データを追加する

新規ログインしたユーザに対して初期データを追加したい場合がある。

やり方

  • 初期データ登録用のメソッド setDefaultData を store の actions に定義する。
  • データを表示する page で setDefaultData を呼び出す

setDefaultData の定義

setDefaultData では対象のデータを検索して1件も存在しない場合に予め定義しておいた初期データ default_dataを firestore の I/F add を呼び出して登録する。 id は firestore に登録時に自動付与されるので初期データには含めない。

store/projects.js

const default_data = [
  { name: 'My First Project', order: 0 },
  { name: 'My Second Project', order: 1 }
]

export const actions = {
                 :

  setDefaultData: (context, payload) => {
    // 対象データを検索
    projects
      .where('userId', '==', payload.userId)
      .get()
      .then(querySnapshot => {
        if (querySnapshot.size == 0) {
          // データが存在しなければ初期データを追加する
          default_data.forEach(data => {
            projects.add({ ...data, userId: payload.userId })
          })
        }
      })
  },

                  :
}

setDefaultData の呼び出し

呼び出し側は、データを表示する page の created あたりで setDefaultData を呼び出す。 追加した初期データは vuexfire のデータバインディングによって自動的に store と同期される。

pages/projects/index.vue

  created() {
               :

    this.$store.dispatch('projects/setDefaultData', {
      userId: this.user.uid
    })

               :
  },

Nuxt.js + Firebase で vuexfire を使って Cloud Firestore と連携する(データ参照・更新編)

vuexfire を使ってデータ連携する方法。 データ参照や更新については vuexfire はあまり関係なくて vuex の store と firestore の機能をそのまま使うだけ。

読み出し

データの参照としては、vuexfire で Cloud Firestore とデータバインディングすると store とデータが同期されるので基本的には store の getters にメソッドを定義して使用箇所で呼び出す。

store/projects.js

export const getters = {
  // バインドされた全 project データを取得
  projects: state => {
    return state.projects
  },
  // 'order' でソート済みの全 project データを取得
  orderedProjects: state => {
    return _.sortBy(state.projects, 'order')
  },
  // id 指定で project データを取得
  project: state => projectId => {
    return state.projects.filter(project => project.id === projectId)[0]
  }
}

使用方法としては page や component の computed メンバを定義して view とバインドするパターンが多い。

page/projects/index.vue

  computed: {
    projectList: {
      get() {
        return this.$store.getters['projects/orderedProjects']
      }
    }
  },

追加・更新

データの更新・追加は firestore の API を用いる。

以下の例では、 actions に set メソッドを定義して id の有無で更新または追加の Cloud Firestore のI/Fを呼び分ける構成とした。 これだとデータの種類によらず同じパターンで書けるので上手く抽象化できればコードの記述を省略することが出来そう。

ただ、更新時に payload に id を含めていると、Cloud Firestore 上に不要な id カラムが出来てしまうので、事前に削除している。この辺は呼び出し側で更新対象のデータにに id を含めないようにするなどデータの持ち方を工夫すれば解消できそうだが、Cloud Firestore のデータの持ち方の都合は store 層で面倒を見る方が良い気がしているので現状は以下の実装としている。

store/projects.js

set: (state, payload) => {
  if (payload.id) {
    // NOTE: id カラムは firestore 上では不要なので削除する
    delete payload.project.id
    return projects.doc(payload.id).update(payload.project)
  } else {
    return projects.add(payload.project)
  }
},

使用箇所

      const project = {
        id: projectId,
        order: 0,
        userId: user.uid,
        title: 'hoge project'
      }
      this.$store.dispatch('projects/set', {
        id: projectId,
        project: project
      })
  • set は Promise を返すので後処理を行いたい場合は、 .then(() => { ... }) でメソッドチェーンできる。 (async/await にもできそう)

削除

削除も同様に actions に定義した。 削除対象の id をもらって Cloud Firestore の delete() I/Fを呼び出す。

store/projects.js

delete: (state, payload) => {
  return projects.doc(payload.id).delete()
}

使用箇所

      this.$store.dispatch('projects/delete', {
        id: projectId,
      })
  • delete は Promise を返すので後処理を行いたい場合は、 .then(() => { ... }) でメソッドチェーンできる。 (async/await にもできそう)

Nuxt.js + Firebase で vuexfire を使って Cloud Firestore と連携する(データバインド・階層データ編)

データ間に親子関係がある場合は、子データに親のIDを持たせる方法と、親データのサブコレクションとして子データを持たせる階層データの方法があり得る。

ここでは、後者の階層データをバインドする方法について述べる。

以下のような階層を想定とする。

projects
└── stages

vuexfire 用いたバインドメソッド bindFirestoreRef の ref には、 projects.doc(payload.projectId).collection('stages') のように projects のドキュメントを id で指定してさらにそのサブコレクション stages を指定する。 これによって、特定の projects に紐づいた stages のデータのみバインドできる。

store/stages.js

import { firestoreAction } from 'vuexfire'
import firebase from '@/plugins/firebase'

const db = firebase.firestore()
const projects = db.collection('projects')
let stages = null

      :

export const actions = {
  initStore: firestoreAction(({ bindFirestoreRef }, payload) => {
    stages = projects.doc(payload.projectId).collection('stages')
    bindFirestoreRef('stages', stages)
  }),

ちなみに、 stagesinitStore の外で宣言しているのは別の actions (初期値設定、追加、削除等)でも使用するため。

Firestore のセキュリティルールは以下のようにしている。

service cloud.firestore {
  match /databases/{database}/documents {
    match /projects/{projectId} {
      allow read: if resource.data.userId == request.auth.uid;
      allow write: if request.auth.uid != null;
      
      match /stages/{stage} {
        allow read, write: if request.auth.uid != null;
      }      
    }
  }
}

projects はログインユーザの uid に紐づいたデータのみ参照可能としている。 階層データの stages についてはとりあえずログイン済みユーザでのアクセスを許可しているが、正直どう設定していいのかまだよくわかっていない。。

Nuxt.js + Firebase で vuexfire を使って Cloud Firestore と連携する(データバインド編)

vuexfire を使ってデータ連携する方法。とりあえずデータバインドの設定まで。

前提

  • vuexfire 3.0.0-alpha.15
    • ※ alpha.14 から I/Fが変わっているので注意。まだまだ変わるかも。。

連携方法

基本的には こちら に書いてある方法の通りで動くはず。

ただ、今回は store をファイル単位でモジュール分割してみたので、その構成の実装方法を記す。

ファイル構成

Cloud Firestore 関連の処理は store の action に集約する構成とした。

store
├── index.js
└── projects.js

データバインド

index.js には vuexfire の vuexfireMutations を定義するのみ。

store/index.js

import { vuexfireMutations } from 'vuexfire'

export const mutations = {
  ...vuexfireMutations
}

次に projects のデータバインドを行うメソッド initStore を定義する。 名前はかなり迷ったが、呼び出し元から Cloud Firestore の機能を使っているのを意識した名前で呼びたく無いので bind は使わなかった。(この辺はまだまだ悩み中)

vuexfire の firestoreAction でコールバックの bindFirestoreRef() にバインドしたいデータの名前と collection への参照を渡すと state.projects に Cloud Firestore のデータがバインドされる。これ以降は Cloud Firestore の I/F を使ってデータの変更を行うと自動的に state.projects に反映される。

さらに同じデータを参照している全てのクライアントに変更通知が自動的に行われるため、socket.io を用いた broadcast と同じようなリアルタイム更新の挙動が実現できる。これは凄い。

projects はユーザに紐づいたデータのみ読み込みたいので payload に userId を渡して、 where 句で絞り込んだデータをバインドするようにした。

store/projects.js

import { firestoreAction } from 'vuexfire'
import firebase from '@/plugins/firebase'

const db = firebase.firestore()
const projects = db.collection('projects')

export const state = () => ({
  projects: []
})

export const actions = {
  initStore: firestoreAction(({ bindFirestoreRef }, payload) => {
    bindFirestoreRef('projects', projects.where('userId', '==', payload.userId))
  })
}

bindFirestoreRef の第2引数には collection の ref を渡すが、うっかり絞り込まずに projects をそのまま渡してしまうと別のユーザのデータも含めて全データをバインドしてしまうので注意が必要。

  • 良くない例) bindFirestoreRef('projects', projects)

さらに、セキュリティ的には Cloud Firetore の ルール でごにょごにょしないといけない気がするが、まだよく分かっていないので今後の課題とする。

上記で定義した initStore の呼び出しは、projects データを使用する page または component の creeate あたりで行う。

page/projects/index.vue

const user = firebase.auth().currentUser

        :

this.$store.dispatch('projects/initStore', {
  userId: this.user.uid  // store で uid で絞るために渡してあげる
})

バインドに関してはこのくらいの実装でサーバサイドの実装なしにリアルタイムな自動更新が実現できるのには驚いた。要件によってはリアルタイム系の SPA は socket.io を要らずで済みそうな予感がする。

長くなってきたので、データの参照・更新については別記事にする。

Nuxt.js + Firebase Hosting のクライアントサイドのみで動的ルーティングを行う

Firebase Hosting にクライアントサイドのコードのみ(サーバサイドを未使用)で動的ルーティングを行うにはちょっとしたコツが必要だった。

※サーバサイドのコードを実行するには Cloud Functions という機能を使えば良さそうだが、ここでは触れない。

動的ルーティングを伴う page の構成を以下とする。

pages
├── index.vue
├── login.vue
└── projects
    ├── _id.vue
    └── index.vue

デフォルトのルーティングの設定だと個別のプロジェクトページのURLは以下のようになる。

  • https://localhost/projects/XXXXXXXXXXXXXX
    • ※XXXXXXXXXXXXXX は project の id

ローカルの yarn run dev では問題なく動作する。 ただし、このまま Firebase Hosting にデプロイしても一見動作するが、リロードすると以下のようなエラー画面が出てしまう。

$ yarn run build
$ firebase deploy

f:id:volpe0104:20190330013249p:plain
Firebase Hosting のエラー画面

おそらくサーバサイドのルーティングの処理を定義していないので、 https://localhost/projects/XXXXXXXXXXXXXX というURLを上手く捌けないらしい。 この場合、ルーティングの処理をクライアントサイドで処理できるよう、 router の mode を hash に変更すると良い。

nuxt.config.js に以下を追加

router: {
  mode: 'hash'
},

上記の設定を行うことで、URLが以下のように変更され、Firebase 上でクライアントサイドのコードのみで動作するようになった (#が挿入される)

  • https://localhost/#/projects/XXXXXXXXXXXXXX

ブラウザのキャッシュが残っていたりしてすぐには反映されないかもしれない。その場合はキャッシュをクリアするか少し時間を置いてからアクセスするといい。

ちなみに、 firebase serve で動かすと、ローカルで Firebase Hosting 上で動いている相当の挙動を試すことができるようだ。

$ yarn run build
$ firebase serve

また、今回は試していないが Cloud Functions を使ってサーバサイドの処理を記述すれば hash モードでなくても動的ルーティングを実現できそうなのでいずれやってみようと思う。

vim で Prettier を使えるようにする

Nuxt のプロジェクトで Prettier を使っていて、保存の度にエラーが出るのが面倒なので vimvim-prettier をインストールして整形できるようにしてみた。

前提

vim-prettier のインストール

  • ~/.vimrc に以下を追記
call plug#begin()

       :

Plug 'prettier/vim-prettier', { 'do': 'yarn install', 'for': ['javascript', 'typescript', 'css', 'less', 'scss', 'json', 'graphql', 'markdown', 'vue', 'yaml', 'html'] }
     
       :

call plug#end()

使い方

既存のプロジェクトなどもあって自動で整形されると面倒なので、手動で整形する。

  • 任意の .vue ファイルを開く
  • 適当に編集した後、コマンドで :Prettier を実行する
  • コードが整形された! これは便利!

わからないこと

  • 特定のプロジェクトだけ自動整形できる方法などあるのかな?

Nuxt.js + Firebase で google認証する

Nuxt.js で Firebase の google 認証を実装してみる。 面倒な認証機能がこれほど簡単に実装できるとは!

前提

ログイン処理の実装方法

ログインページ(login.vue)にgoogle認証ボタンを設置し、ログイン後にプロジェクトページ(projects.vue)へ遷移する想定

  • google 認証を実装したい page で firebase を import
    • import firebase from '@/plugins/firebase'
  • view にボタンなどを設置し、google認証を開始するメソッドをバインドする
    • button(@click="googleLogin") Googleでログイン
  • methods: に google認証の開始メソッドを追加 (googleLogin)
googleLogin() {
  const provider = new firebase.auth.GoogleAuthProvider()
  firebase.auth().signInWithRedirect(provider)
}
  • mounted: で認証情報の状態変化のハンドラを実装する
    firebase.auth().onAuthStateChanged(user => {
      if (user) {
        // ログイン後のページに遷移する
        this.$router.push('/projects')
      } else {
        // ログイン失敗。エラー処理など(通常はあり得ない?)
      }
    })

ログイン処理後に遷移してきたページの実装方法

認証済みのユーザ情報を取得。もし取得できてなければログインページへリダイレクトする。 また、ユーザーを切り替えたい場合に備えてログアウトボタンも設置する。

  • 遷移先の page で firebase を import
    • import firebase from '@/plugins/firebase'
  • created: でログインユーザの情報を取得する
    const user = firebase.auth().currentUser
    if (user) {
      // ログイン済み
      this.user = user
    } else {
      // 未ログイン。ログイン画面へ遷移する
      this.$router.push('/login')
      return
    }

    // firestore のデータバインディングなど...
  • view にログアウトボタンを設置
    • button(@click="logOut") ログアウト
  • methods: にログアウト処理を実装する (logOut)
logOut() {
  firebase.auth().signOut()
}

user のプロパティ

user には様々なプロパティが格納されている。 主に使いそうなのは以下

  • uid : ユーザ固有のID。ユーザに関連するデータとの紐づけに使える
  • email : ユーザのメールアドレス。ログイン後のヘッダなどに表示するのに使える
  • photoURL : アイコンのURL。ログイン後のヘッダなどに表示するのに使える

これで、googleアカウントさえあれば簡単にログインできるサービスが作れる! 自前でパスワードなど管理しなくて済むので便利!