本指南介绍如何将扩展程序从已弃用的 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.md、PREINSTALL.md和POSTINSTALL.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()(例如 setProcessingState 或 setFatalError),请删除这些调用,因为如果从正常部署的第 2 代函数中调用这些函数,它们会抛出错误。生命周期状态现在由 afterFirstDeploy 和 afterRedeploy 驱动,不再使用此状态跟踪。
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 Firestore 到 BigQuery 的端到端同步:
- 在 Cloud Firestore 控制台中,创建您设置为 COLLECTION_PATH (users) 的集合(如果该集合尚不存在)。
- 创建一个名为 bigquery-mirror-test 的文档,其中包含具有任意值的任意字段。
- 在 BigQuery 控制台中,查询原始更改日志表。它应包含记录文档创建的单行内容:
SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`
- 查询最新视图,此查询应该返回所出现的唯一文档(即
bigquery-mirror-test)的最新变更事件。
SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`
- 删除 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-<instanceId>-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的重新运行是非交互式的。