通过迁移到模块化 Firebase JS SDK 为您的 Web 应用注入强大动力

1. 准备工作

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

模块化 JS SDK 最明显的不同之处在于,功能现在以您将导入的自由浮动函数的形式组织,而不是像以前那样包含在单个 firebase 命名空间中。这种新的代码组织方式可实现树状结构抖动,您将了解如何将目前使用 v8 Firebase JS 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 开发者工具,前往网络标签页,然后选中停用缓存使用大型请求行停用缓存可确保我们在刷新后始终获得最新更改,而使用大量请求行可使行同时显示资源的传输大小和资源大小。在此 Codelab 中,我们主要关注 main.js 的大小。

48a096debb2aa940.png

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

4397cb2c1327089.png

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

4. 使用兼容性软件包

借助兼容性软件包,您无需一次性更改所有 Firebase 代码,即可升级到新的 SDK 版本。您可以逐步将它们升级为模块化 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. 升级身份验证以使用模块化 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 中。

更新授权代码

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

src/auth.ts

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

正如您之前在更新 import 语句时所见,版本 9 中的软件包围绕您可以导入的函数进行组织,而版本 8 的 API 基于点链命名空间和服务模式。正是这种新的代码组织方式使得可以对未使用的代码进行 tree shaking,因为它可以让 build 工具分析哪些代码被使用,哪些代码未被使用。

在版本 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() 创建指向用户关注列表的文档引用,然后使用 arrayUnion() 通过 setDoc() 向其中添加股票代码:

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 并检查其大小。再次将其与原始软件包大小进行比较 - 我们已将软件包大小减少了 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 是一个 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)。这比原来的软件包大小(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 的模块化 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 的 Web 应用升级为使用新的模块化 JS SDK 所需的关键步骤。

深入阅读

参考文档