将 Firebase Extensions 迁移到 Cloud Functions

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

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

在本指南中,Stream Cloud Firestore to BigQuery 扩展程序 (firestore-bigquery-export) 用作运行示例。每个部分都以一个示例结尾,该示例展示了迁移前后的扩展程序外观,即 @firebase-function-kits/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。

将 Firebase Extensions 来源迁移到第 2 代函数

(可选)使用 Firebase 代理技能自动迁移

您可以使用官方 extension-to-functions-codebase AI 代理技能自动执行第 1 步到第 8 步(清点资源、触发升级、参数和密钥转换、声明式 IAM、生命周期钩子以及生成软件包 README)。

安装技能

如果您或您的 AI 编码助理(Firebase 中的 Gemini、Cursor、Claude Code、GitHub Copilot)尚未安装该技能,请使用技能 CLI 运行以下命令:

npx skills add firebase/agent-skills --skill extension-to-functions-codebase

在项目中安装技能后,AI 编码助理会自动遵循其迁移规则和转换步骤。您可以使用以下提示:

“请按照 extension-to-functions-codebase 技能中的说明,将此 Firebase 扩展程序迁移到可发布的第 2 代函数套件软件包中。”

1. 清点扩展程序

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

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

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

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

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

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

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

  • 将用户配置转换为 Cloud Functions 参数(第 4 步)。

  • 将密钥转换为 Cloud Functions 密钥(第 4 步)。

  • 将 IAM 角色转换为 requiresRole(...) 声明(第 6 步)。

  • 根据需要将必需的 Google API 转换为 requiresAPI(...) 声明(第 6 步)。

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

  • 将实例 ID 从 EXT_INSTANCE_ID 转换为 FIREBASE_KIT_INSTANCE_ID(第 4 步)。

示例:从 Cloud Firestore 到 BigQuery 的流

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

在extension.yaml 数量 / 价值 数据去向
params 25(COLLECTION_PATH、DATASET_ID、TABLE_ID、DATASET_LOCATION、VIEW_TYPE…) Cloud Functions 参数(第 4 步)
apis bigquery.googleapis.com requiresAPI(...)(第 6 步)
roles bigquery.dataEditor、datastore.user、bigquery.user requiresRole(...)(第 6 步)
resources 1 个事件触发器 (fsexportbigquery) + 任务队列函数(initBigQuerySync、setupBigQuerySync) 导出的软件包函数(第 3 步)
lifecycleEvents onInstall → initBigQuerySync;onUpdate / onConfigure → setupBigQuerySync afterFirstDeploy / afterRedeploy(第 7 步)
实例 ID 未使用(无 EXT_INSTANCE_ID 读取) 没有可迁移的内容
scripts/ import/(补余广告)、gen-schema-view/ 保留为脚本(此处不作介绍)

分析。扩展程序未声明任何 type: secret 参数,因此在第 4 步中无需迁移任何密钥。事件触发器已是第 2 代;只有任务队列函数仍是第 1 代(与第 3 步相关)。

2. 更新 package.json

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

最低 SDK 版本:将 firebase-functions >= 7.4.0 和 firebase-admin >= 14.2.0 声明为依赖项。将您的 firebase-functions 版本也声明为对等依赖项,以便用户的 Cloud Functions 项目具有与您的库所用的 SDK 相同的版本。

{
  "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.4.0"
  },
  "dependencies": {
    "firebase-functions": "^7.4.0",
    "firebase-admin": "^14.2.0"
  }
}

示例:从 Cloud 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"
  }
}

之后。可发布软件包:范围限定的名称、exports 映射和 firebase-functions 已移至 peerDependencies:

{
  "name": "@firebase-function-kits/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.4.0"
  },
  "dependencies": {
    "@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
    "firebase-admin": "^14.2.0",
    "firebase-functions": "^7.4.0"
  }
}

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

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

请参阅 Cloud Functions 第 2 代升级指南。 值得注意的是,您可以使用第 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 版本比较。

第 1 代函数和第 2 代函数之间的一个显著区别是,第 2 代函数必须与其触发资源位于同一位置。当用户从使用第 1 代函数的扩展程序迁移到使用第 2 代函数的套件时,可能需要更改函数位置才能满足此要求。

由于系统会保留现有位置,因此用户选择的任何 Cloud Functions 位置在迁移期间都可能会中断。如果扩展程序的事件触发函数依赖于用户提供的函数位置,我们建议您向扩展程序添加一个新参数,以收集事件触发位置并将其映射到新的函数位置。

示例:从 Cloud Firestore 到 BigQuery 的流

defineString("DATABASE_REGION", {
  label: "Firestore Instance Location",
  description:
    "Where is the Firestore database located? You can check your current database location at https://console.cloud.google.com/firestore/databases. The functions in this kit deploy to the Cloud Run region closest to this location.",
  input: select({
    "Multi-region (Europe - Belgium and Netherlands)": "eur3",
    "Multi-region (United States)": "nam5",
    "Multi-region (Iowa, North Virginia, and Oklahoma)": "nam7",
    "Iowa (us-central1)": "us-central1",
    // More locations...
  })
});

// Firestore multi-region locations are not Cloud Run regions; deploying a
// function to one hard-fails, so they map to a region inside the multi-region.
const MULTI_REGION_TO_FUNCTION_REGION: Record<string, string> = {
  nam5: "us-central1",
  nam7: "us-central1",
  eur3: "europe-west1",
};

/**
 * Maps a Firestore database location to the Cloud Run region the functions
 * should deploy to. The lookup is case-insensitive and ignores surrounding
 * whitespace, as the CLI's own region handling is. Regional locations pass
 * through lowercased; an unset or blank location returns `undefined`, meaning
 * the functions declare no region.
 */
export function firestoreLocationToFunctionRegion(
  location: string | undefined
): string | undefined {
  const normalized = location?.trim().toLowerCase();
  if (!normalized) {
    return undefined;
  }
  return MULTI_REGION_TO_FUNCTION_REGION[normalized] ?? normalized;
}

const functionRegion = firestoreLocationToFunctionRegion(
  process.env.DATABASE_REGION
);

export const fsexportbigquery = onDocumentWritten(
  {
    region: functionRegion,
    // Other configuration
  },
  (event) => handleDocumentWrite(event, getHandlerContext())
);

在首次部署该套件时,系统会提示用户输入 DATABASE_REGION,然后将函数部署到相应的 functionRegion,从而解决第 2 代事件触发函数的任何位置问题。

4. 转换扩展程序参数和 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> 中读取其值,或在部署期间提示用户输入值。保留相同的参数名称,以便沿用现有安装中的值。

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

示例:从 Cloud 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, analogous to "required: true" in extension.yaml
  input: { text: { nonEmpty: true } }
}),

参数名称保持不变,因此现有的 .env 仍可正常运行。

实例 ID

扩展程序从 EXT_INSTANCE_ID(由扩展程序运行时注入)读取其实例 ID。函数套件从 FIREBASE_KIT_INSTANCE_ID 读取其实例 ID,Firebase CLI 会将每个套件实例的 设置为 firebase.json 中 instances 映射中的实例键。CLI 在部署时发现期间、在模拟器中以及向已部署的函数提供该变量。

实例 ID 不是参数,因此请勿使用 defineString 进行声明。事实上,FIREBASE_... 是 .env 文件中的预留前缀,因此用户无法在此处设置或替换它。CLI 注入的值对参数系统不可见。直接从环境中读取:

// Before
const instanceId = process.env.EXT_INSTANCE_ID;

// After
const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;

仅当您的软件包作为套装部署时,系统才会设置此变量。如果您的代码也作为独立的代码库进行部署(请参阅第 9 步),请将其视为可选,或者在缺少该代码时快速失败并显示明确的消息。如果您的扩展程序将实例 ID 作为面向用户的参数公开,您必须移除该参数,因为 Firebase CLI 现在拥有该值。

示例:删除用户数据

(Stream Cloud Firestore 到 BigQuery 扩展程序不会读取其实例 ID,因此无需迁移任何内容。“删除用户数据”扩展程序使用它来命名其 Pub/Sub 主题。)

之前。在 config.ts 中读取为原始环境变量,带有扩展程序用于其资源的 ext- 前缀:

// functions/src/config.ts
discoveryTopic: `ext-${process.env.EXT_INSTANCE_ID}-discovery`,
deletionTopic: `ext-${process.env.EXT_INSTANCE_ID}-deletion`,

之后。对 FIREBASE_KIT_INSTANCE_ID 进行简单的 process.env 读取,用于两个普通形参,以便用户可以替换主题名称:

// src/config.ts
const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;

// Non-empty defaults so Pub/Sub trigger bindings resolve during deploy
// discovery without freezing an empty topic name into the manifest.
discoveryTopicName: defineString("DISCOVERY_TOPIC_NAME", {
  default: `kit-${instanceId}-discovery`,
}),
deletionTopicName: defineString("DELETION_TOPIC_NAME", {
  default: `kit-${instanceId}-deletion`,
}),

默认值不得为空,因为触发器绑定会在发现时得到解析。空默认值将作为主题名称写入部署清单。该套件还可防御在套件上下文之外运行的情况。如果缺少该变量,模块级默认值将评估为 kit-undefined-discovery,因此配置加载器会失败并显示说明性错误:

// ...
const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;
if (!instanceId) {
  throw new Error(
    "FIREBASE_KIT_INSTANCE_ID is not set. It is provided automatically to " +
      "kit instances by firebase-tools >= 15.32.0; deploy or emulate this " +
      "kit with a supported CLI version."
  );
}
// ...

此检查会在处理程序首次解析其配置时运行,因此如果缺少变量,系统会生成明确的运行时错误,而不是将函数静默绑定到 kit-undefined-* 主题。如需拒绝部署本身,请在模块范围内执行检查,以便在发现期间运行检查。由于 CLI 从 firebase.json 派生出实例 ID,因此没有可配置的 INSTANCE_ID,也不需要在多个实例之间保持同步。

密钥

在 extension.yaml 中,您可以使用 type: secret 声明 Secret。扩展程序运行时会存储并绑定这些变量,因此您的扩展程序代码可以直接读取 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 软件包/套件后,密钥引用会在最终用户的 .env 文件中进行管理。请务必不要更改代码中声明的 Secret 名称。在迁移期间,最终用户密钥会相应地迁移。

实际操作示例:通过 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();
    // ...
  }
);

5. 迁移内部任务队列调用

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

在 Admin SDK 的先前版本中,扩展程序需要将其自己的扩展程序实例 ID 作为第二个参数传递,以定位同一扩展程序中的任务队列函数。自 firebase-admin 14.2.0 起,此设置既不是必需的,也不建议使用。任务队列 API 现在默认定位到同一上下文(例如扩展程序或套件)中的任务队列。无论是作为扩展程序还是作为独立函数,您都可以放心地在代码中移除此参数,我们甚至鼓励您这样做。移除此参数可确保可移植性和向前兼容性。

enqueue 调用的其他所有方面(包括 locations/<region>/functions/<name> 资源路径、任务载荷和重试逻辑)都保持不变。

如需详细了解如何使用 Cloud Tasks 将函数加入队列,请参阅使用 Cloud Tasks 将函数加入队列。

之前。第 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);请参阅查看并安装替换函数套件实例和以函数套件形式进行测试。

6. 声明所需的 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 支持更窄的模型。

示例:从 Cloud 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/bigquery.dataEditor");
requiresRole("roles/datastore.user");
requiresRole("roles/bigquery.user");

如果您的扩展程序发布 Eventarc 事件,您还需要设置发布事件所需的相应角色和 API。之前,扩展程序会处理此问题,而您的 extensions.yaml 无需进行任何更改。您可以在代码中有条件地执行此操作,以便该套件仅在使用自定义 Eventarc 通道时请求这些权限。

if (!!process.env.EVENTARC_CHANNEL) {
  requiresRole("roles/eventarc.publisher");
  requiresAPI(
    "eventarcpublishing.googleapis.com",
    "Publishes the extension's custom events to its Eventarc channel."
  );
}

7. 转换生命周期钩子

如果您的扩展程序调用了 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

示例:从 Cloud 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 手动重新运行。

8. 为用户设置文档

编写一个软件包 README,其中至少应包含以下内容:

  • 软件包所需的 .env 值。
  • 软件包所需的密钥,以及如何迁移现有密钥值。
  • 软件包通过 requiresRole(...) 声明的 IAM 角色。
  • 软件包启用或需要的 Google API。
  • 软件包声明的生命周期钩子,以及如何手动重新运行这些钩子。
  • 结算备注。
  • 与原始扩展程序相比,发生了哪些变化。
  • 软件包如何获取其实例 ID(由 CLI 设置的 FIREBASE_KIT_INSTANCE_ID),以及所有实例的函数都部署为带有 kit-<instanceId>- 前缀。

示例:从 Cloud Firestore 到 BigQuery 的流

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

问题 作为扩展程序 As @firebase-function-kits/firestore-bigquery-export
配置 扩展程序参数 Cloud Functions通过 .env 传递的参数
IAM 由扩展程序授予 requiresRole(...),在部署时应用
正在预配 扩展程序的生命周期任务 afterFirstDeploy / afterRedeploy 项任务
函数名称 ext-<instanceId>-fsexportbigquery fsexportbigquery(可选前缀)
实例 ID EXT_INSTANCE_ID由扩展程序注入 FIREBASE_KIT_INSTANCE_ID,由 CLI 从 firebase.json 设置

9. 测试第 2 代函数

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

如需调用 setGlobalOptions 来设置全局选项(例如默认区域或 CPU),您必须仅在将套件部署为独立的第 2 代函数时执行此操作。当您的套件作为 npm 软件包安装时,用户会在封装代码中调用 setGlobalOptions 来配置这些参数,如果发生两次,他们会收到警告。 您可以通过检查 FIREBASE_KIT_INSTANCE_ID 环境变量来保护此调用:

import { setGlobalOptions } from "firebase-functions";

if (!process.env.FIREBASE_KIT_INSTANCE_ID) {
  setGlobalOptions({
    region: "us-east1",
    maxInstances: 10,
  });
}

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

firebase deploy --only functions

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

示例:从 Cloud Firestore 到 BigQuery 的流

我们会验证 Cloud Firestore 到 BigQuery 的端到端同步:

  1. 在 Firebase 控制台的 Cloud Firestore 页面中,创建您设置为 COLLECTION_PATH (users) 的集合(如果该集合尚不存在)。
  2. 创建一个名为 bigquery-mirror-test 的文档,其中包含任意字段和任意值。
  3. 在 Google Cloud 控制台的 BigQuery 页面中,查询原始更改日志表。它应包含记录文档创建的单行:

    SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`
    
  4. 查询最新视图,该视图应返回唯一存在的文档 (bigquery-mirror-test) 的最新更改事件:

    SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`
    
  5. 删除 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(部署为典型的第 2 代函数而非套件时没有前缀),而不是 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 的重新运行就是非交互式的。

10. 在 npm 上发布函数套件

验证从扩展程序到第 2 代函数的转换后,您可以按照以下任一指南将发布候选版本发布到 npm,以进行端到端测试:

当您的套件发布到 npm 后,可以使用 firebase functions:kits:install 进行安装,并列为扩展程序的官方替代项。

我们强烈建议您先发布候选版本。套件按软件包名称和版本进行安装,因此预发布版本可让您针对注册表测试实际安装流程,而不会向使用默认 latest 标记安装的用户公开未完成的软件包。

发布前:

  1. 选择软件包名称。无论是限定范围的名称还是未限定范围的名称,都可以使用(请参阅上文中的指南)。请注意,作用域软件包默认是私有的,因此请传递 --access public。
  2. 构建并检查哪些飞船。main 和 types 指向编译后的输出(在我们的示例中为 lib/),因此该目录必须包含在已发布的 tar 文件中。使用 .npmignore 或 files 许可名单,并使用 npm pack --dry-run 检查结果。运行构建的 prepublishOnly 脚本可防止发布过时的输出。

package.json 项新增功能,可确保默认情况下发布安全:

{
  "files": ["lib", "README.md", "CHANGELOG.md"],
  "publishConfig": { "access": "public", "tag": "next" },
  "scripts": {
    "build": "tsc -b",
    "prepublishOnly": "npm run build && npm test"
  }
}

字段 "publishConfig": { "tag": "next" } 可确保纯 npm publish 永远不会覆盖 latest。

创建候选版本:

例如,如需将版本从 0.0.2-rc.3 递增到 0.0.2-rc.4(如果 package.json 位于代码库根目录中,则会在 Git 中提交并添加标记),请执行以下操作:

npm version prerelease --preid rc

如需发布候选版本并在 npm 上注册 @your-org/your-kit@0.0.2-rc.4(使用 next 标记),请执行以下操作:

npm publish

npm 网站可能需要几分钟才能显示新版本;npm view 直接读取注册表:

npm view @your-org/your-kit versions dist-tags

完成本指南中的第 11 步和第 12 步后,您可以将软件包升级为稳定版:

npm version 0.0.2
npm publish --tag latest
npm dist-tag add @your-org/your-kit@0.0.2 next

关于 npm-shrinkwrap.json 的注意事项:我们强烈建议您在软件包中包含 npm-shrinkwrap.json 文件;如果您不包含该文件,CLI 会在安装时向用户发出警告。它可确保用户使用您测试过的确切依赖项,并有助于防范供应链攻击。不过,shrinkwrap 会在用户的项目中逐字应用,包括在 Cloud Functions 构建 (npm ci) 期间,其中仅限开发者的条目可能会失败并显示 EBADPLATFORM。您可能需要从已发布的收缩包装副本中剥离 "dev": true 条目和 devDependencies。

示例:从 Cloud Firestore 到 BigQuery 的流

在第四个候选版本发布时,该套件的 package.json 为:

{
  "name": "@firebase-function-kits/firestore-bigquery-export",
  "version": "0.0.2-rc.4",
  "repository": {
    "type": "git",
    "url": "https://github.com/firebase/extensions.git",
    "directory": "kits/firestore-bigquery-export"
  },
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "engines": { "node": "22" },
  "scripts": { "build": "tsc -b" }
}

请注意,在这种情况下,该软件包位于单代码库中,因此务必添加 repository.directory 以指向正确的文件夹。其 CHANGELOG.md 包含待发布版本的备注。

11. 作为函数套件进行测试

发布套件后,建议您使用 npm 测试套件。

确保您使用的是 firebase-tools 版本 >= 15.32.0 并安装该套件:

firebase functions:kits:install --package <your-package-name>@<your-prerelease-version>

此命令会从 npm 下载软件包,在套件的新源目录中设置该软件包,并引导您完成第一个实例的配置,类似于扩展程序安装流程。在本地安装并设置软件包后,运行部署以在 Google Cloud 项目中创建资源:

firebase deploy --only functions:<your-kit-instance-id>

安装后,Firebase CLI 会输出类似的部署命令,其中包含您在安装期间选择的确切实例 ID。

按照第 9 步。测试第 2 代函数。现在,您使用的是套件进行部署,因此函数会添加前缀并命名为 kit-<instance-id>-<method-name>。这样,套件就可以有多个实例,从而在项目中多次部署同一函数,每个实例都有一个唯一的名称。

12. 测试迁移替换

您可以设置一个可正常运行的扩展程序实例,然后使用用户迁移指南(使用 firebase ext:migrate --package 或函数套件 CLI 命令)完成函数套件的测试,以作为迁移替代方案。

13. 通知用户和 Google 您的官方扩展程序替代方案

当您的函数套件替代项准备就绪并以 npm 软件包的形式提供给用户进行迁移时,请将此正式替代项告知您的用户和 Google。使用以下信息更新托管 扩展程序的 GitHub 代码库中的 README.md 文件:

<!-- FIREBASE_EXTENSION_REPLACEMENT: extension="<your-extesion-id>" package="<your-npm-package-name>" -->
> [!WARNING]
> **Deprecation Notice:** The Firebase Extension `<your-extension>` is deprecated. Migrate to the [<your-npm-package-name>](<link-to-your-npm-package>) package.

Google 会扫描已知的扩展程序 README,查找类似 <!-- FIREBASE_EXTENSION_REPLACEMENT: extension="firebase/firestore-bigquery-export" package="@firebase-function-kits/firestore-bigquery-export" --> 的注释,并使用这些注释填充存储在 firebase-tools 代码库中的官方替换项注册表,如 replacements.json 中所示。 您还可以查看 replacements.json,了解系统将为您的扩展服务扫描哪些 README.md。官方替换列表每周更新一次。

示例:从 Cloud Firestore 到 BigQuery 的流

firestore-bigquery-export 扩展程序 README.md 包含:

<!-- FIREBASE_EXTENSION_REPLACEMENT: extension="firebase/firestore-bigquery-export" package="@firebase-function-kits/firestore-bigquery-export" -->
> [!WARNING]
> **Deprecation Notice:** The Firebase Extension `firebase/firestore-bigquery-export` is deprecated. Please migrate to the [`@firebase-function-kits/firestore-bigquery-export`](https://www.npmjs.com/package/@firebase-function-kits/firestore-bigquery-export) package.