准备将 Firebase Extensions 迁移到 Cloud Functions

本指南介绍了如何将扩展程序从已废弃的 Firebase Extensions 环境迁移到用户在自己的 Cloud Functions for Firebase(第 2 代)代码库中安装和部署的函数。

这是推荐的迁移路径。Firebase 将维护一份包含官方 npm 等效项的扩展程序列表;本指南将引导您创建自己的扩展程序。

在本指南中,我们将使用 Stream Firestore to BigQuery 扩展程序 (firestore-bigquery-export) 作为示例。每个部分都以实际示例 结尾,该示例展示了该扩展程序在迁移之前和之后(作为 @firebase/firestore-bigquery-export 软件包)的样子。

注册以获取有关迁移扩展程序的更多信息和帮助

如果您对如何从 Firebase Extensions 进行迁移有疑问,可以通过 firebase-extensions-migrator-support-external@google.com 与我们联系。我们还会在更新本指南时通过电子邮件向此群组发送通知,其中包含有关打包、测试和分发第 2 代函数的更多信息。

如需加入此群组,请向 firebase-extensions-migrator-support-external+subscribe@google.com发送消息, 系统会回复一封会员资格申请电子邮件。您必须回复该电子邮件,而不是点击“加入此群组”按钮。

准备工作

如需完成此迁移,您将使用以下功能: Cloud Functions

  • 参数化配置. 您在 extension.yaml 中声明的每个参数都会成为软件包代码中的已定义参数。

  • 声明式 IAM 角色和必需的 API。您在 extension.yaml 中声明的每个角色都会成为 requiresRole(...) 调用,每个 API 都会成为 requiresAPI(...) 调用。在部署时, Firebase CLI 会将声明的角色授予代管式运行时服务 账号,并代表您启用声明的 API。

  • Cloud Functions 代码库的生命周期事件。Cloud Functions 代码库现在支持生命周期事件,类似于Firebase Extensions。 使用生命周期钩子 afterFirstDeploy(...)afterRedeploy(...) 声明安装时和更新时设置。这些钩子会替换您在 extension.yaml 中声明的 lifecycleEvents

清点扩展程序

首先,清点扩展程序:列出扩展程序声明、随附和记录的所有内容,以便每个行为在第 2 代函数中都有明确的目标,并且在迁移过程中不会丢失任何内容。

请查看以下各项,并记下您发现的内容:

  • extension.yaml,用于声明参数、函数、事件、IAM 角色、必需的 API、Secret 和生命周期钩子。

  • functions/,其中包含函数代码、依赖项、构建配置、触发器和任务队列函数。

  • README.mdPREINSTALL.mdPOSTINSTALL.md,其中包含设置步骤、警告和结算说明。

  • scripts/,其中包含任何导入、回填、IAM、修复或迁移实用程序,以及您随扩展程序一起提供的任何其他工具。

然后,对于 extension.yaml中的每个项,确定它 在 npm 软件包中的位置:

  • 将用户配置转换为 Cloud Functions 参数(第 5 部分)。

  • 将 Secret 转换为 Cloud Functions Secret(第 6 部分)。

  • 将 IAM 角色转换为 requiresRole(...) 声明(第 8 部分)。

  • 在适当情况下,将必需的 Google API 转换为 requiresAPI(...) 声明(第 8 部分)。

  • 将安装和更新钩子转换为 afterFirstDeploy(...)afterRedeploy(...) 声明(第 8 部分)。

实际示例:Stream Firestore to BigQuery

读取 firestore-bigquery-export/extension.yaml 和 functions/ 会生成以下清单:

在 extension.yaml 中 计数 / 值 位置
params 25 (COLLECTION_PATH, DATASET_ID, TABLE_ID, DATASET_LOCATION, VIEW_TYPE, …) Cloud Functions 参数(第 5 部分)
apis bigquery.googleapis.com requiresAPI(...)(第 7 部分)
roles bigquery.dataEditor, datastore.user, bigquery.user requiresRole(...)(第 7 部分)
resources 1 个事件触发器 (fsexportbigquery) + 任务队列函数(initBigQuerySync、setupBigQuerySync) 导出的软件包函数(第 3 部分)
lifecycleEvents onInstall → initBigQuerySync;onUpdate / onConfigure → setupBigQuerySync afterFirstDeploy / afterRedeploy(第 9 部分)
scripts/ import/(回填)、gen-schema-view/ 保留为脚本(此处超出范围)

该扩展程序未声明任何类型:Secret 参数,因此本指南的第 6 部分中没有任何内容需要迁移。事件触发器已经是第 2 代函数;只有任务队列函数仍然是第 1 代函数(与第 3 部分相关)。

更新 package.json

更新扩展程序的 package.json 文件。如果您要迁移一个扩展程序,则此文件可以是根 package.json。如果您要在一个代码库中迁移多个扩展程序,请为每个扩展程序提供自己的软件包。

最低 SDK 版本。将 firebase-functions >= 7.3 和 firebase-admin >= 14.2.0 声明为依赖项。同时将您的 firebase-functions 版本声明为对等依赖项

{
  "name": "<package-name>",
  "version": "1.0.0",
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "exports": {
    ".": {
      "types": "./lib/index.d.ts",
      "default": "./lib/index.js"
    }
  },
  "engines": {
    "node": ">=22"
  },
  "peerDependencies": {
    "firebase-functions": "^7.3.0"
  },
  "dependencies": {
    "firebase-functions": "^7.3.0",
    "firebase-admin": "^14.2.0"
  }
}

除了常规 依赖项之外,还将 firebase-functions 声明为对等依赖项,以便用户的 Cloud Functions 项目具有与您的库编写时所用的 SDK 相同的版本 。

实际示例:Stream Firestore to BigQuery

之前 。扩展程序的 functions/package.json 是私有的,用于命名扩展程序 ID,并将 firebase-functions 声明为直接依赖项:

{
  "name": "firestore-bigquery-export",
  "main": "lib/index.js",
  "private": true,
  "dependencies": {
    "@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
    "firebase-admin": "^14.2.0",
    "firebase-functions": "^6.3.2"
  }
}

之后 。可发布的软件包:限定范围的名称、导出映射,以及 firebase-functions 移至 peerDependencies

{
  "name": "@firebase/firestore-bigquery-export",
  "version": "0.1.0",
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "exports": {
    ".":     { "types": "./lib/index.d.ts", "default": "./lib/index.js" },  },
  "engines": { "node": ">=22" },
  "peerDependencies": { "firebase-functions": "^7.3.0" },
  "dependencies": {
      "@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
      "firebase-admin": "^14.2.0",
      "firebase-functions": "^7.3.0"
    }
}

将函数从第 1 代升级到第 2 代

如果您的扩展程序仍导出第 1 代函数,请将每个函数转换为其第 2 代等效函数。从 firebase-functions/... 模块导入,并在函数选项中传递运行时设置。

您可以使用 最大限度地减少重写工作,并避免重写函数逻辑 ,因为第 2 代 SDK 现在将 V1 参数作为事件 对象中的字段公开,让您可以使用解构/命名参数并保持业务 逻辑不变。

之前 。第 1 代:

import * as functions from "firebase-functions/v1";

export const sync = functions.firestore
  .document("{collectionId}/{documentId}")
  .onWrite(async (change, context) => {
    await handleWrite(change.before, change.after, context.params);
  });

之后 。第 2 代:

import { onDocumentWritten } from "firebase-functions/firestore";

export const syncV2 = onDocumentWritten(
  { document: "{collectionId}/{documentId}" },
  async ({change,context}) =>
    await handleWrite(change.before, change.after, context.params);
);

如需查看第 1 代函数和第 2 代函数之间的差异的完整列表,请参阅 Cloud Functions 版本 比较

转换扩展程序参数和 Secret

转换参数

您在 extension.yaml 中声明的每个 Cloud Functions 参数都会成为参数。

转换直接环境读取:

const collectionPath = process.env.COLLECTION_PATH;

转换为 Cloud Functions 参数:

import { defineString } from "firebase-functions/params";
import { onDocumentWritten} from "firebase-functions/firestore";

const collectionPath = defineString("COLLECTION_PATH");

// Pass the param directly when used as a placeholder (e.g. trigger path)
export const sync = onDocumentWritten(
  { document: collectionPath },
  async (event) => {
    // Call .value() to read the string inside a handler
    const path = collectionPath.value();
    await handleWrite(path, event);
  }
);

使用 collectionPath.value() 读取处理程序内的字符串;在需要占位符(例如函数 触发器路径)的位置直接使用 collectionPath

Firebase CLI 会发现您的参数并从 .env、 .env.projectId 中读取其值,或在部署期间提示用户。保留相同的参数名称,以便现有安装中的值可以沿用。

请务必不要 更改代码中声明的参数名称 扩展程序迁移会自动保留现有的最终用户参数值,但前提是名称保持不变。

实际示例:Stream Firestore to BigQuery 之前。在 extension.yaml 中声明的参数,在 config.ts 中读取为原始环境变量:

# extension.yaml
-   param: COLLECTION_PATH
  label: Collection path
  type: string
  required: true
// functions/src/config.ts
collectionPath: process.env.COLLECTION_PATH,

之后 。一个 defineString;CLI 会发现它并从 .env 中读取:

// src/config.ts
import { defineString } from "firebase-functions/params";

collectionPath: defineString("COLLECTION_PATH", {
  label: "Collection path",
  // We now support "nonEmpty: true" to ensure a value other than the empty string
  // is entered, analagous to "required: true" in extensions.yaml
  input: { text: { nonEmpty: true} }
}),

参数名称未更改,因此现有的 .env 仍可正常运行。

转换 Secret

在 extension.yaml 中,您可以使用 type: secret 声明 Secret。Extensions 运行时会存储并绑定它们,因此您的扩展程序代码可以直接读取 process.env.PARAM_NAME。在典型的 Cloud Functions 代码库中,您可以显式声明和绑定每个 Secret:

import { defineSecret } from "firebase-functions/params";
import { onRequest } from "firebase-functions/https";

const apiKey = defineSecret("API_KEY");
export const fn = onRequest({ secrets: [apiKey] }, handler);

将扩展程序迁移到 npm 软件包/套件后,Secret 引用将在最终用户的 .env 文件中进行管理。请务必不要 更改代码中声明的 Secret 名称在迁移期间,最终用户的 Secret 将相应地迁移。

实际示例 :Trigger Email From Cloud Firestore

之前 。MAIL_COLLECTION 和 SMTP_PASSWORD 在 config.ts 中读取为原始环境变量:

# extension.yaml
-   param: MAIL_COLLECTION
  label: Email documents collection
  type: string
  default: mail
  required: true

-   param: SMTP_PASSWORD
  label: SMTP password
  type: secret
// functions/src/config.ts
mailCollection: process.env.MAIL_COLLECTION,
smtpPassword: process.env.SMTP_PASSWORD,

之后 。一个 defineString 和一个 defineSecret;CLI 会发现两者并从 .env 中读取

import { defineString, defineSecret } from "firebase-functions/params";
import { onDocumentWritten } from "firebase-functions/firestore";

const mailCollection = defineString("MAIL_COLLECTION",{ label: "Email documents collection",
 default: "mail"
});

const smtpPassword = defineSecret("SMTP_PASSWORD", { label: "SMTP password" });

export const processQueue = onDocumentWritten(
  { document: `${mailCollection}/{documentId}`, secrets: [smtpPassword] },
  async (event) => {
    const collection = mailCollection.value();
    const password = smtpPassword.value();
    // ...
  }
);

迁移内部任务队列调用

某些扩展程序使用 Firebase Admin SDK 从其 函数代码内部将工作排入其 自己的任务队列。这与接收分派的任务不同(在升级函数转换 生命周期钩子部分中介绍)。在这里,您的代码是调用 queue.enqueue(...) 的生产者。

以前版本的 Admin SDK 要求扩展程序将自己的 扩展程序实例 ID 作为第二个参数传递,以定位 同一扩展程序中的任务队列函数。从 `firebase-admin` 14.2.0 开始,既不需要也不 建议这样做。任务队列 API 现在默认定位同一上下文(例如扩展程序)中的任务队列。建议您在代码中移除此参数(无论是作为扩展程序还是作为独立函数),这样做是安全的。移除此参数可确保可移植性和向前兼容性。

关于排队调用的所有其他内容( 位置/region/函数/name资源路径、任务 载荷和重试逻辑)都保持不变。

如需详细了解如何使用 Cloud Tasks 将函数排队,请参阅 /docs/functions/task-functions

之前 。第 1 代扩展程序

import { getFunctions } from "firebase-admin/functions";

const queue = getFunctions().taskQueue(
  `locations/${config.location}/functions/syncBigQuery`,
  process.env.EXT_INSTANCE_ID, // extension instance ID, injected by the runtime
);
await queue.enqueue(taskData);

之后 。第 2 代扩展程序

import { getFunctions } from "firebase-admin/functions";
import { region } from "firebase-functions/params";

const queue = getFunctions().taskQueue(
  `locations/${region.value()}/functions/syncBigQuery`);
await queue.enqueue(taskData);

如果您的排队调用定位的是带有前缀的代码库,则发现的函数名称也会带有前缀(例如 orders-syncBigQuery)。

声明必需的 API 和 IAM 角色

将扩展程序的 IAM 和 API 要求从 extension.yaml 移到代码中:

import { requiresAPI, requiresRole } from "firebase-functions"

requiresAPI("bigquery.googleapis.com", "Needed to write changelog rows");
requiresRole("roles/bigquery.dataEditor");
requiresRole("roles/bigquery.user");

借助声明式安全性,Firebase CLI 会为代码库创建或更新代管式 运行时服务账号,并向其授予所有 声明角色的并集。向用户说明,代码库中的所有函数都使用这些角色运行,除非最终 API 支持更窄的模型。

实际示例:Stream Firestore to BigQuery

之前 。在 extension.yaml 中声明;Extensions 运行时启用了 API 并将角色授予了代管式账号:

apis:
  -   apiName: bigquery.googleapis.com
roles:
  -   role: bigquery.dataEditor
  -   role: datastore.user
  -   role: bigquery.user

之后 。在代码中使用 requiresAPI 和 requiresRole 声明:

import { requiresAPI, requiresRole } from "firebase-functions/";
requiresAPI("bigquery.googleapis.com",
  "Needed to write changelog rows and views");
requiresRole("roles/biguqery.dataEditor");
requiresRole("roles/datastore.user");
requiresRole("roles/bigquery.user");

转换生命周期钩子

如果您的扩展程序调用 getExtensions().runtime()(例如 setProcessingStatesetFatalError),请删除这些调用,因为如果从正常部署的第 2 代函数调用它们,它们会抛出 错误。生命周期状态现在由 afterFirstDeployafterRedeploy 驱动,在这种情况下不会使用此状态跟踪。

Firebase Extensions 可以在用户安装、更新或 重新配置扩展程序时运行设置。在 npm 软件包中,在代码中声明等效的生命周期操作。

对于一次性设置

import { afterFirstDeploy } from "firebase-functions/lifecycle";
import { onTaskDispatched } from "firebase-functions/tasks";

export const runInitialSetup = onTaskDispatched(async (request) => {
  await initializeResources(request.data);
});

afterFirstDeploy({
  task: {
    function: "runInitialSetup",
    body: {}
  }
});

对于配置或代码更新

import { afterRedeploy } from "firebase-functions/lifecycle";

afterRedeploy({
  task: {
    function: "runInitialSetup",
    body: { reconcile: true }
  }
});

使生命周期操作具有幂等性。如果分派或执行失败,用户可能需要手动重新运行它们:

firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME
firebase functions:lifecycle:run afterRedeploy CODEBASE_NAME

实际示例:Stream Firestore to BigQuery

之前 extension.yaml 中的 lifecycleEvents,由 Extensions 运行时驱动:

lifecycleEvents:
  onInstall:
    function: initBigQuerySync
    processingMessage: Configuring BigQuery Sync.
  onUpdate:
    function: setupBigQuerySync
    processingMessage: Configuring BigQuery Sync
  onConfigure:
    function: setupBigQuerySync
    processingMessage: Configuring BigQuery Sync

之后 。在代码中声明;任务在首次 部署时预配 BigQuery

import { afterFirstDeploy, afterRedeploy } from "firebase-functions/lifecycle";

afterFirstDeploy({ task: { function: "initBigQuerySync" } });
afterRedeploy({ task: { function: "setupBigQuerySync" } });

预配是幂等的,因此重新运行会协调数据集、表和视图。 用户可以使用 firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME 手动重新运行。

为用户记录设置

  • 编写软件包 README,其中至少应说明:

  • 软件包所需的 .env 值。

  • 软件包所需的 Secret,以及如何迁移现有 Secret 值。

  • 软件包使用 requiresRole(...) 声明的 IAM 角色。

  • 软件包启用或需要的 Google API。

  • 软件包声明的生命周期钩子,以及如何手动重新运行它们。

  • 结算说明。

  • 与原始扩展程序相比,发生了哪些变化。

实际示例:Stream Firestore to BigQuery

软件包 README 包含一个具体的“发生了哪些变化”表:

问题 作为扩展程序 作为 @firebase/firestore-bigquery-export
配置 扩展程序参数 通过 .env 传递的函数参数
IAM 由 Extensions 授予 requiresRole(...),在部署时应用
预配 Extensions 的生命周期任务 afterFirstDeploy / afterRedeploy 任务
函数名称 ext-instanceId-fsexportbigquery fsexportbigquery(可选前缀)

测试第 2 代函数

您现在应该有一个第 2 代函数,部署后,其行为与新安装的扩展程序完全相同。最后一步是验证并修复在此过程中意外引入的任何问题。

确保您使用的是 firebase-tools >= 15.24.0,并将转换后的第 2 代函数部署到具有相应资源的测试项目中,以测试其行为。如果您已设置用于测试扩展程序的测试项目,请使用以下命令:

firebase deploy --only functions

输入此命令后,填写生成的向导,提示您输入 参数值,就像您在 Firebase 控制台中为扩展程序填写安装表单一样。

实际示例:Stream Firestore to BigQuery

我们验证 Cloud FirestoreBigQuery 的端到端同步:

  1. Cloud Firestore 控制台中,创建您设置为 COLLECTION_PATH (users) 的集合(如果该集合尚不存在)。
  2. 创建一个名为 bigquery-mirror-test 的文档,其中包含具有任何值的任何字段。
  3. BigQuery 控制台中,查询原始更改日志表。它应包含记录文档创建的单行:
SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`
  1. 查询最新视图,此查询应该返回 所出现的唯一文档的最新变更事件:bigquery-mirror-test
SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`
  1. 删除 bigquery-mirror-test 文档在 Cloud Firestore。它会从最新视图中消失,并且 DELETE 事件会附加到原始更改日志表中。

您可以使用以下命令检查单个文档的完整历史记录:

SELECT *
   FROM `PROJECT_ID.analytics.users_raw_changelog`
   WHERE document_name = "bigquery-mirror-test"
   ORDER BY timestamp ASC

与测试扩展程序的区别

  • 触发器部署为 fsexportbigquery(可选代码库前缀), 而不是 ext-&lt;instanceId&gt;-fsexportbigquery。在 Cloud Functions 信息中心和日志中查找该名称。
  • 您的代码现在将在 Firebase Local Emulator Suite 中作为普通 函数运行。您可以使用 .env.local 设置要在模拟器中使用的参数的值。您还可以使用 firebase-functions-test SDK 对代码进行单元测试,如 Unit testing of Cloud Functions中所述
  • 预配不再由 Extensions 运行时驱动。如果在部署后缺少更改日志 表,请手动重新运行设置任务:firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME。该任务是幂等的,因此重新运行它会协调数据集、表和视图。
  • 参数值来自 .env 而不是安装表单,因此在 .env 完成后,重新运行 firebase deploy 是非交互式的。