遷移至 Firebase JS SDK,可大幅提升網頁應用程式成效

1. 事前準備

模組化 Firebase JS SDK 是現有 JS SDK 的重寫版本,並將以下一個主要版本的形式發布。開發人員可以從 Firebase JS SDK 中排除未使用的程式碼,藉此建立較小的套件並提升效能。

模組化 JS SDK 最明顯的差異在於,功能現在會以您要匯入的自由浮動函式進行整理,而非在包含所有內容的單一 firebase 命名空間中。這種新的程式碼整理方式可讓您進行樹狀圖搖動,您將瞭解如何將目前使用 v8 Firebase JS SDK 的任何應用程式升級至新的模組化 SDK。

為提供順暢的升級程序,我們提供一組相容性套件。在本程式碼研究室中,您將瞭解如何使用相容性套件逐一移植應用程式。

建構項目

在本程式碼研究室中,您將逐步將現有的股票觀察清單網頁應用程式 (使用 v8 JS SDK) 遷移至新的模組化 JS SDK,分成三個階段:

  • 升級應用程式以使用相容性套件
  • 逐一將應用程式從相容性套件升級至模組化 API
  • 使用 Firestore Lite (Firestore SDK 的輕量實作方式),進一步提升應用程式的效能

2d351cb47b604ad7.png

本程式碼研究室著重於升級 Firebase SDK。我們不會對其他概念和程式碼區塊多做介紹,但會事先準備好這些程式碼區塊,屆時您只要複製及貼上即可。

事前準備

  • 你選擇的瀏覽器,例如 Chrome
  • 您偏好的 IDE/文字編輯器,例如 WebStormAtomSublimeVS Code
  • 套件管理工具 npm,通常會隨 Node.js 一併安裝
  • 程式碼研究室的程式碼範例 (請參閱程式碼研究室的下一個步驟,瞭解如何取得程式碼)。

2. 做好準備

取得程式碼

本專案所需的一切都位於 Git 存放區中。如要開始使用,請先取得程式碼,然後在您偏好的開發環境中開啟。

從指令列複製程式碼研究室的 GitHub 存放區

git clone https://github.com/FirebaseExtended/codelab-modular-sdk.git

或者,如果您未安裝 git,可以將存放區下載為 ZIP 檔案,然後解壓縮下載的 ZIP 檔案。

匯入應用程式

  1. 使用 IDE 開啟或匯入 codelab-modular-sdk 目錄。
  2. 執行 npm install 以安裝在本機建構及執行應用程式所需的依附元件。
  3. 執行 npm run build 以建構應用程式。
  4. 執行 npm run serve 以啟動網路伺服器
  5. 開啟瀏覽器分頁,前往 http://localhost:8080

71a8a7d47392e8f4.png

3. 建立基準

從哪裡開始?

您要從專為本次程式碼研究室所設計的股票觀察清單應用程式開始。為了說明本程式碼研究室中的概念,程式碼已簡化,且幾乎沒有錯誤處理。如果您選擇在實際執行應用程式中重複使用任何這類程式碼,請務必處理任何錯誤,並全面測試所有程式碼。

確認應用程式中的所有功能皆正常運作:

  1. 使用右上角的「登入」按鈕匿名登入。
  2. 登入後,請搜尋並將「NFLX」、「SBUX」和「T」加入待觀察清單,方法是按一下「Add」按鈕,輸入字母,然後點選下方彈出的搜尋結果列。
  3. 按一下列尾端的「x」,即可從觀察清單中移除股票。
  4. 查看股票價格的即時更新資訊。
  5. 開啟 Chrome 開發人員工具,前往「Network」分頁,然後勾選「Disable cache」和「Use large request rows」停用快取可確保系統在重新整理後,一律取得最新變更,而使用大型要求列可讓列同時顯示資源的傳輸大小和資源大小。在本程式碼研究室中,我們主要關注 main.js 的大小。

48a096debb2aa940.png

  1. 使用模擬節流功能,在不同網路連線狀況下載入應用程式。您將使用 慢速 3G 來測量本程式碼研究室中的載入時間,因為在這種情況下,較小的套件大小最有幫助。

4397cb2c1327089.png

接下來,我們將開始將應用程式遷移至新的模組化 API。

4. 使用相容性套件

相容性套件可讓您升級至新版 SDK,而無須一次變更所有 Firebase 程式碼。您可以逐步將這些 API 升級為模組化 API。

在這個步驟中,您將將 Firebase 程式庫從 v8 升級至新版本,並變更程式碼以使用相容性套件。在後續步驟中,您將瞭解如何先升級 Firebase Auth 程式碼,以便使用模組化 API,然後再升級 Firestore 程式碼。

在每個步驟結束時,您應該可以編譯及執行應用程式,且不會發生損壞情形,而且隨著每項產品的遷移作業,套件大小也會隨之縮小。

取得新的 SDK

找出 package.json 中的依附元件部分,並替換為以下內容:

package.json

"dependencies": {
    "firebase": "^9.0.0" 
}

重新安裝依附元件

由於我們變更了依附元件的版本,因此需要重新執行 npm install 才能取得新版的依附元件。

變更匯入路徑

相容性套件會在子模組 firebase/compat 下方公開,因此我們會據此更新匯入路徑:

  1. 前往檔案 src/firebase.ts
  2. 將現有的匯入項目替換為下列匯入項目:

src/firebase.ts

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

驗證應用程式是否正常運作

  1. 執行 npm run build 即可重建應用程式。
  2. 開啟瀏覽器分頁,前往 http://localhost:8080,或重新整理現有分頁。
  3. 使用應用程式進行遊戲,一切應該都會正常運作。

5. 升級 Auth 以使用模組化 API

您可以依任何順序升級 Firebase 產品。在本程式碼研究室中,您會先升級 Auth,以便瞭解基本概念,因為 Auth API 相對簡單。升級 Firestore 的程序稍微複雜一些,您將在下一節瞭解如何操作。

更新驗證初始化

  1. 前往檔案 src/firebase.ts
  2. 新增下列匯入項目:

src/firebase.ts

import { initializeAuth, indexedDBLocalPersistence } from 'firebase/auth';
  1. 刪除「import ‘firebase/compat/auth'.
  2. export const firebaseAuth = app.auth(); 替換為:

src/firebase.ts

export const firebaseAuth = initializeAuth(app, { persistence: [indexedDBLocalPersistence] });
  1. 移除檔案結尾的 export type User = firebase.User;User 會直接匯出至 src/auth.ts,您將在下一個步驟進行變更。

更新驗證碼

  1. 前往檔案 src/auth.ts
  2. 請將下列匯入項目新增至檔案頂端:

src/auth.ts

import { 
    signInAnonymously, 
    signOut,
    onAuthStateChanged,
    User
} from 'firebase/auth';
  1. 您已從 ‘firebase/auth'. 匯入 User,因此請從 import { firebaseAuth, User } from './firebase'; 中移除 User
  2. 更新函式,以便使用模組 API。

如您先前在更新匯入陳述式時所見,版本 9 中的套件會根據可匯入的函式進行排序,這與以點鏈結命名空間和服務模式為基礎的版本 8 API 不同。這種新的程式碼組織方式可讓建構工具分析哪些程式碼已使用,哪些程式碼未使用,進而啟用樹狀圖抖動未使用的程式碼。

在 9 版中,服務會做為第一個引數傳遞至函式。服務是指您在初始化 Firebase 服務時取得的物件,例如從 getAuth()initializeAuth() 傳回的物件。它們會保留特定 Firebase 服務的狀態,而函式會使用該狀態執行其工作。讓我們套用這個模式來實作下列函式:

src/auth.ts

export function firebaseSignInAnonymously() { 
    return signInAnonymously(firebaseAuth); 
} 

export function firebaseSignOut() { 
    return signOut(firebaseAuth); 
} 

export function onUserChange(callback: (user: User | null) => void) { 
    return onAuthStateChanged(firebaseAuth, callback); 
} 

export { User } from 'firebase/auth';

確認應用程式運作無誤

  1. 執行 npm run build 即可重建應用程式。
  2. 開啟瀏覽器分頁,前往 http://localhost:8080 或重新整理現有分頁
  3. 使用應用程式進行遊戲,一切應該都會正常運作。

檢查套件大小

  1. 開啟 Chrome 開發人員工具。
  2. 切換至「Network」分頁。
  3. 重新整理網頁以擷取網路要求。
  4. 找出 main.js 並檢查其大小。您只需更改幾行程式碼,就能縮減套件大小 100 KB (經 GZIP 壓縮後為 36 KB),也就是縮減約 22%!在 3G 連線速度緩慢的情況下,網站的載入速度也加快了 0.75 秒。

2e4eafaf66cd829b.png

6. 升級 Firebase 應用程式和 Firestore,以便使用模組化 API

更新 Firebase 初始化程序

  1. 前往檔案 src/firebase.ts.
  2. import firebase from ‘firebase/compat/app'; 替換為:

src/firebase.ts

import { initializeApp } from 'firebase/app';
  1. const app = firebase.initializeApp({...}); 替換為:

src/firebase.ts

const app = initializeApp({
    apiKey: "AIzaSyBnRKitQGBX0u8k4COtDTILYxCJuMf7xzE", 
    authDomain: "exchange-rates-adcf6.firebaseapp.com", 
    databaseURL: "https://exchange-rates-adcf6.firebaseio.com", 
    projectId: "exchange-rates-adcf6", 
    storageBucket: "exchange-rates-adcf6.firebasestorage.app", 
    messagingSenderId: "875614679042", 
    appId: "1:875614679042:web:5813c3e70a33e91ba0371b"
});

更新 Firestore 初始化程序

  1. 在同一個檔案 src/firebase.ts, 中,將 import 'firebase/compat/firestore'; 替換為

src/firebase.ts

import { getFirestore } from 'firebase/firestore';
  1. export const firestore = app.firestore(); 替換為:

src/firebase.ts

export const firestore = getFirestore();
  1. 移除「export const firestore = ...」後面的所有行

更新匯入作業

  1. 開啟檔案 src/services.ts.
  2. 從匯入內容中移除 FirestoreFieldPathFirestoreFieldValueQuerySnapshot。從 './firebase' 匯入的內容現在應如下所示:

src/services.ts

import { firestore } from './firebase';
  1. 在檔案頂端匯入要使用的函式和類型:
    **src/services.ts**
import { 
    collection, 
    getDocs, 
    doc, 
    setDoc, 
    arrayUnion, 
    arrayRemove, 
    onSnapshot, 
    query, 
    where, 
    documentId, 
    QuerySnapshot
} from 'firebase/firestore';
  1. 建立參照資料,指向包含所有股票代碼的集合:

src/services.ts

const tickersCollRef = collection(firestore, 'current');
  1. 使用 getDocs() 擷取集合中的所有文件:

src/services.ts

const tickers = await getDocs(tickersCollRef);

如需完成的程式碼,請參閱 search()

更新 addToWatchList()

使用 doc() 建立使用者觀察清單的文件參照,然後使用 setDoc() 搭配 arrayUnion() 為觀察清單新增資訊流:

src/services.ts

export function addToWatchList(ticker: string, user: User) {
      const watchlistRef = doc(firestore, `watchlist/${user.uid}`);
      return setDoc(watchlistRef, {
       tickers: arrayUnion(ticker)
   }, { merge: true });
}

更新 deleteFromWatchList()

同樣地,您也可以使用 setDoc() 搭配 arrayRemove(),從使用者的待觀看影劇清單中移除資訊串流:

src/services.ts

export function deleteFromWatchList(ticker: string, user: User) {
   const watchlistRef = doc(firestore, `watchlist/${user.uid}`);
   return setDoc(watchlistRef, {
       tickers: arrayRemove(ticker)
   }, { merge: true });
}

更新 subscribeToTickerChanges()

  1. 請先使用 doc() 建立使用者待辦清單的文件參照,然後使用 onSnapshot() 監聽待辦清單變更:

src/services.ts

const watchlistRef = doc(firestore, `watchlist/${user.uid}`);
const unsubscribe = onSnapshot(watchlistRef, snapshot => {
   /* subscribe to ticker price changes */
});
  1. 在觀察清單中加入代碼後,請使用 query() 建立查詢來擷取代碼價格,並使用 onSnapshot() 監聽價格變動:

src/services.ts

const priceQuery = query(
    collection(firestore, 'current'),
    where(documentId(), 'in', tickers)
);
unsubscribePrevTickerChanges = onSnapshot(priceQuery, snapshot => {
               if (firstload) {
                   performance && performance.measure("initial-data-load");
                   firstload = false;
                   logPerformance();
               }
               const stocks = formatSDKStocks(snapshot);
               callback(stocks);
  });

如需查看完整實作方式,請參閱 subscribeToTickerChanges()

更新 subscribeToAllTickerChanges()

首先,您會使用 collection() 建立參照,以便先取得包含所有代碼價格的集合,然後使用 onSnapshot() 監聽價格變更:

src/services.ts

export function subscribeToAllTickerChanges(callback: TickerChangesCallBack) {
   const tickersCollRef = collection(firestore, 'current');
   return onSnapshot(tickersCollRef, snapshot => {
       if (firstload) {
           performance && performance.measure("initial-data-load");
           firstload = false;
           logPerformance();
       }
       const stocks = formatSDKStocks(snapshot);
       callback(stocks);
   });
}

確認應用程式運作無誤

  1. 執行 npm run build 即可重建應用程式。
  2. 開啟瀏覽器分頁,前往 http://localhost:8080,或重新整理現有分頁
  3. 使用應用程式進行遊戲,一切應該都會正常運作。

檢查套件大小

  1. 開啟 Chrome 開發人員工具。
  2. 切換至「Network」分頁。
  3. 重新整理網頁以擷取網路要求。
  4. 找出 main.js 並檢查其大小。再比較一下原始套件大小,我們已將套件大小縮減超過 200 KB (經 GZIP 壓縮後為 63.8 KB),也就是縮小了 50%,因此載入時間也縮短了 1.3 秒!

7660cdc574ee8571.png

7. 使用 Firestore Lite 加快初始網頁轉譯速度

什麼是 Firestore Lite?

Firestore SDK 提供複雜的快取、即時串流、永久性儲存空間、多分頁離線同步、重試、樂觀並行處理等功能,因此檔案大小相當大。但您可能只想取得一次資料,而不需要任何進階功能。針對這些情況,Firestore 推出了簡單輕量級的解決方案,也就是全新的 Firestore Lite 套件。

Firestore Lite 的一個絕佳用途,就是改善初始網頁轉譯的效能,您只需要知道使用者是否已登入,然後從 Firestore 讀取一些資料來顯示。

在這個步驟中,您將瞭解如何使用 Firestore Lite 減少套件大小,加快初始網頁轉譯速度,然後動態載入主要 Firestore SDK 來訂閱即時更新。

您將重構程式碼,以便:

  1. 將即時服務移至個別檔案,以便使用動態匯入功能動態載入。
  2. 建立新的函式,使用 Firestore Lite 擷取觀察清單和股票價格。
  3. 使用新的 Firestore Lite 函式擷取資料,以便初始頁面算繪,然後動態載入即時服務,以便監聽即時更新。

將即時服務移至新檔案

  1. 建立名為 src/services.realtime.ts. 的新檔案
  2. subscribeToTickerChanges()subscribeToAllTickerChanges() 函式從 src/services.ts 移至新檔案。
  3. 在新的檔案頂端新增必要的匯入項目。

您仍需要在此處進行幾項變更:

  1. 首先,請在檔案頂端使用主要 Firestore SDK 建立 Firestore 例項,以便在函式中使用。您無法在此匯入來自 firebase.ts 的 Firestore 例項,因為您將在幾個步驟中將其變更為 Firestore Lite 例項,該例項只會用於初始網頁轉譯。
  2. 其次,請移除 firstload 變數和由其保護的 if 區塊。這些函式的功能會移至您在下一個步驟中建立的新函式。

src/services.realtime.ts

import { User } from './auth'
import { TickerChange } from './models';
import { collection, doc, onSnapshot, query, where, documentId, getFirestore } from 'firebase/firestore';
import { formatSDKStocks } from './services';

const firestore = getFirestore();
type TickerChangesCallBack = (changes: TickerChange[]) => void

export function subscribeToTickerChanges(user: User, callback: TickerChangesCallBack) {

   let unsubscribePrevTickerChanges: () => void;

   // Subscribe to watchlist changes. We will get an update whenever a ticker is added/deleted to the watchlist
   const watchlistRef = doc(firestore, `watchlist/${user.uid}`);
   const unsubscribe = onSnapshot(watchlistRef, snapshot => {
       const doc = snapshot.data();
       const tickers = doc ? doc.tickers : [];

       if (unsubscribePrevTickerChanges) {
           unsubscribePrevTickerChanges();
       }

       if (tickers.length === 0) {
           callback([]);
       } else {
           // Query to get current price for tickers in the watchlist
           const priceQuery = query(
               collection(firestore, 'current'),
               where(documentId(), 'in', tickers)
           );

           // Subscribe to price changes for tickers in the watchlist
           unsubscribePrevTickerChanges = onSnapshot(priceQuery, snapshot => {
               const stocks = formatSDKStocks(snapshot);
               callback(stocks);
           });
       }
   });
   return () => {
       if (unsubscribePrevTickerChanges) {
           unsubscribePrevTickerChanges();
       }
       unsubscribe();
   };
}

export function subscribeToAllTickerChanges(callback: TickerChangesCallBack) {
   const tickersCollRef = collection(firestore, 'current');
   return onSnapshot(tickersCollRef, snapshot => {
       const stocks = formatSDKStocks(snapshot);
       callback(stocks);
   });
}

使用 Firestore Lite 擷取資料

  1. 未解決 src/services.ts.
  2. 將匯入路徑從 ‘firebase/firestore' 變更為 ‘firebase/firestore/lite',,新增 getDoc,並從匯入清單中移除 onSnapshot:

src/services.ts

import { 
    collection, 
    getDocs, 
    doc, 
    setDoc, 
    arrayUnion, 
    arrayRemove,
//  onSnapshot, // firestore lite doesn't support realtime updates
    query, 
    where, 
    documentId, 
    QuerySnapshot, 
    getDoc // add this import
} from 'firebase/firestore/lite';
  1. 新增函式,使用 Firestore Lite 擷取初始網頁轉譯所需的資料:

src/services.ts

export async function getTickerChanges(tickers: string[]): Promise<TickerChange[]> {

   if (tickers.length === 0) {
       return [];
   }

   const priceQuery = query(
       collection(firestore, 'current'),
       where(documentId(), 'in', tickers)
   );
   const snapshot = await getDocs(priceQuery);
   performance && performance.measure("initial-data-load");
   logPerformance();
   return formatSDKStocks(snapshot);
}

export async function getTickers(user: User): Promise<string[]> {
   const watchlistRef = doc(firestore, `watchlist/${user.uid}`);
   const data =  (await getDoc(watchlistRef)).data();

   return data ? data.tickers : [];
}

export async function getAllTickerChanges(): Promise<TickerChange[]> {
   const tickersCollRef = collection(firestore, 'current');
   const snapshot = await getDocs(tickersCollRef);
   performance && performance.measure("initial-data-load");
   logPerformance();
   return formatSDKStocks(snapshot);
}
  1. 開啟 src/firebase.ts,將匯入路徑從 ‘firebase/firestore' 變更為 ‘firebase/firestore/lite':

src/firebase.ts

import { getFirestore } from 'firebase/firestore/lite';

將所有內容整合在一起

  1. 未解決 src/main.ts.
  2. 您需要新建立的函式,才能擷取初始頁面轉譯的資料,以及幾個輔助函式來管理應用程式狀態。因此,現在請更新匯入內容:

src/main.ts

import { renderLoginPage, renderUserPage } from './renderer';
import { getAllTickerChanges, getTickerChanges, getTickers } from './services';
import { onUserChange } from './auth';
import { getState, setRealtimeServicesLoaded, setUser } from './state';
import './styles.scss';
  1. 在檔案頂端使用動態匯入功能載入 src/services.realtime。變數 loadRealtimeService 是應保證,會在程式碼載入後,與即時服務解析。稍後您會用它訂閱即時更新。

src/main.ts

const loadRealtimeService = import('./services.realtime');
loadRealtimeService.then(() => {
   setRealtimeServicesLoaded(true);
});
  1. onUserChange() 的回呼變更為 async 函式,以便在函式主體中使用 await

src/main.ts

onUserChange(async user => {
 // callback body
});
  1. 接著擷取資料,使用我們在先前步驟中建立的新函式,初始頁面呈現。

onUserChange() 回呼中,找出使用者登入的 if 條件,然後複製並貼到 if 陳述式中:

src/main.ts

onUserChange(async user => {
      // LEAVE THE EXISTING CODE UNCHANGED HERE
      ...

      if (user) {
       // REPLACE THESE LINES

       // user page
       setUser(user);

       // show loading screen in 500ms
       const timeoutId = setTimeout(() => {
           renderUserPage(user, {
               loading: true,
               tableData: []
           });
       }, 500);

       // get data once if realtime services haven't been loaded
       if (!getState().realtimeServicesLoaded) {
           const tickers = await getTickers(user);
           const tickerData = await getTickerChanges(tickers);
           clearTimeout(timeoutId);
           renderUserPage(user, { tableData: tickerData });
       }

       // subscribe to realtime updates once realtime services are loaded
       loadRealtimeService.then(({ subscribeToTickerChanges }) => {
           unsubscribeTickerChanges = subscribeToTickerChanges(user, stockData => {
               clearTimeout(timeoutId);
               renderUserPage(user, { tableData: stockData })
           });
       });
   } else {
     // DON'T EDIT THIS PART, YET   
   }
}
  1. 在沒有使用者登入的 else 區塊中,使用 Firestore Lite 擷取所有股票的價格資訊、轉譯網頁,然後在載入即時服務後監聽價格變更:

src/main.ts

if (user) {
   // DON'T EDIT THIS PART, WHICH WE JUST CHANGED ABOVE
   ...
} else {
   // REPLACE THESE LINES

   // login page
   setUser(null);

   // show loading screen in 500ms
   const timeoutId = setTimeout(() => {
       renderLoginPage('Landing page', {
           loading: true,
           tableData: []
       });
   }, 500);

   // get data once if realtime services haven't been loaded
   if (!getState().realtimeServicesLoaded) {
       const tickerData = await getAllTickerChanges();
       clearTimeout(timeoutId);
       renderLoginPage('Landing page', { tableData: tickerData });
   }

   // subscribe to realtime updates once realtime services are loaded
   loadRealtimeService.then(({ subscribeToAllTickerChanges }) => {
       unsubscribeAllTickerChanges = subscribeToAllTickerChanges(stockData => {
           clearTimeout(timeoutId);
           renderLoginPage('Landing page', { tableData: stockData })
       });
   });
}

如需查看完成的程式碼,請參閱 src/main.ts

確認應用程式運作無誤

  1. 執行 npm run build 即可重建應用程式。
  2. 開啟瀏覽器分頁,前往 http://localhost:8080,或重新整理現有分頁。

檢查套件大小

  1. 開啟 Chrome 開發人員工具。
  2. 切換至「Network」分頁。
  3. 重新整理網頁以擷取網路要求
  4. 找出 main.js 並檢查其大小。
  5. 目前只有 115 KB (經 GZIP 壓縮後為 34.5 KB)。比原始套件的大小(446 KB,經 GZIP 壓縮後為 138 KB) 小了 75%!因此,在 3G 連線下,網站的載入速度加快了 2 秒以上,大幅改善效能和使用者體驗!

9ea7398a8c8ef81b.png

8. 恭喜!

恭喜,您已成功升級應用程式,讓應用程式變得更小、更快速!

您使用了相容性套件逐一升級應用程式,並使用 Firestore Lite 加快初始網頁轉譯作業,然後動態載入主要 Firestore 以串流傳輸價格變更。

您也縮減了套件大小,並在本程式碼研究室中縮短了載入時間:

main.js

資源大小 (KB)

經過 GZIP 壓縮的大小 (KB)

載入時間 (秒) (使用緩慢的 3g)

v8

446

138

4.92

v9 相容性

429

124

4.65

僅限 v9 模組式驗證

348

102

4.2

v9 完全模組化

244

74.6

3.66

v9 完全模組化 + Firestore lite

117

34.9

2.88

32a71bd5a774e035.png

您現在已瞭解升級使用 v8 Firebase JS SDK 的網頁應用程式,以便使用新的模組化 JS SDK 所需的重要步驟。

延伸閱讀

參考文件