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 的轻量级实现)进一步提升应用的性能
此 Codelab 重点介绍如何升级 Firebase SDK。对于其他概念和代码块,我们仅会略作介绍,但是会提供相应代码块供您复制和粘贴。
所需条件
2. 进行设置
获取代码
您完成此项目所需的一切都位于一个 Git 代码库中。首先,您需要获取相关代码,然后在您常用的开发环境中将其打开。
通过命令行克隆此 Codelab 的 GitHub 代码库:
git clone https://github.com/FirebaseExtended/codelab-modular-sdk.git
或者,如果您尚未安装 git,可以以 ZIP 文件的形式下载代码库,然后解压缩下载的 ZIP 文件。
导入应用
- 使用 IDE 打开或导入
codelab-modular-sdk
目录。 - 运行
npm install
以安装在本地构建和运行应用所需的依赖项。 - 运行
npm run build
以构建应用。 - 运行
npm run serve
以启动 Web 服务器 - 打开一个浏览器标签页以访问 http://localhost:8080
3. 建立基准
从何处入手?
首先,针对此 Codelab 设计一个股票监视列表应用。为了说明此 Codelab 中的概念,代码已简化,而且几乎没有错误处理。如果您选择在正式版应用中重复使用该代码的任意部分,请务必处理所有错误并全面测试所有代码。
确保应用中的所有功能均正常运行:
- 使用右上角的登录按钮匿名登录。
- 登录后,点击添加按钮,输入字母,然后点击下方随即弹出的搜索结果行,以搜索并将“NFLX”“SBUX”和“T”添加到观看列表。
- 点击相应行末尾的 x 即可将相应股票从“观察名单”中移除。
- 观看股票价格的实时更新。
- 打开 Chrome 开发者工具,前往 Network(网络)标签页,然后选中 Disable cache(停用缓存)和 Use large request rows(使用大型请求行)。停用缓存可确保我们在刷新后始终获取最新更改,使用大量请求行可让行同时显示传输大小和资源大小。在此 Codelab 中,我们主要关注
main.js
的大小。
- 使用模拟的节流功能在不同的网络条件下加载应用。在本 Codelab 中,您将使用慢速 3G 来衡量加载时间,因为在这种网络环境下,较小的软件包大小最有帮助。
现在,开始将应用迁移到新的模块化 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
下公开,因此我们将相应地更新导入路径:
- 前往文件
src/firebase.ts
- 将现有的 import 替换为以下 import:
src/firebase.ts
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
import 'firebase/compat/firestore';
验证应用能否正常运行
- 运行
npm run build
以重新构建应用。 - 打开一个浏览器标签页,然后访问 http://localhost:8080,或刷新现有标签页。
- 玩一下应用。一切应该仍然正常运行。
5. 升级 Auth 以使用模块化 API
您可以按任意顺序升级 Firebase 产品。在此 Codelab 中,您将先升级 Auth,以便学习基本概念,因为 Auth API 相对简单。升级 Firestore 需要完成一些额外的步骤,您将在下文中了解如何执行这些步骤。
更新了身份验证初始化
- 前往文件
src/firebase.ts
- 添加以下 import 语句:
src/firebase.ts
import { initializeAuth, indexedDBLocalPersistence } from 'firebase/auth';
- 删除
import ‘firebase/compat/auth'.
- 将
export const firebaseAuth = app.auth();
替换为:
src/firebase.ts
export const firebaseAuth = initializeAuth(app, { persistence: [indexedDBLocalPersistence] });
- 移除文件末尾的
export type User = firebase.User;
。User
将直接导出到您接下来要更改的src/auth.ts
中。
更新 Auth 代码
- 前往文件
src/auth.ts
- 将以下 import 添加到文件顶部:
src/auth.ts
import {
signInAnonymously,
signOut,
onAuthStateChanged,
User
} from 'firebase/auth';
- 从
import { firebaseAuth, User } from './firebase';
中移除User
,因为您已从‘firebase/auth'.
导入User
- 更新函数以使用模块化 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';
验证应用是否正常运行
- 运行
npm run build
以重新构建应用。 - 打开一个浏览器标签页,然后访问 http://localhost:8080,或刷新现有标签页
- 使用该应用。一切应该仍然正常运行。
检查软件包大小
- 打开 Chrome 开发者工具。
- 切换到 Network(网络)标签页。
- 刷新页面以捕获网络请求。
- 查找 main.js 并检查其大小。只需更改几行代码,您就将软件包大小缩减了 100KB(经过 Gzip 压缩后为 36 KB),缩减幅度约为 22%!在 3G 网络连接速度较慢的情况下,该网站的加载速度也加快了 0.75 秒。
6. 升级 Firebase App 和 Firestore 以使用模块化 API
更新了 Firebase 初始化
- 前往文件
src/firebase.ts.
- 将
import firebase from ‘firebase/compat/app';
替换为:
src/firebase.ts
import { initializeApp } from 'firebase/app';
- 将
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 初始化
- 在同一文件
src/firebase.ts,
中,将import 'firebase/compat/firestore';
替换为
src/firebase.ts
import { getFirestore } from 'firebase/firestore';
- 将
export const firestore = app.firestore();
替换为:
src/firebase.ts
export const firestore = getFirestore();
- 移除“
export const firestore = ...
”后面的所有行
更新导入内容
- 打开文件
src/services.ts.
- 从导入内容中移除了
FirestoreFieldPath
、FirestoreFieldValue
和QuerySnapshot
。从'./firebase'
导入的内容现在应如下所示:
src/services.ts
import { firestore } from './firebase';
- 在文件顶部导入要使用的函数和类型:
**src/services.ts**
import {
collection,
getDocs,
doc,
setDoc,
arrayUnion,
arrayRemove,
onSnapshot,
query,
where,
documentId,
QuerySnapshot
} from 'firebase/firestore';
更新了 search()
- 创建对包含所有股票代码的集合的引用:
src/services.ts
const tickersCollRef = collection(firestore, 'current');
- 使用
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()
- 首先使用
doc()
创建对用户的观看列表的文档引用,然后使用onSnapshot()
监听观看列表更改:
src/services.ts
const watchlistRef = doc(firestore, `watchlist/${user.uid}`);
const unsubscribe = onSnapshot(watchlistRef, snapshot => {
/* subscribe to ticker price changes */
});
- 将股票代码添加到观察列表后,使用
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);
});
}
验证应用是否正常运行
- 运行
npm run build
以重新构建应用。 - 打开一个浏览器标签页,然后访问 http://localhost:8080,或刷新现有标签页
- 使用该应用。一切应该仍然正常运行。
检查软件包大小
- 打开 Chrome 开发者工具。
- 切换到 Network(网络)标签页。
- 刷新页面以捕获网络请求。
- 查找
main.js
并检查其大小。再次将其与原始软件包大小进行比较 - 我们将软件包大小缩减了 200KB 以上(经过 Gzip 压缩后为 63.8 KB),也就是缩减了 50%,这意味着加载时间缩短了 1.3 秒!
7. 使用 Firestore Lite 加快初始页面呈现速度
什么是 Firestore Lite?
Firestore SDK 提供复杂的缓存、实时流式传输、永久存储、多标签页离线同步、重试、乐观并发等功能,因此体积非常大。但您可能只需要获取一次数据,而无需任何高级功能。对于这些情况,Firestore 创建了一个简单轻量级的解决方案,即全新的软件包 Firestore Lite。
Firestore Lite 的一个绝佳用例是优化初始页面呈现的性能,您只需知道用户是否已登录,然后从 Firestore 读取要显示的一些数据即可。
在此步骤中,您将学习如何使用 Firestore Lite 减小软件包大小以加快初始网页呈现速度,然后动态加载主要 Firestore SDK 以订阅实时更新。
您将重构代码以实现以下目标:
- 将实时服务移至单独的文件,以便使用动态导入功能动态加载这些服务。
- 创建新函数,以使用 Firestore Lite 检索观看列表和股票价格。
- 使用新的 Firestore Lite 函数检索数据以进行初始页面呈现,然后动态加载实时服务以监听实时更新。
将实时服务移至新文件
- 创建一个名为
src/services.realtime.ts.
的新文件 - 将
subscribeToTickerChanges()
和subscribeToAllTickerChanges()
函数从src/services.ts
移至新文件。 - 在新文件的顶部添加必要的导入项。
您仍然需要在此处进行一些更改:
- 首先,在文件顶部使用主 Firestore SDK 创建一个 Firestore 实例,以便在函数中使用。您无法在此处从
firebase.ts
导入 Firestore 实例,因为您将通过几个步骤将其更改为 Firestore Lite 实例,该实例仅用于初始页面呈现。 - 其次,移除
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 提取数据
- 打开
src/services.ts.
- 将导入路径从
‘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';
- 添加函数以使用 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);
}
- 打开
src/firebase.ts
,然后将导入路径从‘firebase/firestore'
更改为‘firebase/firestore/lite':
src/firebase.ts
import { getFirestore } from 'firebase/firestore/lite';
将所有内容整合到一起
- 打开
src/main.ts.
- 您需要使用新创建的函数提取初始页面呈现的数据,还需要一些辅助函数来管理应用状态。现在,更新导入内容:
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';
- 在文件顶部使用动态导入加载
src/services.realtime
。变量loadRealtimeService
是一个 Promise,它会在代码加载后解析为实时服务。稍后,您将使用该 ID 订阅实时更新。
src/main.ts
const loadRealtimeService = import('./services.realtime');
loadRealtimeService.then(() => {
setRealtimeServicesLoaded(true);
});
- 将
onUserChange()
的回调更改为async
函数,以便我们在函数体中使用await
:
src/main.ts
onUserChange(async user => {
// callback body
});
- 现在,使用我们在上一步中创建的新函数提取数据,以进行初始页面呈现。
在 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
}
}
- 在未登录用户的 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。
验证应用是否正常运行
- 运行
npm run build
以重新构建应用。 - 打开一个浏览器标签页,然后访问 http://localhost:8080,或刷新现有标签页。
检查软件包大小
- 打开 Chrome 开发者工具。
- 切换到 Network(网络)标签页。
- 刷新页面以捕获网络请求
- 查找
main.js
并检查其大小。 - 现在,它只有 115KB(经过 Gzip 压缩后为 34.5KB)。这比原始软件包大小(446KB,经过 Gzip 压缩为 138KB)缩小了 75%!因此,在 3G 连接下,网站的加载速度提高了 2 秒以上,性能和用户体验得到了显著提升!
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 |
现在,您已经了解了将使用 v8 版 Firebase JS SDK 的 Web 应用升级为使用新的模块化 JS SDK 所需执行的主要步骤。