通过迁移到模块化 Firebase JS SDK,让您的 Web 应用更上一层楼

1. 准备工作

模块化 Firebase JS SDK 是对现有 JS SDK 的重新编写,将作为下一个主要版本发布。借助该功能,开发者可以从 Firebase JS SDK 中排除未使用的代码,以创建更小的软件包并实现更好的性能。

模块化 JS SDK 中最明显的区别在于,功能现在是按您要导入的自由浮动函数进行整理的,而不是在包含所有内容的单个 firebase 命名空间中进行整理。这种新的代码组织方式支持树摇动,您将了解如何将目前使用 v8 版 Firebase JS SDK 的任何应用升级到新的模块化 SDK。

为了顺利完成升级流程,我们提供了一组兼容性软件包。在此 Codelab 中,您将学习如何使用兼容性软件包逐个移植应用。

构建内容

在此 Codelab 中,您将分三个阶段逐步将使用 v8 JS SDK 的现有股票观察列表 Web 应用迁移到新的模块化 JS SDK:

  • 升级应用以使用兼容性软件包
  • 逐步将应用从兼容性软件包升级到模块化 API
  • 使用 Firestore Lite(Firestore SDK 的轻量级实现)进一步提升应用的性能

2d351cb47b604ad7.png

此 Codelab 重点介绍如何升级 Firebase SDK。对于其他概念和代码块,我们仅会略作介绍,但是会提供相应代码块供您复制和粘贴。

所需条件

  • 您所选的浏览器(例如 Chrome)
  • 您选择的 IDE/文本编辑器,例如 WebStormAtomSublimeVS Code
  • 软件包管理器 npm(通常随 Node.js 一起提供)
  • 此 Codelab 的示例代码(请参阅 Codelab 的下一步,了解如何获取代码)。

2. 进行设置

获取代码

您完成此项目所需的一切都位于一个 Git 代码库中。首先,您需要获取相关代码,然后在您常用的开发环境中将其打开。

通过命令行克隆此 Codelab 的 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 以启动 Web 服务器
  5. 打开一个浏览器标签页以访问 http://localhost:8080

71a8a7d47392e8f4.png

3. 建立基准

从何处入手?

首先,针对此 Codelab 设计一个股票监视列表应用。为了说明此 Codelab 中的概念,代码已简化,而且几乎没有错误处理。如果您选择在正式版应用中重复使用该代码的任意部分,请务必处理所有错误并全面测试所有代码。

确保应用中的所有功能均正常运行:

  1. 使用右上角的登录按钮匿名登录。
  2. 登录后,点击添加按钮,输入字母,然后点击下方随即弹出的搜索结果行,以搜索并将“NFLX”“SBUX”和“T”添加到观看列表。
  3. 点击相应行末尾的 x 即可将相应股票从“观察名单”中移除。
  4. 观看股票价格的实时更新。
  5. 打开 Chrome 开发者工具,前往 Network(网络)标签页,然后选中 Disable cache(停用缓存)和 Use large request rows(使用大型请求行)。停用缓存可确保我们在刷新后始终获取最新更改,使用大量请求行可让行同时显示传输大小和资源大小。在此 Codelab 中,我们主要关注 main.js 的大小。

48a096debb2aa940.png

  1. 使用模拟的节流功能在不同的网络条件下加载应用。在本 Codelab 中,您将使用慢速 3G 来衡量加载时间,因为在这种网络环境下,较小的软件包大小最有帮助。

4397cb2c1327089.png

现在,开始将应用迁移到新的模块化 API。

4. 使用兼容性软件包

借助兼容性软件包,您无需一次性更改所有 Firebase 代码,即可升级到新版 SDK。您可以逐步将其升级为模块化 API。

在此步骤中,您将 Firebase 库从 v8 升级到新版本,并更改代码以使用兼容性软件包。在以下步骤中,您将学习如何先升级 Firebase Auth 代码以使用模块化 API,然后再升级 Firestore 代码。

在完成每个步骤后,您应该能够编译和运行应用,且不会出现问题。随着我们迁移每个产品,软件包大小也会随之缩减。

获取新版 SDK

package.json 中找到“dependencies”部分,并将其替换为以下内容:

package.json

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

重新安装依赖项

由于我们更改了依赖项的版本,因此需要重新运行 npm install 以获取新版本的依赖项。

更改导入路径

兼容性软件包在子模块 firebase/compat 下公开,因此我们将相应地更新导入路径:

  1. 前往文件 src/firebase.ts
  2. 将现有的 import 替换为以下 import:

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 产品。在此 Codelab 中,您将先升级 Auth,以便学习基本概念,因为 Auth API 相对简单。升级 Firestore 需要完成一些额外的步骤,您将在下文中了解如何执行这些步骤。

更新了身份验证初始化

  1. 前往文件 src/firebase.ts
  2. 添加以下 import 语句:

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 中。

更新 Auth 代码

  1. 前往文件 src/auth.ts
  2. 将以下 import 添加到文件顶部:

src/auth.ts

import { 
    signInAnonymously, 
    signOut,
    onAuthStateChanged,
    User
} from 'firebase/auth';
  1. import { firebaseAuth, User } from './firebase'; 中移除 User,因为您已从 ‘firebase/auth'. 导入 User
  2. 更新函数以使用模块化 API。

如我们在更新导入语句时所述,版本 9 中的软件包是围绕您可以导入的函数进行组织的,而版本 8 的 API 则基于点链命名空间和服务模式。正是这种新的代码组织方式让系统能够摇动树来移除未使用的代码,因为它允许构建工具分析哪些代码已被使用,哪些代码未被使用。

在 v9 中,服务作为第一个参数传递给函数。服务是指您通过初始化 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 并检查其大小。只需更改几行代码,您就将软件包大小缩减了 100KB(经过 Gzip 压缩后为 36 KB),缩减幅度约为 22%!在 3G 网络连接速度较慢的情况下,该网站的加载速度也加快了 0.75 秒。

2e4eafaf66cd829b.png

6. 升级 Firebase App 和 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 并检查其大小。再次将其与原始软件包大小进行比较 - 我们将软件包大小缩减了 200KB 以上(经过 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 是一个 Promise,它会在代码加载后解析为实时服务。稍后,您将使用该 ID 订阅实时更新。

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. 现在,它只有 115KB(经过 Gzip 压缩后为 34.5KB)。这比原始软件包大小(446KB,经过 Gzip 压缩为 138KB)缩小了 75%!因此,在 3G 连接下,网站的加载速度提高了 2 秒以上,性能和用户体验得到了显著提升!

9ea7398a8c8ef81b.png

8. 恭喜

恭喜,您已成功升级应用,使其变得更小、更快!

您使用兼容性软件包逐个升级了应用,并使用 Firestore Lite 加快了初始页面呈现速度,然后动态加载主 Firestore 以流式传输价格变动。

在本 Codelab 的学习过程中,您还缩减了软件包大小并缩短了其加载时间:

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 的 Web 应用升级为使用新的模块化 JS SDK 所需执行的主要步骤。

深入阅读

参考文档