背景
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
デプロイフロー
- githubでmasterブランチに対してpull requestをなげる
- cloud buildで変更を検知し、cloudbuild.yaml記載の内容を実行開始
- cloudbuild.yaml:terraform_apply.shを実行してterraformをデプロイ
- cloudbuild.yaml:deploy.shを実行してnuxt.jsをデプロイ
- 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から利用できるためテストデータが準備しやすい
例えば以下のようなデータを作りたいとします
collection | field | 型 | 内容 |
---|---|---|---|
users | – | – | – |
. | name | STRING | ユーザー名 |
. | sex | STRING | 性別(男性 |
. | age | INTEGER | 年齢 |
その場合は以下のような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を紐付ける場合)
collection | field | field | 型 | 内容 |
---|---|---|---|---|
users | – | – | – | – |
. | name | – | STRING | ユーザー名 |
. | sex | – | STRING | 性別(男性 |
. | age | – | INTEGER | 年齢 |
. | foods | – | – | – |
. | . | food_ref | REFERENCE | 特定のteacherと紐づくreference |
collection | field | field | 型 | 内容 |
---|---|---|---|---|
foods | – | – | – | – |
. | name | – | STRING | フード名 |
. | users | – | – | – |
. | . | user_red | REFERENCE | 特定のuserと紐づくreference |
感想
「S3の静的ホスティング・cdn版vuejs・uikit」のセットにハマって割と使っていたのですが、 nuxtjsを使うとコンポーネント化など便利な機能が多く自社サイトもnuxtjs+firebaseに切り替えたいなと感じました。 おそらく時間が空いたタイミングで実施すると思うので、 その際は「同じページを別の構成で作った場合どのような差分があるか」についてまとめていきたいと思います。 最後まで読んでいただきありがとうございました。