👏

【Nuxt3】 firebaseのstoreやstorageへの書き込み クライアントから実行するかサーバーから実行するか(編集中)

2022/04/09に公開

打ち上げ花火って英語でfireflowerというらしいです。

結論

クライアントサイドから実行したほうが良い

firebaseはフロントの開発に集中できるように、認証などサーバーサイドの設定をできる限りしなくて済むよう提供されている。
その恩恵を十分に受けるためにも、まずはクライアントサイドで実行できるように実装しましょう。

そもそもフロントorサーバーから実行とは?

フロント → pagesやcomponentsの.vue内にfirebaseのクライアントオブジェクトを作成して実行
サーバー → server/apiに記述する。pagesやcomponentからuseFetchを使ってアクセスする

フロントから実行する方法

firebaseの公式ドキュメントにも書かれている方法です。

ディレクトリ構成

  • pluginsでfirebase initを実行。$firebaseClientで提供
  • composableにデータ取得やログイン関係のメソッドを記述し
  • pagesや各componentでメソッドを利用します。
  • 欠点としては、ページ読み込みの時にusersを表示させようとするとエラーとなります。
    原因はdbの定義前にfirebaseAppの非同期的処理による初期化がに行われず、dbがundefinedになってしまうため
<script setup lang="ts">
// これはうまくいく
const users = ref<User[]>([])
async getUsersTest(){
	users.value = await useFirestore().getUsers()
}

// ページ読み込みの時にユーザー一覧を表示させようとして以下のように書くとエラーが出る
const users = await useFirestore().getUsers()
console.log(users)
 
</script>

<template>
<button @click="getUsersTest()">ユーザー取得</button>
</template>
expected first argument to collectin() to be a CollectionReference・・・

フロントで実行する方法

クリックで表示

plugins/firebase.client.ts

  • pluginsのファイルにclientをつけることで、クライアントサイドでのみ動作します。
  • firestoreやauthを使うには、firebaseappで初期認証したオブジェクトが必要です。初期化処理は全てpluginsで行うのがシンプルです。
  • 初期化処理が完了したauthやdbはuseStateに格納して、composableから使えるようにしています。useStateに格納することで、各コンポーネントから共通して使えるようになります。
import { defineNuxtPlugin, useState } from '#app'
import { FirebaseApp, initializeApp } from 'firebase/app'
import { Firestore, getFirestore } from 'firebase/firestore'
import { Auth, initializeAuth } from 'firebase/auth'

// Initialize Firebase
const firebaseConfig = {
    apiKey: "",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: "",
    measurementId: ""
};

const firebaseApp:FirebaseApp = initializeApp(firebaseConfig)

export const db:Firestore = getFirestore(firebaseApp)
export const auth:Auth = initializeAuth(firebaseApp)

export default defineNuxtPlugin((nuxtApp) => {
    useState('firebaseApp', () => firebaseApp)
    useState('auth', () => auth)
    useState('db', () => db)
})

composables

1. useFirestore.ts

  • 各コンポーネントからはcomposableの関数や状態変数にアクセスします。
    composableの関数は全てPromiseを返すようにしていますので、例えば書き込み成功の場合は”success”というresが返されますし、失敗の場合は”failure”というresが返されます。
  • それぞれのresに対して何をするか(メッセージ表示やページ遷移など)といった結果に応じたアクションはコンポーネント内に書いた関数に記述します。
import {
    Firestore,
    collection,
    query,
    where,
    getDocs,
    doc,
    getDoc
} from 'firebase/firestore';

type User = {
    id:String,
    name:String
}
type Users = Array<User>

export const useFirestore = () => {
    const db:Firestore = useState('db').value;
		// 全ユーザーを取得
    async function getUsers(){
        return new Promise(async(resolve, reject)=>{
            const q = query(
                collection(db, 'users'),
            );
            const querySnapshot = await getDocs(q);

            const users:Users = querySnapshot.docs.map((doc) => {
                const data = doc.data()
                const user:User = {
                    id:doc.id,
                    name:data.name
                }
                return user
            });
            resolve(users)
        })
    };

    async function getUserById(id:string){
        return new Promise(async(resolve, reject)=>{
            const docRef = doc(db, 'users', id);
            const docSnap = await getDoc(docRef);

            if (docSnap.exists()) {
                const data = docSnap.data()
                const user:User={
                    id:docSnap.id,
                    name:data.name
                }
                resolve(user)
              } else {
                reject(null)
              }
        })
    }
    async function getHashiraUsers(){
        return new Promise(async(resolve, reject)=>{
            const q = query(
                collection(db, 'users'),
                where('hashira','==',true)
            );
            const querySnapshot = await getDocs(q);

            const users:Users = querySnapshot.docs.map((doc) => {
                const data = doc.data()
                const user:User = {
                    id:doc.id,
                    name:data.name
                }
                return user
            });
            resolve(users)
        })
    }
    return {
        getUsers,getUserById,getHashiraUsers
    };
};

2. useAuth.ts

import {
    Auth,
    createUserWithEmailAndPassword,
    signInWithEmailAndPassword,
    onAuthStateChanged,
    signOut
} from 'firebase/auth'

export const useAuth = () => {
    const currentUser = ref<T>(null)    
    const auth:Auth = useState('auth').value

    async function signUp(email:string, password:string){
        return new Promise((resolve)=>{
            createUserWithEmailAndPassword(auth, email, password)
            .then((userCredential) => {
                // サインアップできたらログインする
                const currentUser = userCredential.user;
                resolve("success")
            })
            .catch((error) => {
                const errorCode = error.code;
                const errorMessage = error.message;
                resolve(errorCode)
            });            
        })

    }

    async function passwordSignIn(email:string,password:string){
        return new Promise((resolve)=>{
            signInWithEmailAndPassword(auth, email, password)
                .then((userCredential) => {
                    // ログインできた時
                    currentUser.value = userCredential.user
                    resolve('success')
                })
                .catch((error) => {
                    // ログインできていない時
                    resolve(error)
                })
        })
    };

    function getUserData(){
        console.log(`getUserDataが呼び出された`)
        onMounted(() => {
            onAuthStateChanged(auth, (currentUser) => {
                if (currentUser) {
                    console.log(`currentUserあり`)
                    currentUser.value = currentUser
                }else{
                    console.log(`currentUserなし`)
                    user.value = null
                }
            })
        })
    }

    async function signout() { 
        return new Promise((resolve)=>{
            signOut(auth)
                .then(()=>{
                    currentUser.value = null
                    resolve("success")
                })
                .catch((error)=>{
                    resolve(error)
                })
        })
    }

    return {
      signUp, passwordSignIn, signout, currentUser, getUserData
    };
};

app

app.vue

  • nuxt2ではlayoutsにレイアウトを記述していました。nuxt3でもlayoutsは使えるのですが、layouts/default.vueしか置かない場合はわざわざ作らなくてもapp.vueで代用できます。
  • ログインしているかしていないかでメインコンテンツ部分の表示をv-ifで変えています。
<script setup lang="ts">
const {signUp, passwordSignIn, signout, currentUser, getUserData} = useAuth();

const router = useRouter()

const email = ref<String>('kanawo@kisatsu.co.j')
const password = ref<String>('password')

const message = ref<String>('ログインしていません')

const items = [
    {
        title:'ダッシュボード',
        to:'/',
        icon:''
    },{
        title:'authのテスト',
        to:'/authtest',
        icon:''        
    },{
        title:'storeのテスト',
        to:'/dbtest',
        icon:''
    }
]
async function createAccount(){
    const res:String = await signUp(email.value,password.value)
    if(res=="success"){
        // ダッシュボードに遷移するなど
    }else if(res=="email-already-in-use"){
        message.value = '既にアカウントを作成されているメールアドレスです'
    }
}

async function login(){
    const res:string = await passwordSignIn(email.value,password.value)
    console.log(`ログイン結果, ${res}`)
    if(res==="success"){
        // ダッシュボードに遷移するなど
        console.log(`ログインしました`)
    }else if(/^.+wrong-password.+$/.test(res)){
        message.value = `パスワードが間違っています`
    }else if(/^.+user-not-found.+$/.test(res)){
        message.value = `メールアドレスが間違っています`
    }else{
        message.value = `エラーが発生しました`
    }
}

async function logout(){
    const res:string = await signout()
    console.log(`ログアウト結果, ${res}`)
    if(res=="success"){
        router.push('/')
        message.value = 'ログアウトしました'
    }else{
        message.value = 'ログアウトに失敗しました。もう一度やり直してください'
    }
}

</script>

<template>
    <div style="outline:1px solid;height:50px;display:flex;justify-content: space-between;">
        <h1 style="margin:0 10px;">Nuxt3</h1>
        <div v-if="currentUser">{{currentUser.uid}}</div>
        <button v-if="currentUser" @click="logout">ログアウト</button>
    </div>

    <div v-if="currentUser" style="display:flex">
        <div style="outline:1px solid;width:150px;height:90vh">
            <ul style="list-style-type: none;padding:10px">
                <li v-for="item of items" :key="item.title">
                    <NuxtLink :to="item.to">{{item.title}}</NuxtLink>
                </li>                
            </ul>
        </div>
        <div style="padding:10px">
            <NuxtPage/>            
        </div>
    </div>

    <div v-else style="margin:0 auto;width:30vw">
        {{message}}
        <div>
            <input type="text" id="email" v-model="email" placeholder="メールアドレス">            
        </div>
        <div>
            <input type="text" id="password" v-model="password" placeholder="パスワード">            
        </div>
        <button @click="createAccount">サインアップ</button>
        <button @click="login">ログイン</button>
    </div>
</template>

pages

authtest.vue

<script setup lang="ts">
const {signUp, passwordSignIn, signout, user} = useAuth();

const email = ref<String>('kanawo@kisatsu.co.jp')
const password = ref<String>('password')

const message = ref<String>('')

async function createAccount(){
    const res:String = await signUp(email.value,password.value)
    if(res=="success"){
        // ダッシュボードに遷移するなど
    }else if(res=="email-already-in-use"){
        message.value = '既にアカウントを作成されているメールアドレスです'
    }
}

async function login(){
    const res:string = await passwordSignIn(email.value,password.value)
    console.log(`ログイン結果, ${res}`)
    if(res==="success"){
        // ダッシュボードに遷移するなど
        console.log(`ログインしました`)
    }else if(/^.+wrong-password.+$/.test(res)){
        message.value = `パスワードが間違っています`
    }else if(/^.+user-not-found.+$/.test(res)){
        message.value = `メールアドレスが間違っています`
    }else{
        message.value = `エラーが発生しました`
    }
}

async function logout(){
    const res:string = await signout()
    console.log(`ログアウト結果, `)
    if(res=="success"){
        // ログインページに遷移するなど
    }else{
        message.value = 'ログアウトに失敗しました。もう一度やり直してください'
    }
}

</script>

<template>
    <input
        type="text"
        id="email"
        v-model="email"
        placeholder="メールアドレス"
    >
    <input
        type="text"
        id="password"
        v-model="password"
        placeholder="パスワード"
    >
    <button @click="createAccount">サインアップ</button>
    <button @click="login">ログイン</button>
    <button @click="logout">ログアウト</button>

    <div>メッセージ</div>
    <div>{{message}}</div>

    <div>ログインユーザー情報</div>
    <div>{{user}}</div>

</template>

dbtest.vue

<script setup lang="ts">
type User = {
    id:String,
    name:String
}
type Users = Array<User>
const users = ref<Users>()
const user = ref<User>()

const { getUsers, getUserById, getHashiraUsers } = useFirestore();

async function test1(){
    users.value = await getUsers()
}

async function test2() {
    user.value = await getUserById('tanjiro')
}

async function test3(){
    users.value = await getHashiraUsers()
}
async function test4(){
    users.value = []
    user.value=null
}
</script>

<template>
    <div>
        <button @click="test1">全ユーザーを取得</button>
        <button @click="test2">炭治郎を取得</button>
        <button @click="test3">柱を取得</button>
        <button @click="test4">クリア</button>
        <div style="height:200px;outline:solid 1px">
            ここにusersを表示
            <ul>
                <li v-for="user of users" :key="user.id">{{user.name}}</li>
            </ul>
        </div>

        <div style="height:300px;outline:solid 1px">
            ここにuserを表示
            <div>{{user}}</div>
        </div>
    </div>
</template>

参考

nuxt3 + firebase v9(Firestore, Authentication) を試してみる。

How to integrate Firebase v9 with Nuxt3? · Discussion #2404 · nuxt/framework

サーバーから実行する

  • 解決法としては、データ取得やログイン関係のメソッドをcomposableではなくserver/apiに格納します。
  • これにより、各コンポーネントからはuseFetchでデータ取得ができるようになります。

これを使った書き方については、別記事で作成しています。

設計概要

firebaseの関数はサーバー側で実行します。

  • servers/apiにfirebaseの初期化処理や各コンポーネントで利用する関数を配置します。
  • 各コンポーネントからはuseFetchでデータを取得できます。

※以下リンクで紹介されている方法とほとんど同じです
https://enterflash.io/posts/connect-to-firestore-db-using-admin-sdk-nuxt-3--firebase

※firebaseの処理をフロント側で実行する方法もあります。
しかし、フロント側で実行する方法では、ページ読み込み時にdbオブジェクトを利用する方法を確立できなかったため、今の私はサーバー側で実行する方法をとっています。
https://zenn.dev/yuta_enginner/articles/f2eb0d4f36dfc7

フロント側で実行する方法ではSDKでconfig({apiKey:~~~,authDomain:~~~と書いてあるやつ)でinitializeAppを実行できましたが、サーバー側で実行する方法では秘密鍵でinitializeAppを実行します。SDKで実行できないので注意。

Discussion