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

1.准备工作

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

模块化 JS SDK 最明显的区别在于,现在各项功能以您将导入的自由浮动函数进行组织,而不是在单个 firebase 命名空间中(包含所有内容)。这种新的代码组织方式支持摇树优化。您还将学习如何将当前使用 Firebase JS SDK v8 的任何应用升级为新的模块化应用。

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

构建内容

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

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

2d351cb47b604ad7

此 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

3. 建立基准

从何处入手?

首先,您需要构建一款专为此 Codelab 设计的股票监控列表应用。为了说明此 Codelab 中的概念,代码已经过简化,几乎没有错误处理。如果您选择在正式版应用中重复使用上述任何代码,请确保能处理所有错误并全面测试所有代码。

确保应用中一切正常:

  1. 使用右上角的登录按钮匿名登录。
  2. 登录后,搜索并添加“NFLX”“SBUX”和“T”添加到监控列表,方法是点击添加按钮,输入字母,然后点击下方弹出的搜索结果行。
  3. 点击股票行尾的 x,即可将股票从监控列表中移除。
  4. 关注股价的实时更新。
  5. 打开 Chrome 开发者工具,转到网络标签页,然后勾选停用缓存使用大型请求行停用缓存可确保我们在刷新后始终获得最新的更改,而使用大型请求行则会让该行同时显示传输的资源大小和资源大小。在此 Codelab 中,我们主要关注 main.js 的大小。

48a096debb2aa940

  1. 使用模拟节流在不同网络条件下加载应用。在此 Codelab 中,您将使用 Slow 3G 来测量加载时间,因为在这方面,越小的软件包帮助越大。

4397cb2c1327089

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

4. 使用兼容性软件包

借助兼容性软件包,您可以升级到新的 SDK 版本,而无需一次性更改所有 Firebase 代码。您可以逐步将其升级为模块化 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 产品。在此 Codelab 中,您首先要升级 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. 将以下 import 语句添加到文件顶部:

src/auth.ts

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

正如我们之前在更新 import 语句时所看到的,版本 9 中的软件包是围绕您可以导入的函数组织的,而版本 8 API 则是基于点链式命名空间和服务模式的 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 并检查其大小。通过只更改几行代码,您已将 app bundle 的大小缩减了 100KB(gzip 压缩后为 36KB),即缩减了约 22%!此外,在速度较慢的 3G 连接下,该网站的加载速度提高了 0.75 秒。

2e4eafaf66cd829b

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.appspot.com", 
    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. 切换到网络标签页。
  3. 请刷新页面以捕获网络请求。
  4. 查找 main.js 并检查其大小。再次将其与原始软件包大小进行比较:我们使软件包的大小缩减了超过 200KB(gzip 压缩后为 63.8KB),或缩减了 50%,这相当于缩短了加载时间 1.3 秒!

7660cdc574ee8571

7. 使用 Firestore Lite 加快初始页面渲染速度

什么是 Firestore Lite?

Firestore SDK 提供复杂的缓存、实时流式传输、永久性存储、多标签页离线同步、重试、乐观并发以及更多功能,因此其体量相当大。但您可能只想获取一次数据,而无需使用任何高级功能。针对这些情况,Firestore 创建了一个简单的轻量级解决方案,即一个全新的软件包 - Firestore Lite。

Firestore Lite 的一个绝佳使用场景是优化初始页面渲染的性能。在这种情况下,您只需知道用户是否已登录,然后从 Firetore 读取一些数据进行显示。

在此步骤中,您将学习如何使用 Firestore Lite 减小 bundle 大小以加快初始页面渲染速度,然后动态加载主 Firestore SDK 以订阅实时更新。

您将重构代码以执行以下操作:

  1. 将实时服务移至单独的文件,以便使用动态导入功能动态加载这些服务。
  2. 创建新函数以使用 Firestore Lite 检索监控列表和股票价格。
  3. 使用新的 Firestore Lite 函数检索数据,以进行初始页面渲染,然后动态加载实时服务以监听实时更新。

将实时服务移动到新文件

  1. 新建一个名为 src/services.realtime.ts. 的文件
  2. 将函数 subscribeToTickerChanges()subscribeToAllTickerChanges()src/services.ts 移至新文件中。
  3. 在新文件的顶部添加必要的 import 语句。

您仍然需要在此处进行一些更改:

  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. 您将需要新创建的函数来提取初始页面渲染的数据,还需要几个辅助函数来管理应用状态。现在,更新 import 语句:

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,会在代码加载完成后通过实时服务进行解析。稍后您将使用它来订阅实时动态。

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. 现在,它的大小仅为 115KB(Gzip 压缩后的大小为 34.5KB)。这比原始软件包大小小 75%,即 446KB(gzip 压缩后 138KB)!因此,该网站在采用 3G 连接的情况下加载速度提高了 2 秒以上,这在性能和用户体验方面都得到了极大的改善!

9ea7398a8c8ef81b

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

现在,您已了解将使用 v8 Firebase JS SDK 的 Web 应用升级为使用新的模块化 JS SDK 所需的关键步骤。

深入阅读

参考文档