Firebase Functions Gen2部署失败求助:代码分析错误排查
已有多个正常运行的Firebase Functions Gen1函数,新增Gen2函数并计划升级所有Gen1函数时,部署出现以下错误:
shutdown requested via /__/quitquitquit
Error: Functions codebase could not be analyzed successfully. It may have a syntax or runtime error
注释掉该Gen2函数后部署可正常完成,推测问题出在Gen2函数代码中。
Gen2函数代码
"use strict"; Object.defineProperty(exports, "__esModule", {value: true}); const {getFirestore} = require("firebase-admin/lib/firestore"); const db = getFirestore(); const {onDocumentWritten} = require("firebase-functions/v2/firestore"); export const crmGroupRecipientsCounter = onDocumentWritten("marketing/crm/recipients/{recipientID}", async (event:any) => { // Get an object with the current document va/lues. // If the document does not exist, it was deleted if (event.data) { // const document = event.data.after.data(); const id = event.data.after.id; const counter = await getGroupCount(id); updateGroupCounter(id, counter).then((r) => null); } }); const getGroupCount = async (id:string) => { const recipients = db.collection("marketing").doc("crm").collection("recipients"); const snapshot = await recipients.where("guid", "==", id).where("disabled", "==", false).get(); if (!snapshot.empty) { return snapshot.size; } else { return 0; } }; const updateGroupCounter = async (id:string, counter:number) => { await db.collection("marketing").doc("crm").collection("recipients").doc(id).update({ counter: counter, }); };
index.ts文件
// // Start writing Firebase Functions // // https://firebase.google.com/docs/functions/typescript // // export const helloWorld = functions.https.onRequest((request, response) => { // functions.logger.info("Hello logs!", {structuredData: true}); // response.send("Hello from Firebase!"); // }); import {initializeApp, cert} from "firebase-admin/app"; // const {initializeApp, cert} = require("firebase-admin/lib/app"); const serviceAccount = require("../env/serviceAccount.json"); initializeApp({ credential: cert(serviceAccount), databaseURL: "https://xxxxxxxx.firebaseio.com", }); import {crmGroupRecipientsCounter} from "./actions/marketing-module/countGroupRecipients"; exports.crmGroupRecipientsCounter = crmGroupRecipientsCounter; const getDailyRate = require("./actions/get-daily-rates"); exports.getDailyRate = getDailyRate.getDailyRate; const sendMail = require("./actions/send-mails"); exports.postmark = sendMail.postmark; exports.cronMail = sendMail.cronMail; const manageUser = require("./actions/manage-guests"); exports.manageUser = manageUser.manageUser; const bulkReservations = require("./actions/bulk-create-reservations"); exports.bulkCreateReservations = bulkReservations.bulkCreateReservations; const cFixer = require("./actions/credentials-fixer"); exports.credentialsFixer = cFixer.fixCredentials; const getWeather = require("./actions/get-weather"); exports.getWeather = getWeather.getWeather; const reservationTracker = require("./actions/track-last-reservation"); exports.reservationTracker = reservationTracker.reservationTracker;
firebase.json文件
{ "functions": { "source": "functions", "codebase": "default", "runtime": "nodejs20", "predeploy": [ "npm --prefix \"$RESOURCE_DIR\" run lint", "npm --prefix \"$RESOURCE_DIR\" run build" ] }, "hosting": { "public": "dist/pwa", "site": "my-site", "ignore": [ "firebase.json", "**/.*", "**/node_modules/**" ], "rewrites": [ { "source": "**", "destination": "/index.html" } ], "headers": [ { "source": "service-worker.js", "headers": [ { "key": "Cache-Control", "value": "max-age=0" } ] }, { "source": "/**", "headers": [ { "key": "Cache-Control", "value": "no-cache, no-store, must-revalidate" } ] } ] } }
package.json文件
{ "name": "functions", "scripts": { "lint": "eslint --ext .js,.ts .", "build": "tsc", "serve": "npm run build && firebase emulators:start --only functions", "shell": "npm run build && firebase functions:shell", "start": "npm run shell", "deploy": "firebase deploy --only functions", "logs": "firebase functions:log" }, "engines": { "node": "20", "npm": ">= 6.13.4" }, "main": "lib/index.js", "dependencies": { "axios": "^1.4.0", "cors": "^2.8.5", "dayjs": "^1.11.9", "es6-promise-pool": "^2.5.0", "firebase-admin": "^11.10.1", "firebase-functions": "^4.4.1", "indicadores-economicos-bccr": "^2.0.1", "openweather-apis": "^4.5.0", "postmark": "^3.0.18" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", "eslint": "^8.45.0", "eslint-config-google": "^0.14.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-promise": "^6.1.1", "firebase-functions-test": "^0.3.3", "typescript": "^5.1.6" }, "private": true }
开发环境信息
OS: Mac OS Ventura Computer: Macbook Pro M1 Pro Node version: v20.4.0 Firebase Tools version: firebase-tools@12.4.4
此前使用node v18.5时也出现相同问题,根据文档node v20应该兼容,同时希望得到代码结构优化建议。
1. 代码语法与导入问题
Gen2函数混用
require和ES模块导出,且Firebase Admin导入路径错误。统一使用ES模块语法,正确导入方式:import { getFirestore } from 'firebase-admin/firestore'; import { onDocumentWritten } from 'firebase-functions/v2/firestore';避免从
firebase-admin/lib/firestore这类内部路径导入。增加
event.data.after的空值判断:当文档被删除时,event.data.after为null,外层if(event.data)无法覆盖该场景:if (event.data?.after) { const id = event.data.after.id; // 后续逻辑 }处理
updateGroupCounter的Promise,避免函数提前终止:await updateGroupCounter(id, counter);替换原有的
.then((r) => null)写法。
2. 依赖版本兼容
当前firebase-functions版本^4.4.1可能存在Node.js 20兼容性问题,升级到最新版本:
npm install firebase-functions@latest
3. 解决无限循环问题
当前函数监听marketing/crm/recipients/{recipientID}的写入事件,又更新该文档的counter字段,会触发函数重复执行。可通过以下方式修复:
- 更新前检查当前
counter值是否与计算值一致,一致则跳过更新; - 使用事务确保操作原子性,避免重复触发;
- 调整监听逻辑,避免更新操作触发自身。
- 统一模块语法:所有文件使用ES模块(
import/export)替代CommonJS(require/exports),避免混合使用导致编译错误。 - 分离Admin初始化:将Firebase Admin的初始化逻辑单独放在
admin.ts文件中,其他函数导入初始化后的实例,避免重复初始化。 - 类型安全:为
event参数指定正确类型(Event<DocumentSnapshotBeforeAndAfter<DocumentData>>),替换any类型,提升代码可维护性。 - 错误处理:所有异步操作添加
try/catch块,使用Firebase日志工具记录错误:import { logger } from 'firebase-functions'; try { // 异步操作逻辑 } catch (error) { logger.error('操作失败', error); throw error; } - 减少硬编码:将重复的集合路径定义为常量:
const RECIPIENTS_COLLECTION = 'marketing/crm/recipients'; const recipientsRef = db.collection(RECIPIENTS_COLLECTION);
内容的提问来源于stack exchange,提问作者NaturalDevCR

