【Nuxt3】 firebase フロントから実行するかサーバーから実行するか


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

フロントから実行する

  • やってみた的な書き方なら以下の記事が参考になります。(一つのcomposableに初期化もメソッドも含めているので、コレクションがusersとproductsなど複数になってくると書ききれません)

https://zenn.dev/ramo798/articles/5707ce0c2bff9e

  • もうちょい実用的なら、以下の記事が参考になります。

https://github.com/nuxt/framework/discussions/2404

  • 上記の方法では、Nuxtのフォルダに以下のように役割を分担させています。
    • pluginsにfirebase initを記述、useStateでfirebase情報を格納します。
    • 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>('[email protected]')
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>('[email protected]')
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でデータ取得ができるようになります。

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