准备将 Firebase Extensions 迁移到 Cloud Functions

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

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

在本指南中,我们将以将 Firestore 流式传输到 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、密钥和生命周期钩子。

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

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

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

然后,对于 extension.yaml 中的每个项,决定将其放置在 npm 软件包中的哪个位置:

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

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

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

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

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

实际操作示例:将 Firestore 数据流式传输到 BigQuery

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

在 extension.yaml 中 数量 / 值 数据去向
params 25 (COLLECTION_PATH, DATASET_ID, TABLE_ID, DATASET_LOCATION, VIEW_TYPE, …) Cloud Functions 个形参(第 5 部分)
API bigquery.googleapis.com requiresAPI(…)(第 7 部分)
角色 bigquery.dataEditor、datastore.user、bigquery.user requiresRole(…)(第 7 部分)
资源 1 个事件触发器 (fsexportbigquery) + 任务队列函数(initBigQuerySync、setupBigQuerySync) 导出的软件包函数(第 3 部分)
lifecycleEvents onInstall → initBigQuerySync;onUpdate / onConfigure → setupBigQuerySync afterFirstDeploy / afterRedeploy(第 9 部分)
scripts/ import/(补余广告)、gen-schema-view/ 保留为脚本(此处不作介绍)

扩展程序未声明任何类型:secret params,因此本指南的第 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 相同的版本。

实际操作示例:将 Firestore 数据流式传输到 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 代已修补的事件解构,您可以最大限度地减少重写工作,并避免重写函数逻辑,因为第 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 中读取其值,或在部署期间提示用户输入这些值。保持相同的参数名称,以便沿用现有安装中的值。

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

工作示例:将 Firestore 流式传输到 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 中,您可以使用类型“secret”声明 Secret。Extensions 运行时会存储并绑定它们,因此您的扩展程序代码可以直接读取 process.env.PARAM_NAME。在典型的 Cloud Functions 代码库中,您需要明确声明并绑定每个密钥:

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 软件包/套件后,密钥引用将在最终用户的 .env 文件中进行管理。请务必不要更改代码中声明的密钥名称。在迁移期间,最终用户密钥将相应地进行迁移。

实际操作示例:通过 Cloud Firestore 触发电子邮件

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

# 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 现在默认会以同一上下文(例如扩展程序)中的任务队列为目标。无论是作为扩展程序还是作为独立函数,您都可以放心地在代码中移除此参数,我们甚至建议您这样做。移除此形参可确保可移植性和向前兼容性。

enqueue 调用中的其他所有内容(包括位置/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";

const queue = getFunctions().taskQueue(
  `locations/${process.env.FUNCTION_REGION}/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 支持更窄的模型。

实际操作示例:将 Firestore 数据流式传输到 BigQuery

之前。在 extension.yaml 中声明;扩展程序运行时已启用该 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

实际操作示例:将 Firestore 数据流式传输到 BigQuery

之前。lifecycleEvents(在 extension.yaml 中),由扩展程序运行时驱动:

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。

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

  • 结算备注。

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

实际操作示例:将 Firestore 数据流式传输到 BigQuery

软件包 README 包含一个具体的“更改内容”表格:

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

测试第 2 代函数

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

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

firebase deploy --only functions

输入此命令后,请按照您在 Firebase 控制台中填写扩展程序安装表单的方式,填写向导提示您输入的参数值。

实际操作示例:将 Firestore 数据流式传输到 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. 删除 Cloud Firestore 中的 bigquery-mirror-test 文档。它会从最新视图中消失,并且原始变更日志表中会附加一个 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 对代码进行单元测试,如 Cloud Functions 的单元测试中所述。
  • 预配不再由扩展程序运行时驱动。如果部署后缺少变更日志表,请手动重新运行设置任务:firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME。该任务是幂等的,因此重新运行该任务可协调数据集、表和视图。
  • 形参值来自 .env,而不是安装表单,因此在 .env 完成后,firebase deploy 的重新运行是非交互式的。