从命名空间型 API 升级到模块化 API

对于目前正在使用命名空间型 Firebase Web API 的应用(从 compat 库开始,一直回溯到 8 或更低版本),应考虑按照本指南中的说明迁移到模块化 API。

本指南假定您熟悉命名空间型 API,并且能够利用模块打包器(例如 webpackRollup)升级和持续进行模块化应用开发。

强烈建议在开发环境中使用模块打包器。如果您不使用模块打包器,将无法发挥模块化 API 在缩减应用大小方面的主要优势。您需要使用 npmyarn 来安装 SDK。

本指南中的升级步骤将基于使用 AuthenticationCloud Firestore SDK 的虚构 Web 应用。通过学习这些示例,您可以掌握升级所有受支持的 Firebase Web SDK 所需了解的概念和实际步骤。

命名空间型 (compat) 库简介

Firebase Web SDK 可以使用两种类型的库:

  • 模块化 - 一种新的 API 接口,用于实现摇树优化(移除未使用的代码),使您的 Web 应用尽可能精简快速。
  • 命名空间型 (compat) - 一种熟悉的 API 接口,与旧版本的 SDK 完全兼容,让您无需更改任何 Firebase 代码即可立即升级。与命名空间型库相比,兼容型库在大小或性能方面的优势很少,甚至没有优势。

本指南假定您将利用兼容型库来推动升级。通过这些库,您可以一边继续使用命名空间型代码,一边逐步针对模块化 API 重构代码。这意味着您在升级过程中可以更轻松地编译和调试应用。

如果应用极少调用 Firebase Web SDK (例如,只对 Authentication API 进行简单调用),可以直接重构旧版命名空间型代码,而不需要使用兼容型库。如果要升级此类应用,您可以按照本指南中有关“模块化 API”的说明进行操作,而无需使用兼容型库。

升级流程简介

升级流程的每个步骤都限定了范围,这样您便可以完成应用源代码的修改,然后在不出现中断的情况下编译并运行源代码。总的来说,如需升级应用,您需要执行以下操作:

  1. 将模块化库和兼容型库添加到您的应用中。
  2. 将代码中的 import 语句更新为使用兼容型库。
  3. 将针对单个产品(例如 Authentication)的代码重构为模块化样式。
  4. 可选:现在移除用于 AuthenticationAuthentication 兼容型库和兼容型代码,以便先在 Authentication 方面实现应用大小的精简,然后再继续。
  5. 将针对每个产品(例如 Cloud FirestoreFCM 等)的函数重构为模块化样式,然后进行编译和测试,直到完成所有方面。
  6. 将初始化代码更新为模块化样式。
  7. 从应用中移除所有剩余的兼容型语句和兼容型代码。

获取最新版 SDK

首先,使用 npm 获取模块化库和兼容型库:

npm i firebase@11.0.2

# OR

yarn add firebase@11.0.2

将导入项更新为兼容型

更新依赖项后,为了确保代码正常运行,请将 import 语句更改为使用每个语句的“兼容型”版本。例如:

更新前:8 或更低版本

import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';

更新后:兼容型

// compat packages are API compatible with namespaced code
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
import 'firebase/compat/firestore';

重构为模块化样式

命名空间型 API 基于(以句点串联的)命名空间和服务模式,而模块化方法则主要围绕函数来组织代码。在模块化 API 中,firebase/app 包和其他包不会返回包含包中所有方法的全面导出结果,而是会导出一个个的函数。

在模块化 API 中,服务将作为第一个参数传递,函数随后会使用服务的详细信息执行其余操作。我们通过两个重构 AuthenticationCloud Firestore API 调用的示例来了解具体的工作原理。

示例 1:重构 Authentication 函数

重构前:兼容型

兼容型代码与命名空间型代码完全相同,但导入项已更改。

import firebase from "firebase/compat/app";
import "firebase/compat/auth";

const auth = firebase.auth();
auth.onAuthStateChanged(user => { 
  // Check for user status
});

重构后:模块化

getAuth 函数将 firebaseApp 作为其第一个参数。 onAuthStateChanged 函数不会像在命名空间型 API 中那样需要从 auth 实例串联;因此,它是一个自由函数,将 auth 作为其第一个参数。

import { getAuth, onAuthStateChanged } from "firebase/auth";

const auth = getAuth(firebaseApp);
onAuthStateChanged(auth, user => {
  // Check for user status
});

更新对 Auth 方法 getRedirectResult 的处理方式

模块化 API 在 getRedirectResult 中实施了一项重大变更。如果未调用重定向操作,模块化 API 会返回 null,而命名空间型 API 则不同,它会返回一个 UserCredential(其中包含一个 null 用户)。

重构前:兼容型

const result = await auth.getRedirectResult()
if (result.user === null && result.credential === null) {
  return null;
}
return result;

重构后:模块化

const result = await getRedirectResult(auth);
// Provider of the access token could be Facebook, Github, etc.
if (result === null || provider.credentialFromResult(result) === null) {
  return null;
}
return result;

示例 2:重构 Cloud Firestore 函数

重构前:兼容型

import "firebase/compat/firestore"

const db = firebase.firestore();
db.collection("cities").where("capital", "==", true)
    .get()
    .then((querySnapshot) => {
        querySnapshot.forEach((doc) => {
            // doc.data() is never undefined for query doc snapshots
            console.log(doc.id, " => ", doc.data());
        });
    })
    .catch((error) => {
        console.log("Error getting documents: ", error);
    });

重构后:模块化

getFirestore 函数将 firebaseApp 作为其第一个参数,后者是从前面的示例中的 initializeApp 返回的。请注意,在模块化 API 中,代码构建查询的方式有很大不同;没有串联,并且 querywhere 等方法现在作为自由函数提供。

import { getFirestore, collection, query, where, getDocs } from "firebase/firestore";

const db = getFirestore(firebaseApp);

const q = query(collection(db, "cities"), where("capital", "==", true));

const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
  // doc.data() is never undefined for query doc snapshots
  console.log(doc.id, " => ", doc.data());
});

更新对 Firestore DocumentSnapshot.exists 的引用

模块化 API 实施了一项重大变更,即属性 firestore.DocumentSnapshot.exists 已更改为方法。其功能基本相同(测试文档是否存在),但您必须重构代码以使用较新的方法,如下所示:

重构前:兼容型

if (snapshot.exists) {
  console.log("the document exists");
}

重构后:模块化

if (snapshot.exists()) {
  console.log("the document exists");
}

示例 3:组合命名空间型和模块化代码样式

在升级过程中使用兼容型库让您可以一边继续使用命名空间型代码,一边逐步针对模块化 API 重构代码。这意味着,您在将 Authentication 或其他 Firebase SDK 代码重构为模块化样式时,可以保留 Cloud Firestore 的现有命名空间型代码,并且仍然能使用两种代码样式成功编译您的应用。产品(例如 Cloud Firestore)中的命名空间型和模块化 API 代码也是如此;只要您导入兼容型软件包,新旧代码样式就可以共存:

import firebase from 'firebase/compat/app';
import 'firebase/compat/firestore';
import { getDoc } from 'firebase/firestore'

const docRef = firebase.firestore().doc();
getDoc(docRef);

请注意,虽然您的应用将可正常编译,但只有从应用中完全移除兼容型语句和代码,才能获享模块化代码在应用大小方面的优势。

更新初始化代码

更新应用的初始化代码,以使用模块化语法。重构应用中的所有代码后,请务必更新初始化代码;这是因为 firebase.initializeApp() 会初始化兼容型和模块化 API 的全局状态,而模块化 initializeApp() 函数仅会初始化模块化 API 的状态。

重构前:兼容型

import firebase from "firebase/compat/app"

firebase.initializeApp({ /* config */ });

重构后:模块化

import { initializeApp } from "firebase/app"

const firebaseApp = initializeApp({ /* config */ });

移除兼容型代码

为了发挥模块化 API 在精简应用大小方面的优势,您最终应将所有调用转换为上述的模块化样式,并从代码中移除所有 import "firebase/compat/* 语句。完成后,代码中不应再包含对 firebase.* 全局命名空间的引用或对任何其他命名空间型 API 代码的引用。

从窗口中使用兼容型库

模块化 API 经过优化,可以与模块(而不是浏览器的 window 对象)配合使用。该库之前的版本允许使用 window.firebase 命名空间加载和管理 Firebase。建议您以后不要采用这种做法,因为这种方法不允许消除未使用的代码。不过,对于不希望立即开始模块升级的开发者,JavaScript SDK 的兼容型版本也支持 window

<script src="https://www.gstatic.com/firebasejs/11.0.2/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/11.0.2/firebase-firestore-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/11.0.2/firebase-auth-compat.js"></script>
<script>
   const firebaseApp = firebase.initializeApp({ /* Firebase config */ });
   const db = firebaseApp.firestore();
   const auth = firebaseApp.auth();
</script>

兼容型库在后台使用模块化代码,并提供与命名空间型 API 相同的 API;这意味着您可以参阅命名空间型 API 参考文档和命名空间型代码段了解详情。此方法不建议长期使用,而是应作为着手点来升级到完全模块化的库。

模块化 SDK 的优势和限制

与早期版本相比,完全模块化的 SDK 具有以下优势:

  • 模块化 SDK 可大幅缩减应用大小。 它采用新型的 JavaScript 模块格式,允许执行“摇树优化”做法,这样就只需导入应用需要的工件。与使用命名空间型 API 构建的类似应用相比,使用模块化 SDK 进行摇树优化可使大小(以 KB 为单位)减少 80%,具体取决于您的应用。
  • 模块化 SDK 将继续受益于后续的功能开发,但命名空间型 API 无法受益。