Firebase+Vue.js(Vue3 Composition API)でログイン/サインアップフォームを作成する

Vue.js

Firebase+Vue.js(Vue3 Composition API)でログイン/サインアップフォーム作成する方法です。

JavaScriptでパスワードベースのアカウントを使用してFirebase認証を行う場合のFirebaseの公式ドキュメントはこちらです。

ディレクトリ構造


root
├─ src
   ├─ App.vue
   ├─ main.js
   ├─ assets
   │   └─ main.css
   ├─ components
   │   ├─ LoginForm.vue
   │   ├─ Navbar.vue
   │   └─ SignupForm.vue
   ├─ composables
   │   ├─ getUser.js
   │   ├─ useLogin.js
   │   ├─ useLogout.js
   │   └─ useSignup.js
   ├─ firebase
   │   └─ config.js
   ├─ views
   │   ├─ Chatroom.vue
   │   └─ Welcome.vue

全体のcssを設定する-main.css

bodyやconteinerクラスなど全体的なcssを設定する場合、assetsの中にmain.cssなどを作成してcssを記述します。

cssなどを読み込む-main.js

main.cssなどを作成したら、main.jsでインポートすることでスタイルを適用できます。

import './assets/main.css'

main.jsは認証で

App.vueは不要なものを削除する

Welcome.vueをメインとして使うとして、App.vueのscritpタグは削除し、templateタグ内は、router-viewのみにします。

  <router-view />

Firebaseを使えるようにする-Firebase/config.js

Firebaseの設定は次の記事を参考にしてみてください。

import firebase from 'firebase/app'
import 'firebase/firestore'
import 'firebase/auth'

const firebaseConfig = {
  apiKey: "APIキー",
  authDomain: "ドメインアドレス",
  projectId: "プロジェクトID",
  storageBucket: "ストレージバケット",
  messagingSenderId: "メッセージの送信者ID",
  appId: "appID"
};

firebase.initializeApp(firebaseConfig)

const projectAuth = firebase.auth()
const projectFirestore = firebase.firestore()
const timestamp = firebase.firestore.FieldValue.serverTimestamp


export {projectAuth, projectFirestore, timestamp}

ログインフォームとサインアップフォームを切り替える-Welcome.vue

<template>
  <div class="welcome container">
    <p>Welcome</p>
    <div v-if="showLogin">
      <h2>Login</h2>
      <LoginForm @login="enterChat" />
      <p>
        No accout yet? <span @click="showLogin = false">Signup</span> instead
      </p>
    </div>
    <div v-else>
      <h2>Signup</h2>
      <SignupForm @signup="enterChat" />
      <p>
        Already refisterd? <span @click="showLogin = true">Login</span> instead
      </p>
    </div>
  </div>
</template>
<script>

import SignupForm from '../components/SignupForm.vue'
import LoginForm from '../components/LoginForm.vue'
import { ref } from 'vue'
import { useRouter } from 'vue-router'

export default {
  components: { SignupForm, LoginForm },
  setup() {
    const showLogin = ref(true)
    const router = useRouter()
    const enterChat = () => {
      router.push({ name: 'Chatroom' })
    }
    return { showLogin, enterChat }
  }
}
</script>

SignupFormとLoginFormコンポーネントが使えるように、importして、componentsに登録します。
リアクティブにするためref関数をimportします。
ログインやサインアップ後にチャットページに飛べるようにuseRouterをimportします。

ログインフォームとサインアップフォームは、v-ifを使ってshowLoginで条件分岐させることで表示の切り替えが可能です。

サインアップフォームを作成する-SignupForm.vue

<template>
  <form @submit.prevent="handleSubmit">
    <input
      type="text"
      required
      placeholder="display name"
      v-model="displayName"
    />
    <input type="email" required placeholder="email" v-model="email" />
    <input type="password" required placeholder="password" v-model="password" />
    <div class="error">{{ error }}</div>
    <button>Sign up</button>
  </form>
</template>

名前、メール、パスワードを入力できるようにinputタグを用意します。
それぞれv-modelで双方向データバインディングを作成します。
errorメッセージはないときは何も表示されないので、v-ifなどを使う必要はありません。

<script>
import { ref } from 'vue'
import useSignup from '../composables/useSignup'

export default {
  setup(props, context) {//Welcome.vueにわたすemitを使えるようにするためcontextを引数に設定する
    const { error, signup } = useSignup()

    // refs
    const displayName = ref('')
    const email = ref('')
    const password = ref('')

    const handleSubmit = async () => {
      // console.log(displayName.value, email.value, password.value)
      await signup(email.value, password.value, displayName.value)
      if (!error.value) {
        // console.log('user signed up')
        context.emit('signup') //Welcome.vueにわたす
      }
    }

    return { displayName, email, password, handleSubmit, error }
  }
}
</script>

リアクティブにするためref関数をimportします。
useSignupというjsファイルを別に作成するので、それをimportします。

useSignupより、errorとsignupを受け取ります。

ref関数でv-modelで必要な名前、メール、パスワードを初期化します。

送信フォームがクリックしたときに発火するイベントを作成します。
useSignupというjsファイルのsignupは非同期なので、async-awaitを使います。
ref関数なので、値にアクセスするときはvalueを使って、名前、メール、パスワードをsignup()メソッドに渡します。
errorがないとき、composition APIでは#emit()が使えないので、代わりにsetup()の第二引数にcontextを設定します。
そうすることで、context.emitを使えるようになるため、signupをWelcome.vueにわたすことができるようになります。

最後に、忘れずに必要な変数をreturnします。

サインアップの処理部分を作成する-useSignup.js

import { ref } from '@vue/reactivity'
import { projectAuth } from '../firebase/config'

const error = ref(null)

const signup = async (email, password, displayName) => {
  error.value = null //エラーをずっと表示しないようにする

  try {
    const res = await projectAuth.createUserWithEmailAndPassword(
      email,
      password
    )
    if (!res) {
      throw new Error('Could not complete the signup')
    }
    await res.user.updateProfile({ displayName }) //updateProfile()はdisplayNameに入力した名前を表示するために必要
    error.value = null //エラーが起きたあとまたサインインしようとするときにエラーを表示させないため
    console.log(res.user)
    return res
  } catch (err) {
    console.log(err.message)
    error.value = err.message //errorを更新する
  }
}

const useSignup = () => {
  return { error, signup }
}

export default useSignup

まず、Firebaseとのデータのやりとりする回数を必要最低限にするため、jsファイル内部で実行するsignupという関数と外部で実行するuseSingupに分けます。
useSingupメソッドはあくまで値をリターンするだけにして、外部にその値を渡すだけの役割にします。

エラーもfirebaseとのデータのやりときもuseSingupメソッドを呼び出すたびに毎回実行する必要はないので、useSingupメソッドの外に記述して役割を切り分けます。

エラーをリアクティブにするのでref関数をimportします。
FirebaseのAuthenticationを使いたいので、config.jsからAuthをimportします。

Firebaseとデータのやり取りを行うので、async-awaitを使って非同期処理します。
ユーザーがサインアップを失敗したときにerrorに値が入ります。再度サインアップしようとするときにエラーをずっと表示しないようにするため、errorの値にnullを設定して空にします。

そして、try-catchを使います。
tryブロックには、responseを格納する変数resに、createUserWithEmailAndPassword()メソッドを呼び出すことで、Firebaseプロジェクトに新しいユーザーが作成されます。
このメソッドには名前の通り、メールアドレスとパスワードを渡します。
そして、もしresがない場合にエラーメッセージを投げるようにしておきます。
そして、これだけではresの中の名前が反映されないので、updateProfile()メソッドを使ってプロフィームに登録される名前を更新します。

ログインフォームを作成する-LoginForm.vue

<template>
  <form @submit.prevent="handleSubmit">
    <input type="email" required placeholder="email" v-model="email" />
    <input type="password" required placeholder="password" v-model="password" />
    <div class="error">{{ error }}</div>
    <button>Log in</button>
  </form>
</template>
<script>

import { ref } from 'vue'
import useLogin from '../composables/useLogin'
export default {
  // composition APIで$emitを使う場合props,contextを引数にわたす
  // context.emit()が使える
  setup(props, context) {
    // refs
    const email = ref('')
    const password = ref('')

    const { error, login } = useLogin()

    const handleSubmit = async () => {
      // console.log(email.value, password.value)
      await login(email.value, password.value)
      if (!error.value) {
        console.log('user logged in')
        context.emit('login') //Welcome.vueにわたす
      }
    }

    return { email, password, handleSubmit, error }
  }
}
</script>

ログインフォームはサインアップフォームとほとんど同じです。
名前がない程度の違いになります。

ログインの処理部分を作成する-useLogin.js

<script>

import { ref } from '@vue/reactivity'
import { projectAuth } from '../firebase/config'

const error = ref(null)

const login = async (email, password) => {
  error.value = null
  try {
    const res = await projectAuth.signInWithEmailAndPassword(email,password)
    error.value = null
    console.log(res)
    return res
  } catch(err) {
    console.log(err.message);
    error.value = 'Incorrect login credentials'
  }
}

const useLogin = () => {
  return {error, login}
}

export default useLogin
</script>

こちらもサインアップのuseSignup.jsとほとんど同じになります。
違いは、名前が不要なため、名前に関わるような記述は不要になります。

userを取得する-getUser.js

import { ref } from 'vue'
import { projectAuth } from '../firebase/config'

// userを初期化するときログインしていたりする場合もあるのでnullは指定できない。
// projectAuthにはcurrentUserがあるのでそれを使う
const user = ref(projectAuth.currentUser)

// onAuthStateChanged()メソッドはログインしたりログアウトしたりサインアップしたりと認証の状態が変化するたびに実行される
projectAuth.onAuthStateChanged(_user => {
  // _userにログインしたりログアウトしたユーザーを格納する
  // 上のuserと分けるため、_userとする
  console.log(('user state changed. Current user is:', _user))
  user.value = _user
})

const getUser = () => {
  return { user }
}

export default getUser

ユーザーをリアクティブにするので、ref関数をimportします。
FirebaseのprojectAuthを使うのでconfig.jsからimportします。
そして、ユーザーの初期値として、projectAuth.currentUserを指定してログインしていてもいていなくても現在のユーザーを格納できるようにします。
そして、projectAuthのメソッドであるonAuthStateChanged()メソッドを使って、ログインしたりログアウトしたりサインアップしたりと認証の状態が変化するたびに実行される関数を作成します。
その引数でログインなどのステータスを変更したユーザーを格納して、それを現在のユーザーに代入することでユーザーを取得することができます。

ログアウトを作成する-useLogout.js

import { ref } from 'vue'
import { projectAuth } from '../firebase/config'

const error = ref(null)

const logout = async () => {
  error.value = null

  try {
    await projectAuth.signOut()
  } catch (err) {
    console.log(err.message)
    error.value = err.message
  }
}

const useLogout = () => {
  return { logout, error }
}

export default useLogout

loginやsignupと同様に、logoutするメインの処理と値を渡すだけのuseLogout()に関数を分けます。

エラーをリアクティブにするので、ref関数をimportします。
FirebaseのprojectAuthを使うのでconfig.jsからimportします。
projectAuthのメソッドでsignOut()を使うことでサインアウト(ログアウト)することができます。
try-catchを用意して、非同期処理のためasync-awaitを使います。

表示する場所が必要なので、Navbar.vueを作成します。

ログイン時に表示するナビゲーション-Navbar.vue

<template>
  <nav v-if="user">
    <div>
      <p>ログイン名:{{ user.displayName }}</p>
      <p class="email">現在のログイン(アドレス):{{user.email}}</p>
    </div>
    <button @click="handleClick">Log out</button>
  </nav>
</template>
<script>

import useLogout from '../composables/useLogout'
import getUser from '../composables/getUser'

export default {
  setup() {
    const { logout, error } = useLogout()
    const { user } = getUser()

    const handleClick = async () => {
      await logout()
      if (!error.value) {
        console.log('user logged out')
      }
    }

    return { handleClick, user }
  }
}
</script>

getUser.jsのgetUser()メソッドよりuser情報を受け取ります。
ログアウトをクリックすると、logout()メソッドを実行してログアウトできるように、useLogout.jsからlogoutとerrorを受け取ります。

userには、いろいろな情報が格納されているので、コンソールで確認して、名前やメールに該当する部分をtemplateタグ内で使用することでリアクティブに表示することができます。

そして、ユーザーがログインしていないときには表示する必要がないので、navタグにv-ifでuserがtrueのときだけ表示するようにします。

認証されているユーザーとそうでないユーザーで切り分ける

Auth Gourdを設定する-index.js

import { createRouter, createWebHistory } from 'vue-router'
import Welcome from '../views/Welcome.vue'
import Chatroom from '../views/Chatroom.vue'
import { projectAuth } from '../firebase/config'

// auth guard 
const requireAuth = (to, from, next) => {
  let user = projectAuth.currentUser
  // console.log('current user in auth guard:', user)
  if (!user) {
    // userが認証されていないとき
    next({ name: 'Welcome' }) //nextでメインページに飛ばす
    // これだけだとリロードするたびに認証がとれなくなるのでmain.jsを修正する
  } else {
    next() //userが認証されているとき、飛び先に送る
  }
}

// No auth guard
// こちらは認証されているユーザーが、ログイン画面にアクセスできないようにしたい
const requireNoAuth = (to, from, next) => {
  let user = projectAuth.currentUser
  if (user) {
    // userが認証されていないとき
    next({ name: 'Chatroom' }) //nextでchatroomに飛ばす
  } else {
    next() //userが認証されているとき、飛び先に送る
  }
}


const routes = [
  {
    path: '/',
    name: 'Welcome',
    component: Welcome,
    beforeEnter: requireNoAuth //認証をしているユーザーがみれないようにする
  },
  {
    path: '/chatroom',
    name: 'Chatroom',
    component: Chatroom,
    beforeEnter: requireAuth //認証をしているユーザーのみがみれるようにする
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

認証されているユーザーのみが中身をみれるようにする

ログインできているユーザーのみが、中身のページ(上の場合はchatroom)をみれるようにします。
そのためには、ユーザーが認証されているかどうかを取得して、それを分岐にして処理を記述します。

auth guardで認証できているユーザーとそうでないユーザーのときのルーティングを設定します。
to, from, nextの3つの引数を用意します。
toは飛び先、fromは飛び元、nextは関数で次の処理です。
next()を使うことで、routerと同じようなことができます。

変数routesにbeforeEnter: requireAuthとして、認証がとれているユーザーのみが該当のページをみれるようにします。
認証がとれていないユーザーはメインのWelcome.vueにリダイレクトするようにします。

ただし、これだけではリロードしたりページをリフレッシュするたびにログインが必要になります。

そのため、main.jsを修正します。

認証されているユーザーはログイン画面をみれないようにする

中身のページ(上の場合はchatroom)からログイン画面URLに直接アクセスしたときにログイン画面が表示されてしまうことを防ぎたいです。
上と要領は同じで条件を逆にするだけです。
認証されているユーザーが直接ログイン画面にアクセスしたときに、中身のページへリダイレクトされるように設定します。

認証ステータスに変化があったときにappを実行する-main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

import './assets/main.css'

// Firebase authサービスをインポート
import { projectAuth } from './firebase/config'

let app

// appに値があるときはappを毎回実行しないようにする
projectAuth.onAuthStateChanged(() => {
  if (!app) {
    app = createApp(App)
      .use(router)
      .mount('#app')
  }
})

認証ステータスの変更で処理を切り分けたいので、Firebase authサービスをimportします。

そして、onAuthStateChanged()メソッドを使って、appがない場合にのみappを作成することでログインしていれば、
そのログインしているという認証ステータスに変更がない限りページをリロードしても該当のページが表示されるようになります。

認証していないユーザーをトップページに飛ばす-Chatroom.vue

<script>

import Navbar from '../components/Navbar.vue'
import getUser from '../composables/getUser'
import { watch } from 'vue'
import { useRouter } from 'vue-router'

export default {
  components: { Navbar },
  setup() {
    const { user } = getUser()
    const router = useRouter()

    watch(user, () => {
      if (!user.value) {
        router.push({ name: 'Welcome' })
      }
    })
  }
}
</script>

認証していないユーザーでも、chatroomというurlにアクセスすれば一応アクセスできてしまうので、
認証していないユーザーがアクセスした場合は、メインページに飛ばすという処理を追加します。

そのためには、現在のユーザーを知るために、getUser.jsからuser情報を受け取ります。
そして、ユーザーを検知できるようにwatchをvueからimportします。
飛び先を指定できるように、useRouterをvue-routerからimportします。

userに値がない場合が認証されていない場合なので、それを条件にしてrouterをpushしてメインページに飛ばします。

Vue.js

Posted by devsakaso