将已安装的 Firebase 扩展(或扩展源代码)转换为独立的 Cloud Functions for Firebase 代码库或可发布的 npm 包,包括将触发器从 V1 升级到 V2,以及配置生命周期钩子和声明式安全性的技能。
扩展到 Functions 代码库及 npm 包迁移
概述
本技能指导代理将 Firebase 扩展仓库或实例迁移为以下之一:
- 独立的 Cloud Functions for Firebase 代码库(
firebase-functions,用于最终用户应用集成),或 - 可发布的 npm 包 / 可共享的开源包(用于分发可复用 V2 函数的扩展发布者)。
它利用 Cloud Functions for Firebase 的正式可用(GA)能力,在代码中原生处理权限、依赖和生命周期钩子,并提供使用解构兼容垫片(Destructuring Compatibility Shim)将旧版 V1 触发器升级到 V2 的说明。
触发器
当用户请求以下操作时激活此技能:
- 将已安装的 Firebase 扩展迁移或转换为独立的 functions 代码库。
- 将扩展仓库转换为可发布的 npm 包(可共享的开源包)。
- 将扩展触发器从 V1 升级到 V2。
目标迁移工作流
开始之前,与开发者确定目标目的地:
-
目标 A:本地 Functions 代码库(最终用户应用集成)
- 输出:源代码放置在项目的
functions/src/文件夹中。 - 配置:通过
defineString、defineSecret等定义参数,并放在.env中。 - 部署:直接通过
firebase deploy --only functions部署。
- 输出:源代码放置在项目的
-
目标 B:可发布的 npm 包 / 可共享的开源包
- 输出:包含导出的 V2 函数的可复用 npm 包。
- 配置:
package.json中包含exports映射,并在dependencies(或peerDependencies)中声明firebase-functions。 - 使用:最终用户安装该包(
npm i <package-name>)并在其index.ts中重新导出函数。
开始与 Git 安全
- Git 状态:开始前验证工作区具有干净的 git 状态。
- 就地复制:如果在同一仓库内复制代码到新的子目录:
- 使用
git cp(或复制文件并提交)将扩展的源目录复制到目标目录。 - 立即提交:
"Copying [extension-name] extension to [directory] in preparation for rewrite"
- 使用
规则与约束
1. 零本地开销(Cloud Functions 集成)
假设 Cloud Functions for Firebase 工作负载身份、声明式安全性和 SDK 生命周期钩子已完全正式可用(GA)。
- 不要输出指示用户运行手动
gcloudIAM 命令或创建服务账户的说明或脚本。 - 不要编写指示用户在云控制台中手动启用 Google API 的代码注释。
- 相反,使用 SDK 中的声明式
requiresAPI和requiresRole导入。
2. 全局参数访问限制
- 切勿在全局作用域中对任何参数调用
.value()。 - 如果使用参数值初始化全局变量或类实例,请在全局声明变量,并在
onInit()回调中初始化:import { defineString } from "firebase-functions/params"; import { onInit } from "firebase-functions/v2"; const bqDataset = defineString("DATASET_ID"); let bqClient: BigQuery; onInit(() => { bqClient = new BigQuery({ datasetId: bqDataset.value() }); });
3. V2 的并发与成本对等
升级触发器到 V2 时:
- 默认情况下,V2 函数启用并发(每个实例最多 80 个请求)。
- 如果要保持 V1 的分级 CPU 定价(并禁用并发),请在函数的选项对象中设置
cpu: "gcf_gen1"。
逐步迁移执行
第 1 步:盘点扩展
对扩展声明、附带和文档化的所有内容进行全面盘点,以确保迁移过程中不丢失任何内容:
- 盘点
extension.yaml:params:转换为 Functions 参数(defineString、defineSecret等)。apis:转换为requiresAPI(...)声明。roles:转换为requiresRole(...)声明。lifecycleEvents(onInstall、onUpdate、onConfigure):转换为afterFirstDeploy和afterRedeploy钩子。resources:记录所有需要从第 1 代(firebase-functions/v1)转换为第 2 代(firebase-functions/v2)的函数触发器,包括标准事件触发器、HTTP 处理程序和任务队列(onTaskDispatched)。
- 盘点文件与工具:
functions/:源代码、触发器、辅助函数和任务队列处理程序。- 文档:
README.md、PREINSTALL.md和POSTINSTALL.md。 scripts/:记录扩展附带的任何回填、导入或辅助脚本。
第 2 步:创建 / 更新 package.json
为迁移的扩展代码创建 npm 包(在项目根目录或专用工作区目录):
- 设置可发布的包名(
name: "<package-name>")。 - 保留开发与测试依赖:保留旧扩展(
functions/package.json或根package.json)中所有现有的devDependencies、测试运行器(jest、ts-jest、@types/jest、mocha、@types/mocha)和测试脚本("test": "...")。不要丢弃测试框架或类型定义。 - 在
dependencies(或peerDependencies,如果创建轻量级中间件包,由根消费者管理运行时版本)中声明firebase-functions(例如^7.0.0):{ "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" }, "dependencies": { "firebase-admin": "^12.0.0", "firebase-functions": "^7.0.0" } }
第 3 步:移动函数代码并暴露可部署函数
将扩展的函数源代码移动到包的 src/ 文件夹中:
- 保留正常的 Firebase 触发器导出。包必须暴露可部署的函数,最终用户可以从其入口点(
index.ts)重新导出。 - 记录消费者必须通过重新导出来部署打包函数:
注意:仅裸导入(export * from "<package-name>"; // 或命名导出: // export { syncV2, initBigQuerySync } from "<package-name>";import "<package-name>")是不够的。Firebase CLI 只部署从用户根入口文件导出的函数。
第 4 步:将函数从第 1 代升级到第 2 代
将每个导出的第 1 代函数触发器(onWrite、onRequest、tasks.taskQueue().onDispatch)转换为对应的第 2 代触发器(onDocumentWritten、onRequest、onTaskDispatched,来自 firebase-functions/v2/...):
- 签名与解构垫片:使用解构兼容垫片(
{ shimmedKey, context })保留 V1 业务逻辑,无需重写函数体。参见 signature-mapping.md 和 destructuring-shim.md。 - 身份验证触发器例外:如果您的扩展使用 v1 身份验证触发器(
auth.user().onCreate()、auth.user().onDelete()),指示代理实时检查已安装的firebase-functions包或实时文档中是否存在第 2 代替代方案。如果这些事件尚不支持第 2 代 Auth 触发器,请明确警告用户,并在 V2 替代方案可用之前暂停或拒绝这些特定触发器的迁移。
第 5 步:用 Functions 参数替换扩展参数
extension.yaml 中的每个参数都变为参数化配置调用:
- 映射参数原始类型和属性:
- 类型
string->
defineString("PARAM_NAME", { label: "...", description: "...", default: "..." }) - 类型
secret->defineSecret("PARAM_NAME") - 类型
int->defineInt("PARAM_NAME", { label: "...", default: 123 }) - 类型
boolean->defineBoolean("PARAM_NAME", { default: true }) - 类型
select/multiSelect-> 将options映射到input:
defineString("PARAM_NAME", { input: { select: { options: [{ value: "val", label: "Val" }] } } }) validationRegex-> 映射到文本输入选项:
defineString("PARAM_NAME", { input: { text: { validationRegex: "^[a-z]+$" } } })required: true/ 非空验证 -> 将nonEmpty: true映射到输入选项:
defineString("PARAM_NAME", { input: { text: { nonEmpty: true } } })
- 类型
- 在处理程序内部使用
paramName.value()读取参数值。 - 保持参数名称不变:切勿更改参数名称(
COLLECTION_PATH、DATASET_ID等),以便现有值在.env中无缝延续。
第 6 步:迁移内部任务队列调用(queue.enqueue(...))
如果您的扩展代码使用 Admin SDK(getFunctions().taskQueue(...))将任务入队到自己的队列:
- 在扩展运行时下,Admin SDK 通过函数名称加上
process.env.EXT_INSTANCE_ID解析队列。 - 在 npm 包 / 常规代码库中:完全移除第二个参数(
EXT_INSTANCE_ID)。Admin SDK 自动定位当前代码库:// 之前(扩展运行时): // const queue = getFunctions().taskQueue(`locations/${region}/functions/syncBigQuery`, process.env.EXT_INSTANCE_ID); // 之后(npm 包): const queue = getFunctions().taskQueue(`locations/${region}/functions/syncBigQuery`); await queue.enqueue(taskData);
第 7 步:迁移密钥
对于声明为 type: secret 的参数:
- 显式声明密钥:
import { defineSecret } from "firebase-functions/params"; const apiKey = defineSecret("API_KEY"); - 在触发器选项中绑定密钥:
export const fn = onRequest({ secrets: [apiKey] }, handler); - 保持密钥名称不变,以便现有的 Secret Manager 绑定正常工作。
第 8 步:声明所需的 API 和 IAM 角色
用入口文件中的声明式代码替换 extension.yaml 中的 apis 和 roles:
import { requiresAPI, requiresRole } from "firebase-functions";
requiresAPI("bigquery.googleapis.com", "需要写入变更日志行");
requiresRole("roles/bigquery.dataEditor");
requiresRole("roles/bigquery.user");
部署时,Firebase CLI 自动将这些角色授予托管运行时服务账户,并启用所需的 API。
第 9 步:转换生命周期钩子(afterFirstDeploy 和 afterRedeploy)
用 SDK 生命周期钩子替换 lifecycleEvents(onInstall、onUpdate、onConfigure):
- 将任务队列处理程序(
initBigQuerySync、setupBigQuerySync)转换为来自firebase-functions/v2/tasks的 V2onTaskDispatched(移除旧的getExtensions().runtime().setProcessingState(...)调用)。 - 在代码中注册生命周期钩子:
import { afterFirstDeploy, afterRedeploy } from "firebase-functions/v2"; // 替换 onInstall: afterFirstDeploy({ task: { function: "runInitialSetup", body: {} } }); // 替换 onUpdate 和 onConfigure: afterRedeploy({ task: { function: "runInitialSetup", body: { reconcile: true } } }); - 确保处理程序是幂等的,并记录手动重新运行的命令:
firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME firebase functions:lifecycle:run afterRedeploy CODEBASE_NAME
第 10 步:为用户记录设置(README.md)
保留原始扩展在 README.md(以及 PREINSTALL.md / POSTINSTALL.md)中已记录的任何文档、密钥和设置说明,并根据需要进行更新:
- 更新配置引用:将引用旧版
extension.yaml安装提示或ext-*.env文件的说明替换为与defineString参数匹配的标准.env参数化配置设置。 - 包含重新导出代码片段:确保显示基本的根重新导出片段(
export * from "<package-name>";),以便用户知道如何在根index.ts中暴露函数。 - 避免不必要的样板:如果设置已在代码中显而易见或已由现有 README 部分覆盖,则不要生成冗余的比较表或通用安装步骤。
第 11 步:构建与测试验证
- 验证源代码编译:运行
npm run build(tsc)以确保src/中没有 TypeScript 编译错误。 - 验证单元测试套件和类型定义:
- 如果扩展中存在现有单元测试(
__tests__/、test/):- 确保
@types/jest(或原始测试框架类型)存在于devDependencies中,以便测试文件干净地通过类型检查。 - 更新升级后的 V2 触发器的测试调用,以传递单个解构事件对象
(({ change: mockChange, context: mockContext })而不是位置参数(mockChange, mockContext))。 - 运行
npm test或对测试文件进行类型检查(npx tsc --noEmit)以验证零回归。
- 确保
- 如果扩展中存在现有单元测试(
参考与官方资源
- 官方 Google 迁移指南:
准备 Firebase 扩展以迁移到 Cloud Functions - 发布者支持联系方式:
- 支持邮箱:
firebase-extensions-migrator-support-external@google.com - 支持群组订阅:
firebase-extensions-migrator-support-external+subscribe@google.com
- 支持邮箱:
- 解构垫片:参见
destructuring-shim.md 了解事件属性转换的详细信息。 - 触发器映射:参见
signature-mapping.md 了解 V1 与 V2 触发器定义和垫片键。 - 配置与参数:参见
configuration-migration.md 了解runWith选项、参数和密钥绑定。






