本指南介绍了如何将扩展程序从已废弃的 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.md、PREINSTALL.md和POSTINSTALL.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()(例如
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
实际示例: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 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`
- 删除
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-<instanceId>-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是非交互式的。