BLOG

ブログ

PWA+SPAのwebアプリ作成にnuxtjs+firebaseがめちゃ便利だった

背景

 PWA+SPAのwebアプリを作る際にnuxt.js+firebaseを合わせて利用すると便利だったので、そこで得た知見をメモとして残しておこうと思います。まだ勉強を進めている途中のため不明点も多々があり、もしおすすめのやり方などをご存知の方がいらっしゃればコメントをいただけると幸いです。記事自体はこちらのQiita記事にも投稿していますが、こちらブログではシステム構成やコードなど詳細について書いていこうと思います。

構成の説明

ディレクトリ構成

 ディレクトリ構成としては以下のように大きく「terraform系・firebase系・nuxt.js系」の3つに分けています。

.
├── ci                              # CI周りのコードを格納するフォルダ
   ├── deploy.sh                   # Nuxtデプロイシェル
   ├── terraform_apply.sh          # terraformデプロイシェル
   └── terraform                   # tfファイル格納フォルダ
       ├── backend.tf              # terraform設定ファイル
       ├── firebase.tf             # firebase全般周り
       ├── firestore.tf            # firestoreのデータ格納ファイル
       └── variables.tf
├── cloudbuild.yaml             # GCP Cloud Build設定ファイル
├── firebase.json                   # firebase設定ファイル
├── firestore.indexes.json          # firestoreインデックスファイル
├── firestore.rules                 # firestoreインデックスファイル
├── functions                       # firebase cloudfunction設定ファイル
   └── ...
└── {{ アプリ名 }}                   # nuxt.jsアプリフォルダ
    ├── README.md
    ├── assets
    ├── components
    ├── jsconfig.json
    ├── layouts
    ├── middleware
    ├── node_modules
    ├── nuxt.config.js
    ├── package-lock.json
    ├── package.json
    ├── pages
    ├── plugins
    ├── static
    └── store

全体構成図

  • コード管理:github
  • cicd:GCP Cloud Build
  • ホスティング:firebase hosting
  • データベース:firebase firestore
  • 認証:firebase authentication

デプロイフロー

  1. githubでmasterブランチに対してpull requestをなげる
  2. cloud buildで変更を検知し、cloudbuild.yaml記載の内容を実行開始
  3. cloudbuild.yaml:terraform_apply.shを実行してterraformをデプロイ
  4. cloudbuild.yaml:deploy.shを実行してnuxt.jsをデプロイ
  5. cloudbuild.yaml:firebase deployコマンドを実行し、firebase関連のデプロイ

デプロイ用コードの説明

cloudbuild.yaml

ciにおいても大きく「terraform系・firebase系・nuxt.js系」の3つに分けてそれぞれデプロイしています。

substitutions:
  _TERRAFORM_VERSION_: 0.12.10
steps:
  - id: 'terraform apply'   # terraformのデプロイ
    name: 'hashicorp/terraform:${_TERRAFORM_VERSION_}'
    entrypoint: 'sh'
    args: ['./ci/terraform_apply.sh','dev']
  - id: 'nuxtjs deploy'     # nuxt.jsのデプロイ
    name: 'gcr.io/cloud-builders/npm'
    entrypoint: 'sh'
    args: ['./ci/deploy.sh']
  - id: 'firebase deploy'   # firebaseのデプロイ
    name: gcr.io/${PROJECT_ID}/firebase
    args: [ 'deploy', '--project=${PROJECT_ID}' ]
timeout: 3600s

※ firebaseのデプロイをcloud buildで行う際は、 firebase コミュニティ ビルダー の設定を行っておく必要があります

terraform_apply.sh

WORKSPACE=$1
 
cd ./ci/terraform
 
terraform init -backend-config="prefix=${WORKSPACE}" -reconfigure
 
if [ ! $(terraform workspace list | grep ${WORKSPACE}) ]; then
  # workspaceがない場合は新規作成を行う
  terraform workspace new ${WORKSPACE}
fi
terraform workspace select ${WORKSPACE}
 
echo 'terraform apply'
terraform apply -auto-approve -parallelism=50

deploy.sh

cd {{ アプリ名 }}
npm install
npm run generate

便利だと感じた箇所(コード付)

nuxt.js

静的サイトとして出力できる

以下コマンドを実行することによって静的サイトとしてコードを生成することができる。 (デフォルトだとdistフォルダ配下に作成される)

$ npm run generate

ページ内で利用する部品をコンポーネント化して再利用することができる

例えば以下のようなタイトルがちょっとおしゃれなカードなどをコンポーネントとして作っておけば、どのページからでも再利用することができます。

<template>
  <div>
    <v-card elevation="1" class="mb-5" tile>
      <v-card-title class="justify-center mt-10">
        <v-chip class="primary pl-15 pr-15 mt-n12" x-large>{{
          title
        }}</v-chip></v-card-title
      >
      <v-card-text>
        <slot />
      </v-card-text>
    </v-card>
  </div>
</template>

<script>
<em>export</em> <em>default</em> {
  props: {
    title: {
      type: String,
      <em>default</em>: '',
    },
  },
}
</script>

firebase

firestoreはterraformから利用できるためテストデータが準備しやすい

例えば以下のようなデータを作りたいとします

collectionfield内容
users
.nameSTRINGユーザー名
.sexSTRING性別(男性
.ageINTEGER年齢

その場合は以下のようなterraformファイルを作っておけば、 dev環境の場合のみ初めからテストデータを準備することができます。 terraformであればランダムな名前や数値で複数生成することができるため、 テストデータを100件作って開発を進めるといったことができます。

resource google_firestore_document users {
  count = "${terraform.workspace == "dev" ? var.dev_data_count : "0"}"
  project     = google_firebase_project.default.project
  collection  = "users"
  document_id = format("USR-%s-%s", count.index, element(random_id.document_id.*.hex,count.index))
  fields      = templatefile("./user.json",{
    name = element(random_pet.name.*.id,count.index),
    age =  element(random_integer.age.*.result,count.index),,
    sex = element(random_shuffle.sex.*.result,count.index)[0],
    classroom_ref = element(google_firestore_document.classrooms.*.name, count.index)
  })
}

variable "dev_data_count" {
  default = 3
}

////////////////////////////////////////////////////////////////
// random id ランダムなIDを生成する
////////////////////////////////////////////////////////////////
resource random_id document_id {
  count = var.dev_data_count
  byte_length = 10
}

////////////////////////////////////////////////////////////////
// random pet ランダムな名前(英語)を生成する
////////////////////////////////////////////////////////////////
resource random_pet name {
  count = var.dev_data_count
  length = 1
}
 
////////////////////////////////////////////////////////////////
// random inteder ランダムな数値を生成する
////////////////////////////////////////////////////////////////
resource random_integer age {
  count = var.dev_data_count
  min     = 1
  max     = 50
}
 
////////////////////////////////////////////////////////////////
// random shuffle 配列からランダムに数値を取り出す
////////////////////////////////////////////////////////////////
resource random_shuffle sex {
  count = var.dev_data_count
  input = ["男性", "女性", "その他" ]
  result_count = 1
}

「user.json」

{
    "name": {
        "stringValue":"${name}"
    },
    "age": {
        "integerValue":"${age}"
    },
    "sex": {
        "stringValue":"${sex}"
    }
}

firestoreは複雑な権限管理が行える

firestoreのrule(権限管理)はかなり柔軟性が高く、 例えば上の例で「10才以上のユーザーのみ編集を行える」といった複雑な権限を簡単に設計できる

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read: if request.auth.uid == userId;
      allow create, update, delete: if resource.data.age > 9;
    }
  }
}

難しいと感じた箇所(コード付)

nuxt.js

storeでpromise系が入るとコードが複雑になる

promise系が入ると変更検知を行う必要があるため、 componentなどで分離しても基本的に複雑なコードになってしまいがちでした。 例えばfirestoreからuserのデータを取得する場合、 promiseを返却するかstore内のmutationを監視する必要があります。 個人的にはgetter内でうまく書いてgetterで取得すれば、 いい感じに待ってくれたらなぁって思いながら書いていました。 (もしいいやり方をご存知の方がいらっしゃれば教えていただけるととても助かります!)

promiseを返却する場合(store内)
export const actions = {
fetchTexts({ commit, getters, state }) {
    return new Promise((resolve, reject) => {
      firebase.auth().onAuthStateChanged((user) => {
        if (!user) {
          reject(new Error('not authenticated'))
        }
        db.collection('users')
          .doc({{ ユーザーID }})
          .get()
          .then((res) => {
            commit('setUser', res.data())
            resolve(res.data())
          })
          .catch((error) => {
            reject(new Error('error : ' + error))
          })
      })
    })
  },
}
mutationを監視する場合(page側のvueファイル内)
<em>export</em> <em>default</em> {
  ...
  mounted() {
    <em>this</em>.$store.subscribe((mutations, state) => {
      <em>if</em> (mutations.type === 'setUser') {
        <em>this</em>.userName = state.user.name
      }
    })
  }
}

firebase

firestoreでN対Nが作りにくい

firestoreはreferenceという機能があり便利なのですが、 それでもN対Nを結びつける際はいいやり方が特に思い付かず複雑になりがちでした。 僕の場合は以下のような対応で実施しているのですが正直いいやり方なのかは判断がつかないです。 (例としてuserとteacherを紐付ける場合)

collectionfieldfield内容
users
.nameSTRINGユーザー名
.sexSTRING性別(男性
.ageINTEGER年齢
.foods
..food_refREFERENCE特定のteacherと紐づくreference
collectionfieldfield内容
foods
.nameSTRINGフード名
.users
..user_redREFERENCE特定のuserと紐づくreference

感想

「S3の静的ホスティング・cdn版vuejs・uikit」のセットにハマって割と使っていたのですが、 nuxtjsを使うとコンポーネント化など便利な機能が多く自社サイトもnuxtjs+firebaseに切り替えたいなと感じました。 おそらく時間が空いたタイミングで実施すると思うので、 その際は「同じページを別の構成で作った場合どのような差分があるか」についてまとめていきたいと思います。 最後まで読んでいただきありがとうございました。

SinkCapitalではデータに関する支援を行っています

弊社はスペシャリスト人材が多く在籍するデータ組織です。 データ分析や分析基盤の設計などでお困りの方がいらっしゃれば、 まずは無料で、こちらから各分野のスペシャリストに直接相談出来ます。