Try   HackMD

Firebase 筆記 (Google 登入 API、資料寫入 Firestore)

此筆記是基於 zerotomastery.io 推出 React 課程 Complete React Developer in 2022 所撰寫。

88. Setting Up Firebase

  1. firebase 官網新增新專案
  2. yarn add firebase

89. Authentication Flow (Firebase 串 Google 登入驗證流程)

腳色:

  1. 客戶端(我方網站)
  2. Google Server
  3. Firebase Server

背後流程:

  1. 客戶端向 Google Server 發出一個登入 Request ⇒ 登入成功 Google Server 會回傳一個 auth_token 給客戶端。
  2. 客戶端將 auth_token 發給 Firebase Server,Firebase Server 再向 Google Server 驗證這個 auth_token ⇒ 驗證成功 Google Server 會回傳一個 verification token 給 Firebase Server,此時 Firebase Server 會產生一個 access token 並回傳給客戶端。
  3. 客戶端可以開始發 CRUD Requests(帶著 access token)到 Firebase Server

92. Authenticating With Firebase (串 Google 登入實作開始)

建置 Firebase 實例並 import 進 React 專案

  1. 至 Firebase 新增一個網頁 App:

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    點選安卓圖案右邊的按鈕。

  2. 輸入一個自訂 App 名稱並且註冊:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  1. 註冊完成後,呈現以下畫面並把 SDK 段落全部複製:
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "xxx",
  authDomain: "xxx",
  projectId: "xxx",
  storageBucket: "xxx",
  messagingSenderId: "xxx",
  appId: "xxx"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
  1. 新增檔案:src/utils/firebase/firebase.utils.js 貼上複製的 SDK

登入驗證實作

  1. import 登入、驗證相關 API from 'firebase/auth'
import {
	getAuth,
	signInWithRedirect,
	signInWithPopup, 
	GoogleAuthProvider
} from 'firebase/auth'
  1. 建立 Google provider 實例,並呼叫 setCustomParameters(),此方法接受一個 configuration 物件作為參數,去定義這個 googleAuthProvider 要有什麼行為。這邊 configuration 物件填入 {prompt: "select_account"} ,意思是當每次有人接觸到我們的 provider,我們強制他一定要選擇一個帳號(若沒有加這個選項,使用者是在登入 Google 帳號狀態下會直接默認登入該帳號):
const provider = new GoogleAuthProvider();
provider.setCustomParameters({
	prompt: "select_account"
})
  1. 呼叫 getAuth()signInWithPopup() ,後者參數接收 getAuth() 的結果跟 provider ,最後 export 出去:
export const auth = getAuth();
export const signInWithGooglePopup = () => signInWithPopup(auth, provider);
  • 完整程式碼:

    ​​​​// firebase.utils.js
    ​​​​import {
    ​​​​  getAuth,
    ​​​​  signInWithRedirect,
    ​​​​  signInWithPopup,
    ​​​​  googleAuthProvider,
    ​​​​} from 'firebase/auth';
    ​​​​import { initializeApp } from 'firebase/app';
    ​​​​
    ​​​​const firebaseConfig = {
    ​​​​  apiKey: 'AIzaSyCtIONL9cEIXntjElT-w7gGXK9JG_NzN8w',
    ​​​​  authDomain: 'crwn-clothing-db-2a895.firebaseapp.com',
    ​​​​  projectId: 'crwn-clothing-db-2a895',
    ​​​​  storageBucket: 'crwn-clothing-db-2a895.appspot.com',
    ​​​​  messagingSenderId: '758180544862',
    ​​​​  appId: '1:758180544862:web:eeb130b62cb76c1186cfcf',
    ​​​​};
    ​​​​
    ​​​​// Initialize Firebase
    ​​​​const firebaseApp = initializeApp(firebaseConfig);
    ​​​​
    ​​​​const provider = new googleAuthProvider();
    ​​​​provider.setCustomParameters({
    ​​​​  prompt: 'select_account',
    ​​​​});
    ​​​​
    ​​​​export const auth = getAuth();
    ​​​​export const signInWithGooglePopup = () => signInWithPopup(auth, provider);
    
  1. 回 Firebase 網站,點選昨方欄 Authentication,選擇 Google 接著照下圖指示:

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  2. sign-in.component.jsx import utils 中的 signInWithGooglePopupsignInWithGooglePopup() 的結果會是一個 Promise 物件。

import { signInWithGooglePopup } from '../../utils/firebase/firebase.utils';

const SignIn = () => {
  const logGoogleUser = async () => {
    const response = await signInWithGooglePopup();
    console.log(response);
  };
  return (
    <div>
      <h1>i am SignIn</h1>
      <button onClick={logGoogleUser}>Sign in with Google Popup</button>
    </div>
  );
};

export default SignIn;

本小節完成後,進度如下,可以看到 await signInWithGooglePopup() 的結果:

user 的資訊,如 accessToken 跟 uid 等等……

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

注意(重要)

signInWithGooglePopup() 在做的事情是將透過 Google 登入的 user 通過 Firebase 的 Authentication 而已,並未寫入 Firestore 裡面:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

93. Introducing Firestore Data Models (Firestore 資料結構)

Collection > Document > Data

94. Setting Up User Documents 開始使用 Firestore

本小節情境:

承 93. 小節,使用者透過 Google 登入後,我們要把使用者的資料存進 Firestore 裡面!

啟用 Firestore

  1. 進入 Firebase 左方選擇 Firestore Database,選擇以正式版模式啟動

  2. Cloud Firestore 位置任選

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  3. 進入規則,將 allow read, write: if false 改為 allow read, write: if true ,這是為了讓我們可以對這個資料庫的 document 進行修改。

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

使用 Firestore API 將登入的使用者資料存入 Firestore

  1. src/utils/firebase/firebase.utils.js import 'firebase/firestore' 相關 API
import {
  getFirestore, // 用來創造一個 firestore 實例
  doc, // 用來創造一個 document 實例
  getDoc, // 取得 document data
  setDoc // 設定 document data
} from 'firebase/firestore';
  1. 建置 database (firestore 實例)
export const db = getFirestore();
  1. 建立一個 function 用來將登入的使用者資料存入 Firestore
    • doc() 建立一個 document 實例,有兩種語法:

      1. 接收三個參數,第一個是 database、第二個是 collections(可想像為資料表名稱),第三個是一個字串作為此 doc 的 title,本範例使用 uid (Unique ID) 去識別該項目,uid 為當使用者通過 Google 登入後,Firebase 回傳的 uid。
      2. 接收兩個參數,第一個是 collection 實例,第二個是一個字串作為此 doc 的 title。

      語法1:const userDocRef = doc(db, 'users', userAuth.uid);

      語法2:const docRef = doc(collectionRef, docTitle);

      補充:此時我們才剛建立 firestore,資料庫中我們沒有 userDocRef 的參照,根本也沒有 users collection,但是在呼叫 doc() 之後,firestore 還是會建立一個物件指向一個獨立的位址,讓我們把資料 set 進去。(這個物件的 ID 就是我們傳入 doc() 裡的 userAuth.uid

    • getDoc() 用來把一個 document 實例的資料取出來,參數接收一個 document 實例,會回傳 Promise 物件。

      語法:const userSnapshot = await getDoc(userDocRef);

      • getDoc() 的結果取出來後(用 await 或是 .then())可直接呼叫 exists() 去判定在此 DB 中的該 collection 裡面到底存不存在這個值,這會回傳一個布林值。
    • setDoc() 用來設定一個 document 實例的資料,第一個參數接收一個 document 實例,第二個是一個要寫進資料庫的物件,會回傳 Promise 物件。

      語法:await setDoc(userDocRef, {obj});

export const createUserDocumentFromAuth = async (userAuth) => {
	// 建立一個 document 實例
  const userDocRef = doc(db, 'users', userAuth.uid);
 
	// 將 document 實例的資料取出來
  const userSnapshot = await getDoc(userDocRef);
  console.log(userSnapshot);
  console.log(userSnapshot.exists());

	// 如果使用者不存在
  if (!userSnapshot.exists()) {
    const { displayName, email } = userAuth;
    const createdAt = new Date();
		// 就把資料寫進 Firestore
    try {
      await setDoc(userDocRef, {
        displayName,
        email,
        createdAt,
      });
    } catch (error) {
      console.log('建立使用者失敗' + error.message);
    }
  }

  // 如果使用者存在直接回傳 userDocRef
  return userDocRef;
};

97、98. Sign Up Form Pt.1 & Pt.2 + 99. Sign Up With Email + Password 信箱/密碼登入功能

本三節重點:

新增信箱/密碼的登入方式,藉由呼叫 createUserWithEmailAndPassword() 將透過使用信箱/密碼的 user ****通過 Firebase 的 Authentication。

  • createUserWithEmailAndPassword()signInWithGooglePopup() 都是將 user 通過 Firebase 的 Authentication,並且會回傳 user 的一些資訊,如 uid、accessToken 等等。

Firebase 設定

回到 Firebase 左方欄 Authentication,Sign-in method 新增電子郵件/密碼,如圖:

建置 sign-up-form 表單 ⇒ input 欄位掛 onChange 放進 useState

src/components/sign-up-form/sign-up-form.component.jsx

import { useState } from 'react';

const defaultFormFields = {
  displayName: '',
  email: '',
  password: '',
  confirmPassword: '',
};

const SignUpForm = () => {
  const [formFields, setFormFields] = useState(defaultFormFields);
  const { displayName, email, password, confirmPassword } = formFields;
 
  const handleChange = (event) => {
    const { name, value } = event.target;
    setFormFields({ ...formFields, [name]: value });
  };

  return (
    <div>
      <h1>Sign up with your email and password</h1>
      <form>
        <label>Display Name</label>
        <input
          type="text"
          required
          onChange={handleChange}
          name="displayName"
          value={displayName}
        />

        <label>Email</label>
        <input
          type="email"
          required
          onChange={handleChange}
          name="email"
          value={email}
        />

        <label>Password</label>
        <input
          type="password"
          required
          onChange={handleChange}
          name="password"
          value={password}
        />

        <label>Confirm Password</label>
        <input
          type="password"
          required
          onChange={handleChange}
          name="confirmPassword"
          value={confirmPassword}
        />

        <button type="submit">Sign Up</button>
      </form>
    </div>
  );
};

export default SignUpForm;

表單 onSumbit 要做的事情

  1. firebase.utils.js
  • 信箱/登入 sign-in method 是 Firebase 的 Native Provider 之一,所以只要 import 他的方法就好 ⇒ createUserWithEmailAndPassword。
  • 補充 1:createUserDocumentFromAuth (在 Firestore 建立一個 user document)新增接收 additionalInformation 參數是因為採用信箱/登入(呼叫 createUserWithEmailAndPassword(auth, email, password) )回傳的 user 資訊的 displayName 是 null,Firebase 應另有提供方法寫入 displayName 的值,但本課程講師決定當 user 以**信箱/登入,**displayName 就採用客戶端提供之 displayName input 值。additionalInformation 就是在 SignIn 時掛上 onChange 的 displayName 欄位的值。
// in firebase.utils.js
import {
  getAuth,
  signInWithRedirect,
  signInWithPopup,
  GoogleAuthProvider,
  createUserWithEmailAndPassword, // import **信箱/登入** sign-in method
} from 'firebase/auth';

// 中間略...

export const createUserDocumentFromAuth = async (
  userAuth,
  additionalInformation = {} // 補充 1
) => {
  if (!userAuth) return;
  const userDocRef = doc(db, 'users', userAuth.uid);
  const userSnapshot = await getDoc(userDocRef);

  // 如果使用者不存在就將資料寫進 Firestore
  if (!userSnapshot.exists()) {
    const { displayName, email } = userAuth;
    const createdAt = new Date();

    try {
      await setDoc(userDocRef, {
        displayName,
        email,
        createdAt,
        ...additionalInformation, // 補充 1
      });
    } catch (error) {
      console.log('建立使用者失敗' + error.message);
    }
  }

  // 如果使用者存在直接回傳 userDocRef
  return userDocRef;
};

export const createAuthUserWithEmailAndPassword = async (email, password) => {
  if (!email || !password) return;

  return await createUserWithEmailAndPassword(auth, email, password);
};
  1. sign-up-form.component.jsx
    1. import { createAuthUserWithEmailAndPassword } from '../../utils/firebase/firebase.utils';
    2. 表單送出時停止預設行為 ⇒ event.preventDefault()
    3. 比對密碼與確認密碼欄位。
    4. createAuthUserWithEmailAndPassword() 傳入 email, password 參數後呼叫後會通過 Firebase 的 Authentication 並回傳一些 user 資訊,如 accessToken、uid 等等但透過信箱/密碼方式登入不會有 displayName。
    5. createUserDocumentFromAuth() 傳入呼叫 createAuthUserWithEmailAndPassword() 回傳的 user 資訊跟客戶端提供的 displayName ⇒ 在 Firestore 存入該使用者資料
import { useState } from 'react';
import {
  createAuthUserWithEmailAndPassword,
  createUserDocumentFromAuth,
} from '../../utils/firebase/firebase.utils';

const defaultFormFields = {
  displayName: '',
  email: '',
  password: '',
  confirmPassword: '',
};

const SignUpForm = () => {
  const [formFields, setFormFields] = useState(defaultFormFields);
  const { displayName, email, password, confirmPassword } = formFields;

  const handleChange = (event) => {
    const { name, value } = event.target;
    setFormFields({ ...formFields, [name]: value });
  };

  const handleSubmit = async (event) => {
    event.preventDefault();
    if (password !== confirmPassword) {
      alert('passwords do not match');
      return;
    }
    try {
      const { user } = await createAuthUserWithEmailAndPassword(
        email,
        password
      );
      await createUserDocumentFromAuth(user, { displayName });
      setFormFields(defaultFormFields);
    } catch (error) {
      if (error.code === 'auth/email-already-in-use') {
        alert('Cannot create user, email already in use');
      }
      console.log('user creation encountered an error' + error);
    }
  };

  return (
    <div>
      <h1>Sign up with your email and password</h1>
      <form onSubmit={handleSubmit}>
	      // inputs 略...
        <button type="submit">Sign Up</button>
      </form>
    </div>
  );
};

export default SignUpForm;

100. Generalizing Form Input Component 美化&元件化 input

src/components 下新增 form-input 資料夾,底下再新增

  • form-input.component.jsx
    • 這裡 …otherProps 運用到剩餘屬性
import './form-input.styles.scss';
const FormInput = ({ label, ...othersProps }) => {
  return (
    <div className="group">
      {label && (
        <label
          className={`${
            othersProps.value.length ? 'shink' : ''
          } form-input-label`}
        >
          {label}
        </label>
      )}

      <input className="form-input" {...othersProps} />
    </div>
  );
};

export default FormInput;
  • form-input.styles.scss

101. Custom Button Component 按鈕元件化

本小節重點:

本專案中有三種相似 button,將 <button> 元件化並依據 class 名稱去顯示不同樣式。

  • src/components/button 新增 button.component.jsx
import './button.styles.scss';
const BUTTON_TYPE_CLASSES = {
  google: 'google-sign-in',
  inverted: 'inverted',
};

const Button = ({ children, buttonType, ...otherProps }) => {
  return (
    <button
      className={`button-container ${BUTTON_TYPE_CLASSES[buttonType]}`}
      {...otherProps} // 這裡的 otherProps 有 type="submit"
    >
      {children}
    </button>
  );
};

export default Button;

102. Sign In Form & 103. Finishing Authentication Page

本兩小節重點:

  • 新增 sign-in.component.jsx
  • 使用 Firebase API signInWithEmailAndPassword 進行登入
  1. firebase.utils.js import {signInWithEmailAndPassword} from 'firebase/auth';
  2. 傳入 auth 參數後 export 出去
export const signInAuthUserWithEmailAndPassword = async (email, password) => {
  if (!email || !password) return;

  return await signInWithEmailAndPassword(auth, email, password);
};
  1. sign-in-form.component.jsx import signInAuthUserWithEmailAndPassword,onSubmit 事件如下:
const handleSubmit = async (event) => {
    event.preventDefault();

    try {
      const response = await signInAuthUserWithEmailAndPassword(
        email,
        password
      );
      console.log(response);
      setFormFields(defaultFormFields);
    } catch (error) {
			// 錯誤處理
      switch (error.code) {
        case 'auth/wrong-password':
          alert('incorrect password for email');
          break;
        case 'auth/user-not-found':
          alert('no user associated with this email');
          break;
        default:
          console.log(error);
      }
    }

補充 return await Promise?