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

1. 事前準備

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

模組化 JS SDK 最明顯的差異在於,功能現在會以您要匯入的自由浮動函式形式呈現,而不是包含所有內容的單一 firebase 命名空間。這種新的程式碼組織方式可進行樹狀結構修剪,您將瞭解如何將目前使用 v8 Firebase JS 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」加入追蹤清單。
  3. 如要從追蹤清單中移除股票,請按一下該列末端的「x」
  4. 即時掌握股價變動。
  5. 開啟 Chrome 開發人員工具,前往「網路」分頁,然後勾選「停用快取」和「使用大型要求列」停用快取可確保我們在重新整理後一律取得最新變更,而使用大型要求列則可讓列同時顯示資源的傳輸大小和資源大小。在本程式碼研究室中,我們主要關注 main.js 的大小。

48a096debb2aa940.png

  1. 使用模擬節流限制,在不同網路連線狀況下載入應用程式。在本程式碼研究室中,您將使用「Slow 3G」測量載入時間,因為在這種情況下,縮減套件大小最有幫助。

4397cb2c1327089.png

現在就開始將應用程式遷移至新的模組化 API。

4. 使用相容性套件

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

在這個步驟中,您會將 Firebase 程式庫從第 8 版升級至新版本,並變更程式碼以使用相容性套件。在接下來的步驟中,您將瞭解如何先升級 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 的程序較為複雜,我們會在下一節說明如何升級。

更新 Auth 初始化

  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. 切換至「網路」分頁。
  3. 重新整理頁面,擷取網路要求。
  4. 找出 main.js 並檢查其大小。您只變更了幾行程式碼,就將套件大小縮減了 100 KB (壓縮後為 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()

同樣地,使用  和 arrayRemove(),即可從使用者的待觀看影劇清單中移除股票代號:setDoc()

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. 切換至「網路」分頁。
  3. 重新整理頁面,擷取網路要求。
  4. 找出 main.js 並檢查其大小。再次與原始套件大小比較,我們已將套件大小縮減超過 200 KB (壓縮後為 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. 切換至「網路」分頁。
  3. 重新整理頁面以擷取網路要求
  4. 找出 main.js 並檢查其大小。
  5. 現在只有 115 KB (壓縮後為 34.5 KB)。這比原始套件大小(446 KB,壓縮後為 138 KB) 小了 75%!因此,網站透過 3G 連線載入的速度加快了 2 秒以上,效能和使用者體驗都大幅提升!

9ea7398a8c8ef81b.png

8. 恭喜

恭喜,您已成功升級應用程式,並縮小應用程式大小及提升速度!

您使用相容性套件逐步升級應用程式,並使用 Firestore Lite 加快初始網頁的算繪速度,然後動態載入主要 Firestore,串流價格變更。

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

main.js

資源大小 (KB)

gzip 壓縮後的大小 (kb)

載入時間 (秒) (透過慢速 3G)

v8

446

138

4.92

v9 compat

429

124

4.65

僅限 v9 的模組化 Auth

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。

延伸閱讀

參考文件