半年間の長期インターンを始めたので、1ヶ月ごとに学んだこととかを吐き出してみるの会【12月編】
シリーズまとめ
はじめに
あけましておめでとうございます。本年もよろしくお願いします!
今年は、幸いなことに昨年とは違い、平穏な新年を迎えることができましたね。
なんの縁か分かりませんが、隣国では悲しいことに飛行機事故がまたしても起きてしまい、来年こそは何事もなく、新年を迎えたいですね。
さて、今月はState管理、処理のイメージに頭を悩ませていました。
そもそも、Stateのイメージがまだ完全ではなく、なんとなく使っている(使ってしまっている)&漠然としてしまっている状態でした。そのため、発展的な使い方やコンポーザブル上でのグローバルなState管理を学ぶ必要があるなと思い、年末年始もあったのでStateの勉強がてら個人の方で取り組んだFirebase Authを使った認証実装について、書き起こしていこうと思います。
前提要件として、ログイン状態の永続化が必要であったことから、Nuxt標準のuseState()ではなく、Storeを使いました。
Storeとは?
簡単に言えば、Stateの拡張版です。アプリケーションの状態を保持・変更することができます。Stateはブラウザやタブを閉じると、状態が初期化されてしまいますが、Storeは永続化すれば、閉じても保持してくれます。保存先はローカルストレージ or セッションストレージに自動的に格納されます。(なお、デフォでローカルストレージのようでした。)
Storeの特徴
- Stateの永続化が可能(ブラウザやタブを閉じてもStateの復元ができる)
- DevTools上で、状態の変更の追跡・デバッグが手軽
- 変更前、変更後の値(状態)の取得ができる
など、useState()より拡張され、より開発しやすく、使いやすくなっています。
Storeを触ってみる
Nuxt2まではStoreはあったようなのですが、3から廃止されてしまっているため、今回はみんな大好きPiniaを使います。
pnpm install pinia pinia-plugin-persistedstate firebase
個人的には、pnpmが好きで使っているのでpnpmになっていますが、環境によって適宜変更してください。
piniaとfirebaseのインストールをしながら、アプリディレクトリ上にstoresという名前のディレクトリを作成してください。
現在のディレクトリ構造
/
├─ assets
├─ components
├─ composables
├─ layouts
├─ middleware
├─ pages
├─ plugins
├─ public
├─ server
└─ stores (※このフォルダを作成)
今作ったstoresにコードを書くことで、状態管理をすることができます。
さっそく実装・・・の前に
Firebaseの設定を先に済ませておきましょう。
「プロジェクトを作成する」から、プロジェクト名を適当に決めます。
この辺は、適宜カスタマイズしてください。
アナリティクスを有効にした場合、下記の画面になると思います。デフォルトのアカウントを選択しておけば問題ありません。Firebaseが良しなにやってくれます。
問題なさそうであれば、「プロジェクトを作成」を押して、しばらく待ちましょう。
作成が完了すると、こんな感じの画面になると思うので、「続行」します。
トップページが表示されたら、サイドバーの構築から、Authenticationを探します。
「始める」を押して、
ログインプロバイダを選択します。今回は、メール/パスワードとGoogleを選択します。
ログインプロバイダの追加が完了したら、プロジェクトの設定画面を開きます。
Androidアイコンの隣にあるWebアプリを選択します。
適当に名前を入れます。今回は、Firebase Hostingは使用しないので、チェックしなくて問題はないです。
すると、APIキーやら色々と値が出てくるので、控えておいてください。後ほど使います。
これで、Firebaseの設定は完了です。
今度こそ、実装
余談ですが、Nuxt3でFirebase Authを実装している記事(最新)のがなく、割と大変だったので、参考になれば幸いです。
Store
まずは、stores配下にコードを追加します。今回は、useAuth.ts
というファイルを作成しました。
VS CodeでNuxtrという拡張機能を入れている方の場合、下記のような雛形ファイルが自動的に作成されるかと思います。(最終的なコードは、本記事の末尾に記載しています)
export const useAuthStore = defineStore('auth', () => {
// ここに具体的な処理を書いていく
}
まずは、必要なライブラリ類をインポートしていきます。
import { defineStore } from 'pinia'
import {
createUserWithEmailAndPassword, getAuth, signInWithEmailAndPassword, signInWithPopup, GoogleAuthProvider, signOut, User
} from 'firebase/auth'
import { ref } from 'vue'
今回は、メールアドレス&パスワードを使った認証とGoogleを使った認証の2つを実装していきますので、事前に型定義をしてあげます。AnyだとTypeScript君、拗ねちゃうので()
interface AuthValues {
email: string
password: string
}
そうしたら、メインディッシュとなるコードを書いていきます。
必要な関数として、
・ユーザーを作成する関数(メールアドレス&パスワード認証)
・メールアドレス&パスワードでログインする関数
・Googleを使ってログインする関数
・ログアウトをする関数
の4つの関数を、とりあえず書いていきます。
詳しいコードの中身については、コメントアウトに記載したので参照してください。
// 認証関連の状態管理ストアを定義
export const useAuthStore = defineStore('auth', () => {
// 現在ログインしているユーザーの情報を保持するリアクティブな状態
// null の場合は未ログイン状態を表す
const user = ref<User | null>(null)
/**
* 新規ユーザーをメール/パスワードで登録する関数
* @param values - メールアドレスとパスワードを含むオブジェクト
*/
async function createUser(values: AuthValues) {
const auth = getAuth()
const userCredential = await createUserWithEmailAndPassword(auth, values.email, values.password)
user.value = userCredential.user
}
/**
* 既存ユーザーをメール/パスワードでログインさせる関数
* @param values - メールアドレスとパスワードを含むオブジェクト
*/
async function signInUser(values: AuthValues) {
const auth = getAuth()
const userCredential = await signInWithEmailAndPassword(auth, values.email, values.password)
user.value = userCredential.user
}
/**
* Googleアカウントを使用してユーザーをログインさせる関数
* ログイン後、ユーザー情報をAmplifyデータベースと同期する
*/
async function signInUserWithGoogle() {
// Googleログインのポップアップを表示
const auth = getAuth()
const userCredential = await signInWithPopup(auth, new GoogleAuthProvider)
user.value = userCredential.user
}
/**
* ユーザーをログアウトさせる関数
* ログアウト後、ローカルのユーザー状態をクリアする
*/
async function signOutUser() {
const auth = getAuth()
await signOut(auth)
user.value = null
}
// ストアから外部に公開する状態と関数
return {
user, // 現在のユーザー状態
createUser, // 新規ユーザー登録
signInUser, // メール/パスワードログイン
signInUserWithGoogle, // Googleログイン
signOutUser, // ログアウト
}
}
最後に、ブラウザやタブを閉じてもStateを保持されるようにします。
(略)
signOutUser, // ログアウト
}
},
{
// ページのリロード後もユーザーのログイン状態を維持する設定
// localStorageにストアの状態が保存される
// 逆にこれがないとログイン状態を保持してくれない
persist: true
}
なんと意外にも、これでStoreの実装は終わりです。あら簡単。
最終的なコード全体
import { defineStore } from 'pinia'
import {
createUserWithEmailAndPassword, getAuth, signInWithEmailAndPassword, signInWithPopup, GoogleAuthProvider, signOut, User
} from 'firebase/auth'
import { ref } from 'vue'
interface AuthValues {
email: string
password: string
}
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
async function createUser(values: AuthValues) {
const auth = getAuth()
const userCredential = await createUserWithEmailAndPassword(auth, values.email, values.password)
user.value = userCredential.user
}
async function signInUser(values: AuthValues) {
const auth = getAuth()
const userCredential = await signInWithEmailAndPassword(auth, values.email, values.password)
user.value = userCredential.user
}
async function signInUserWithGoogle() {
const auth = getAuth()
const userCredential = await signInWithPopup(auth, new GoogleAuthProvider)
user.value = userCredential.user
}
async function signOutUser() {
const auth = getAuth()
await signOut(auth)
user.value = null
}
return {
user,
createUser,
signInUser,
signInUserWithGoogle,
signOutUser,
}
},
{
persist: true
}
)
plugins(firebaseの設定)・NuxtConfig
Nuxt3では、plugins配下にあるコードは、特に何もせずに自動インポートがされるので、こちらにFirebaseの初期化処理・認証処理を記載していきます。
ファイル名は、firebase.client.ts
です。
import { initializeApp } from 'firebase/app'
import { getAuth, onAuthStateChanged, type Auth, type User } from 'firebase/auth'
import type { RuntimeConfig } from '@nuxt/schema'
import { useAuthStore } from '~/stores/useAuth';
export default defineNuxtPlugin((nuxtApp) => {
const config: RuntimeConfig = useRuntimeConfig()
const firebaseConfig = {
apiKey: config.public.firebaseApiKey,
authDomain: config.public.firebaseAuthDomain,
projectId: config.public.firebaseProjectId,
storageBucket: config.public.firebaseStorageBucket,
messagingSenderId: config.public.firebaseMessagingSenderId,
appId: config.public.firebaseAppId,
measurementId: config.public.firebaseMeasurementId
}
const app = initializeApp(firebaseConfig)
const auth: Auth = getAuth(app)
const authStore = useAuthStore()
onAuthStateChanged(auth, (user: User | null) => {
if (user) {
authStore.user = user
}
else {
authStore.user = null
}
})
})
NuxtConfig
次に、FirebaseのAPIキーなどを環境変数に格納するために、nuxt.config.ts
に追記していきます。
runtimeConfig: {
public: {
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL,
firebaseApiKey: process.env.NUXT_PUBLIC_FIREBASE_API_KEY,
firebaseAuthDomain: process.env.NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
firebaseProjectId: process.env.NUXT_PUBLIC_FIREBASE_PROJECT_ID,
firebaseStorageBucket: process.env.NUXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
firebaseMessagingSenderId: process.env.NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
firebaseAppId: process.env.NUXT_PUBLIC_FIREBASE_APP_ID,
firebaseMeasurementId: process.env.NUXT_PUBLIC_FIREBASE_MEASUREMENT_ID
}
},
追記ができたら、nuxt.config.ts
と同じ階層上に.env
を作成し、先ほど控えたFirebaseの値を格納しておきます。
NUXT_PUBLIC_FIREBASE_API_KEY=
NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NUXT_PUBLIC_FIREBASE_PROJECT_ID=
NUXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NUXT_PUBLIC_FIREBASE_APP_ID=
NUXT_PUBLIC_FIREBASE_MEASUREMENT_ID=
ログインページの実装
デザインはかなりシンプルにしているので、自分でいい感じにデザインしてください。
長くなってしまうので、トグル内に収めてます↓
フロントデザイン部分
<template>
<div class="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8 bg-white p-8 rounded-lg shadow-sm">
<div>
<h2 class="mt-6 text-center text-3xl font-bold text-gray-900">
ログイン
</h2>
</div>
<form @submit.prevent="handleEmailAndPasswordLogin" class="mt-8 space-y-6">
<div class="rounded-md -space-y-px">
<div>
<label for="email" class="sr-only">メールアドレス</label>
<input
id="email"
type="email"
v-model="form.email"
required
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="メールアドレス"
/>
</div>
<div>
<label for="password" class="sr-only">パスワード</label>
<input
id="password"
type="password"
v-model="form.password"
required
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="パスワード"
/>
</div>
</div>
<div>
<button
type="submit"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
ログイン
</button>
</div>
</form>
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-gray-500">または</span>
</div>
</div>
<div>
<button
@click="handleWithGoogleLogin"
class="w-full flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<svg class="h-5 w-5 mr-2" viewBox="0 0 48 48">
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/>
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/>
</svg>
Googleでログイン
</button>
</div>
</div>
</div>
</template>
次に内部実装部分
<script lang="ts" setup>
import { ref } from 'vue'
import { useAuthStore } from '~/stores/useAuth';
const auth = useAuthStore()
const form = reactive({
email: '',
password: '',
})
const handleEmailAndPasswordLogin = async() => {
await auth.signInUser(form)
}
const handleWithGoogleLogin = async() => {
await auth.signInUserWithGoogle()
}
</script>
これで、Firebaseの設定が完了していれば、問題なくログインができるようになっているはずです。
比較的簡単...!
簡単とは書いていますが、1週間くらい頭を悩まされたくらいには大変だったので、NuxtでFirebase Authを実装したい方の参考になれば幸いです。
最後に
個人事業がかなり順調に話が進んでいるので、2025年は個人事業にも力を入れつつ、大学生活も残り3年なので、駆け抜けて参りたいと思います。インフルエンザとコロナが流行っているので、どうぞご自愛ください。また次回。
Discussion