很強大的功能。
簡述
在 Firebase 基礎 中已經介紹過怎麼使用 Firebase 的 firestore 服務,這篇要來介紹 Authentication,也就是跟登入相關的功能。
附註:這邊一樣會用 firebase@8.2
來舉例,所以記得裝對版本。
題外話
寫完這篇後我才體會什麼叫做 SDK?其實這篇用到的登入登出 method 都是先經過 Firebase 包裝成 SDK 後,我們才可以用很簡單的一個 function 來完成的。
如果沒有被包裝成 SDK 的話,我們可能就要自己用 fetch
或是 XMLHttpRequest
之類的來自己處理,不會這麼輕鬆。
會寫這段只是因為我以前有做一個 筆記,當時還沒有很能體會 SDK 跟 API 到底實際差在哪,但現在突然被打通了,感覺還蠻神奇的。
後端的部分
Firebase 的專案建好後,回到主控台會看到側邊欄有一個「Authentication」的選項,點進去後再選取「get start」就會看到這個畫面:
這邊是讓你選你要啟用哪種登入方式,這邊會用「電子郵件」來做舉例,所以就點選後把他啟用。
前端的部分
就和設定 firestore 的方式差不多,只是多了一個 auth
的部分而已:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import firebase from 'firebase/app' import 'firebase/firestore' import 'firebase/auth'
const firebaseConfig = { apiKey: '...', authDomain: '...', projectId: '...', storageBucket: '...', messagingSenderId: '...', appId: '...' }
firebase.initializeApp(firebaseConfig)
const db = firebase.firestore() const auth = firebase.auth()
export { db, auth }
|
關於 Firebase 的驗證原理
這邊只是想做個補充,Firebase 的驗證機制是透過 JWT 來實作的。當使用者登入成功時 Firebase 會產生一個 JWT,接著我們只要在接下來的 request 中都帶上 JWT 就可以達到驗證機制了。
如果你對 JWT 有點好奇的話,推薦參考我寫的這篇:關於 JWT(JSON-Web-Token),裡面有解釋 JWT 是怎麼產生的。
註冊功能
這邊先介紹透過「email」來註冊的流程,會以 custom hook 的方式來撰寫。
先來看段 code 再解釋:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { useState } from 'react' import { auth } from 'firebase/config'
export function useSignup() { const [isPending, setIsPending] = useState(false) const [error, setError] = useState(null)
const signup = async (email, password, displayName) => { setIsPending(true) setError(null) try { const response = await auth.createUserWithEmailAndPassword(email, password) if (!response.user) throw new Error('Signup failed.')
await response.user.updateProfile({ displayName }) setIsPending(false) } catch (error) { setError(error.message) setIsPending(false) } }
return { error, isPending, signup } }
|
簡單來說有兩個流程:
- 用
createUserWithEmailAndPassword
來建立基本資料
- 用
updateProfile
來更新 displayName
因為這兩個東西是拆成兩個 API 來做的,所以才要分兩個動作。
總之,在成功建立以後你可以到 Firebase 的主控台來確認:
如果要刪除也是從這邊刪,把它當作資料庫就行了。
加上使用者頭像的功能
這邊只是當作補充的懶人包:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import { useEffect, useState } from 'react' import { auth, storage } from 'firebase/config' import { useAuthContext } from './useAuthContext'
export function useSignup() { const [isPending, setIsPending] = useState(false) const [isCancelled, setIsCancelled] = useState(false) const [error, setError] = useState(null) const { dispatch } = useAuthContext()
const signup = async (email, password, displayName, thumbnail) => { setIsPending(true) setError(null) try { const response = await auth.createUserWithEmailAndPassword(email, password) if (!response.user) throw new Error('Signup failed.')
const uploadPath = `thumbnail/${response.user.uid}/${thumbnail.name}` const img = await storage.ref(uploadPath).put(thumbnail) const imgUrl = await img.ref.getDownloadURL() await response.user.updateProfile({ displayName, photoURL: imgUrl }) dispatch({ type: 'LOGIN', payload: response.user }) if (!isCancelled) { setIsPending(false) } } catch (error) { if (!isCancelled) { setError(error.message) setIsPending(false) } } }
useEffect(() => { return () => setIsCancelled(true) }, [])
return { error, isPending, signup } }
|
簡單來說就是:
- 設定要上傳的路徑:
/thumbnail/${使用者uid}/${檔案名稱}
- 用把圖片上傳到 storage:
await img.ref.getDownloadURL()
- 用圖片的 reference 取得 URL:
await img.ref.getDownloadURL()
- 最後把 URL 寫入 Profile 中就行了
登出
其實關鍵只有一行而已,不過這邊一樣寫成 hook 的形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { auth } from 'firebase/config' import { useState } from 'react' import { useAuthContext } from './useAuthContext'
export const useLogout = () => { const { dispatch } = useAuthContext() const [isPending, setIsPending] = useState(false) const [error, setError] = useState(null) const logout = async () => { setIsPending(true) setError(null) try { await auth.signOut() dispatch({ type: 'LOGOUT' }) } catch (error) { setError(error.message) setIsPending(false) } } return { logout, isPending, error } }
|
登入
一樣關鍵的地方只有一行,這邊用 hook 來表示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import { auth } from 'firebase/config' import { useEffect, useState } from 'react' import { useAuthContext } from './useAuthContext'
export const useLogIn = () => { const { dispatch } = useAuthContext() const [isPending, setIsPending] = useState(false) const [isCanceled, setIsCanceled] = useState(false) const [error, setError] = useState(null)
const login = async (eamil, password) => { setIsPending(true) setError(null) try { const response = await auth.signInWithEmailAndPassword(eamil, password) dispatch({ type: 'LOGIN', payload: response.user })
if (!isCanceled) { setIsPending(false) } } catch (error) { if (!isCanceled) { setError(error.message) setIsPending(false) } } }
useEffect(() => { return () => setIsCanceled(true) }, [])
return { login, isPending, error } }
|
登入與登出的 Clean function
附註:雖然新的 React 好像有對這個做修正,所以就算不這樣做也沒關係,但我覺得最好還是暸解一下這個觀念比較好。
這只是用來避免下面這種情形:
- 我登入時畫面還在 loadnig,但我直接點去別的頁面
- 我登出時畫面還在 loading,但我馬上點去別的頁面
這兩種情形都會引發「明明元件已經被撤銷了,但我還試著要去更新 state 的情形(loading 狀態)」,所以這邊一樣要透過 clean function 來做一些事情。
在 React-這個 fetch 我剛剛要但現在又不要了 中有介紹過怎麼取消 fetch,但我們現在是用 Firebase 提供的 API 來實作,那該怎麼辦才好?
技術上來說我們沒有辦法真的「取消 request」這件事,不過我們可以控制「在 xxx 條件下才准許更新 state」,這也就是這邊的做法,直接來看 code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import { auth } from 'firebase/config' import { useEffect, useState } from 'react' import { useAuthContext } from './useAuthContext'
export const useLogout = () => { const { dispatch } = useAuthContext() const [isPending, setIsPending] = useState(false) const [isCanceled, setIsCanceled] = useState(false) const [error, setError] = useState(null)
const logout = async () => { setIsPending(true) setError(null) try { await auth.signOut() dispatch({ type: 'LOGOUT' }) if (!isCanceled) { setIsPending(false) } } catch (error) { if (!isCanceled) { setError(error.message) setIsPending(false) } } }
useEffect(() => { return () => setIsCanceled(true) }, [])
return { logout, isPending, error } }
|
改寫成這樣後,就算這個元件被撤銷了也不會再去更新 state,因為當下的條件不成立(isCanceled = true
)。
不過這邊你可能會一個疑問是 dispatch
不用放進去判斷嗎?這不是也會更新 state?
記得我一開始說的嗎?我說過我們沒有辦法真的去「取消 request」,也就是說實際上還是把「登出」的 API 給打出去了,所以最後還是會完成「登出」這個動作。
如果這時候我又放進去判斷的話,那就會導致「畫面上顯示已登入,但後端那邊實際上是已登出」的問題,所以才不可以這樣做。
雖然還有另外一個原因是因為 dispatch 更新的 state 是 global 的,所以就算這個元件被撤銷了也沒關係,畢竟有可能會有其他的元件會需要用到它。
登入狀態初始化
前面已經介紹「註冊、登入、登出」這三個功能,接著要介紹的是初始化。
我們在一開始載入頁面時瀏覽器並不會使用者有沒有登入,所以一定是打一隻 API 去問說「使用者現在登入了嗎?」才會知道結果,因此這個動作就是「初始化」。
關於初始化的 API,我原本知道的作法是用 LocalStorage 中的 JWT 去打,但這邊要介紹的是另外一種 Firebase 提供的方法:onAuthStateChanged
。
簡單來說就是 Auth 版的即時資料,只要前後端某一方發生了登入、登出的行為就會立刻 trigger 這個 function,所以他寫起來就跟在抓 real-time-data 的方式很像:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| export const authReducer = (state, action) => { switch (action.type) { case 'INIT_USER': return { ...state, user: action.payload, isUserInit: true } default: return state } }
useEffect(() => { const unsubscribe = auth.onAuthStateChanged((user) => { dispatch({ type: 'INIT_USER', payload: user }) unsubscribe() }) }, [])
|
這邊做的事情有兩個:
- 去看目前 user 的登入狀態,如果有人登入就會拿到那包 Object,沒有的話就會是 null。
- 接著用 dispatch 來更新
user
的資料,並且把 isInitUser
設定為已完成。
最後是 unsubscribe
,因為我希望這個動作只需要「做一次」就夠了,不需要每次 user 狀態更新時都執行一遍,所以在做完初始化以後就立刻「取消追蹤」。
順道一提,isInitUser
這個 state 是用來處理第一次載入時畫面會「閃一下」的問題。反正就是為了提升 UX 用的 state,你可以在 isInitUser = false
時先顯示載入畫面 or 不要顯示任何東西,等到初始化完後再顯示正確的畫面即可,這邊就不示範了。