😊

【firebase + nuxt3開発 2022】firebase v9とnuxt3を用いた開発手順

13 min read

2021年9月にfirebase v9が正式採用になり、2021年10月にはNuxt3のベータ版が公開になりました。
それぞれの変化やメリットは他の人の記事を参考いただくとして、2022年はこの2つを用いた開発になりそうです。

やってみたレベルの単発の記事は見かけますが、実際にアプリ開発のscaffoldレベルの記事は見かけなかったため、ここでは実用的なレベルまで開発します。

概要

nuxt2ではpluginsにfirebase設定を記述し、各コンポーネントでpluginsのfirebaseオブジェクトをインポートして使っていました。

firebase v9からは「必要なものだけをオブジェクトとして作成する」というコンセプトに変わっていますし、nuxt3では各コンポーネント共通で利用するような関数をcomposablesにまとめて記述することで、各コンポーネントをシンプルに記述できるになっています。

どこに何を配置するか、人やプロジェクトによりいくつか書き方はありますが、私が2022年1月現在ベストと思った方法を紹介します。

どういったアプリか

  • ユーザー認証はfirebase authのメール認証を用いています。
  • dbはfirestoreを用い、データは以下のような鬼滅の刃のキャラクターです。

設計概要

  • pluginsに初期化処理関係を配置
  • composablesにdbやauthの関数(作成や保存など)と状態変数を配置
    composableにはdb.collection(’somecolection’).doc(’sumdoc’)といったfirebase内部的な関数を記述し、結果をPromiseで返しています。書き込み成功や失敗は全てresolveで返し、その結果に対して何をするかは各コンポーネントで決めます。
  • 各コンポーネントからはcomposableの関数や状態変数にアクセスします。
    composableの関数は全てPromiseを返すようにしていますので、例えば書き込み成功の場合は”success”というresが返されますし、失敗の場合は”failure”というresが返されます。
  • それぞれのresに対して何をするか(メッセージ表示やページ遷移など)といった結果に応じたアクションはコンポーネント内に書いた関数に記述します。

アプリ作成

こちらの手順に従って、アプリを作成しておいてください。

https://zenn.dev/yuta_enginner/articles/704620c3e7eee4

plugins/firebase.client.ts

  • pluginsのファイルにclientをつけることで、クライアントサイドでのみ動作します。
  • firestoreやauthを使うには、firebaseappで初期認証したオブジェクトが必要です。初期化処理は全てpluginsで行うのがシンプルです。
  • 初期化処理が完了したauthやdbはuseStateに格納して、composableから使えるようにしています。useStateに格納することで、各コンポーネントから共通して使えるようになります(vuexと同じ感覚です)
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

書き方はfirebase v9の書き方です。(別記事でも書きましたが、日本人的感覚ではv8の方が直感的でわかりやすいですね、、、)

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.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

Discussion

ログインするとコメントできます