將第 1 代 Node.js 函式升級至第 2 代

使用第 1 代函式的應用程式應考慮按照本指南中的操作說明,遷移至第 2 代函式。第 2 代函式使用 Cloud Run,可提供更優異的效能、設定和監控功能等。

本文中的範例假設您使用 JavaScript 和 CommonJS 模組 (require 樣式匯入),但相同的原則也適用於 JavaScript 和 ESM (import … from 樣式匯入) 和 TypeScript。

遷移程序

第 1 代和第 2 代函式可以在同一個檔案中並存。這樣一來,您就能在準備就緒時,逐步遷移程式碼集。建議您一次遷移一個函式,並在繼續操作前執行測試和驗證。

確認 Firebase CLI 和 firebase-functions 版本

請確認您使用的是至少 Firebase CLI 12.00firebase-functions 4.3.0 版本。任何更新版本都支援第 2 代和第 1 代。

更新匯入作業

第 2 代函式會從 firebase-functions SDK 的 v2 子套件匯入。Firebase CLI 只要有這個不同的匯入路徑,就能判斷要將函式程式碼部署為第 1 代或第 2 代函式。

v2 子套件是模組化套件,建議您只匯入所需的特定模組。

變更前:第 1 代

const functions = require("firebase-functions/v1");

變更後:第 2 代

// explicitly import each trigger
const {onRequest} = require("firebase-functions/v2/https");
const {onDocumentCreated} = require("firebase-functions/v2/firestore");

更新觸發條件定義

由於第 2 代 SDK 偏好模組化匯入,請更新觸發條件定義,反映上一步中變更的匯入項目。

傳遞至部分觸發條件回呼的引數已變更。在這個範例中,請注意 onDocumentCreated 回呼的引數已整合成單一 event 物件。此外,部分觸發條件也提供便利的新設定功能,例如 onRequest 觸發條件的 cors 選項。

變更前:第 1 代

const functions = require("firebase-functions/v1");

exports.date = functions.https.onRequest((req, res) => {
  // ...
});

exports.uppercase = functions.firestore
  .document("my-collection/{docId}")
  .onCreate((change, context) => {
    // ...
  });

變更後:第 2 代

const {onRequest} = require("firebase-functions/v2/https");
const {onDocumentCreated} = require("firebase-functions/v2/firestore");

exports.date = onRequest({cors: true}, (req, res) => {
  // ...
});

exports.uppercase = onDocumentCreated("my-collection/{docId}", (event) => {
  /* ... */
});

使用 JavaScript 解構功能,盡量減少重寫作業

如果函式有複雜的主體,且高度依賴第 1 代內容或供應商專屬參數 (例如 messagesnapshot),則可以使用第 2 代 SDK 內建的第 1 代相容性輔助程式。

第 2 代 SDK 會自動修補事件物件,並提供與第 1 代簽章相符的 getter。因此,您可以使用 JavaScript 解構,直接在處理常式簽章中擷取這些屬性,盡量減少重寫函式邏輯的需求。

供應商對應參考資料

供應商 第 1 代引數 第 2 代修補事件解構
Pub/Sub (message, context) ({ message, context }) => { ... }
Firestore (snapshot, context) ({ snapshot, context }) => { ... }
儲存空間 (object, context) ({ object, context }) => { ... }
即時資料庫 (snapshot, context) ({ snapshot, context }) => { ... }
遠端設定 (version, context) ({ version, context }) => { ... }
排程器 (context) ({ context }) => { ... }
工作佇列 (data, context) ({ data, context }) => { ... }

先前 (第 1 代):

export const myPubSubV1 = functions.pubsub.topic("my-topic").onPublish((message, context) => {
  const data = message.json;
  const eventId = context.eventId;
  // ... rest of the logic
});

新替代方案 (第 2 代,含解構):

import { onMessagePublished } from "firebase-functions/v2/pubsub";

export const myPubSubV2 = onMessagePublished("my-topic", ({ message, context }) => {
  // No need to change the function body!
  const data = message.json;      // Uses v1 Message wrapper
  const eventId = context.eventId; // Uses v1 EventContext map
  // ... rest of the logic
});

使用參數化設定

第 2 代函式會捨棄對 functions.config 的支援,改用更安全的介面,在程式碼庫中以宣告方式定義設定參數。使用新的 params 模組時,除非所有參數都有有效值,否則 CLI 會封鎖部署作業,確保函式不會在缺少設定的情況下部署。

變更前:第 1 代

const functions = require("firebase-functions/v1");

exports.getQuote = functions.https.onRequest(async (req, res) => {
  const quote = await fetchMotivationalQuote(functions.config().apiKey);
  // ...
});

變更後:第 2 代

const {onRequest} = require("firebase-functions/v2/https");
const {defineSecret} = require("firebase-functions/params");

// Define the secret parameter
const apiKey = defineSecret("API_KEY");

exports.getQuote = onRequest(
  // make the secret available to this function
  { secrets: [apiKey] },
  async (req, res) => {
    // retrieve the value of the secret
    const quote = await fetchMotivationalQuote(apiKey.value());
    // ...
  }
);

如果您有使用 functions.config 的現有環境設定,請在升級至第 2 代時遷移這項設定。

functions.config API 已淘汰,將於 2027 年 3 月停用。 屆時,使用 functions.config 的部署作業將會失敗。

為避免部署失敗,請使用 Firebase CLI 將設定遷移至 Cloud Secret Manager。強烈建議您採用這種方式,因為這是遷移設定最有效率且安全的方法。

  1. 使用 Firebase CLI 匯出設定

    使用 config export 指令將現有環境設定匯出至 Cloud Secret Manager 中的新密鑰:

    $ firebase functions:config:export
    i  This command retrieves your Runtime Config values (accessed via functions.config())
       and exports them as a Secret Manager secret.
    
    i  Fetching your existing functions.config() from your project...     Fetched your existing functions.config().
    
    i  Configuration to be exported:
    ⚠  This may contain sensitive data. Do not share this output.
    
    {
       ...
    } What would you like to name the new secret for your configuration? RUNTIME_CONFIG
    
    ✔  Created new secret version projects/project/secrets/RUNTIME_CONFIG/versions/1```
    
  2. 更新函式程式碼以繫結密鑰

    如要在 Cloud Secret Manager 中使用新密鑰儲存的設定,請在函式來源中使用 defineJsonSecret API。此外,請務必將 Secret 繫結至所有需要 Secret 的函式。

    之前

    const functions = require("firebase-functions/v1");
    
    exports.myFunction = functions.https.onRequest((req, res) => {
      const apiKey = functions.config().someapi.key;
      // ...
    });
    

    之後

    const { onRequest } = require("firebase-functions/v2/https");
    const { defineJsonSecret } = require("firebase-functions/params");
    
    const config = defineJsonSecret("RUNTIME_CONFIG");
    
    exports.myFunction = onRequest(
      // Bind secret to your function
      { secrets: [config] },
      (req, res) => {
        // Access secret values via .value()
        const apiKey = config.value().someapi.key;
        // ...
    });
    
  3. 部署函式

    部署更新後的函式,套用變更並繫結密鑰權限。

    firebase deploy --only functions:<your-function-name>
    

設定執行階段選項

第 1 代和第 2 代的執行階段選項設定有所不同。第 2 代也新增了為所有函式設定選項的功能。

變更前:第 1 代

const functions = require("firebase-functions/v1");

exports.date = functions
  .runWith({
    // Keep 5 instances warm for this latency-critical function
    minInstances: 5,
  })
  // locate function closest to users
  .region("asia-northeast1")
  .https.onRequest((req, res) => {
    // ...
  });

exports.uppercase = functions
  // locate function closest to users and database
  .region("asia-northeast1")
  .firestore.document("my-collection/{docId}")
  .onCreate((change, context) => {
    // ...
  });

變更後:第 2 代

const {onRequest} = require("firebase-functions/v2/https");
const {onDocumentCreated} = require("firebase-functions/v2/firestore");
const {setGlobalOptions} = require("firebase-functions/v2");

// locate all functions closest to users
setGlobalOptions({ region: "asia-northeast1" });

exports.date = onRequest({
    // Keep 5 instances warm for this latency-critical function
    minInstances: 5,
  }, (req, res) => {
  // ...
});

exports.uppercase = onDocumentCreated("my-collection/{docId}", (event) => {
  /* ... */
});

更新預設服務帳戶 (選用)

第 1 代函式使用 Google App Engine 預設服務帳戶授權存取 Firebase API,第 2 代函式則使用 Compute Engine 預設服務帳戶。如果您已將特殊權限授予第 1 代服務帳戶,這項差異可能會導致遷移至第 2 代的函式發生權限問題。如果沒有變更任何服務帳戶權限,可以略過這個步驟。

建議的解決方法是將現有的第 1 代 App Engine 預設服務帳戶,明確指派給要遷移至第 2 代的函式,藉此覆寫第 2 代預設值。如要這麼做,請確保每個遷移的函式都為 serviceAccountEmail 設定正確的值:

const {onRequest} = require("firebase-functions/https");
const {onDocumentCreated} = require("firebase-functions/v2/firestore");
const {setGlobalOptions} = require("firebase-functions");

// Use the App Engine default service account for all functions
setGlobalOptions({serviceAccountEmail: '<my-project-number>@<wbr>appspot.gserviceaccount.com'});

// Now I use the App Engine default service account.
exports.date = onRequest({cors: true}, (req, res) => {
  // ...
});

// I do too!
exports.uppercase = onDocumentCreated("my-collection/{docId}", (event) => {
  // ...
});

或者,您也可以修改服務帳戶詳細資料,確保 App Engine 預設服務帳戶 (適用於第 1 代) 和 Compute Engine 預設服務帳戶 (適用於第 2 代) 具備所有必要權限。

使用並行

第 2 代函式的一大優勢是單一函式執行個體可同時處理多個要求,這能大幅減少使用者遇到的冷啟動次數。並行處理預設為 80,但您可以將其設為 1 到 1000 之間的任何值:

const {onRequest} = require("firebase-functions/v2/https");

exports.date = onRequest({
    // set concurrency value
    concurrency: 500
  },
  (req, res) => {
    // ...
});

調整並行數可提升函式效能並降低成本。如要進一步瞭解並行,請參閱「允許並行要求」。

稽核全域變數用量

如果編寫第 1 代函式時未考慮並行,可能會使用在每個要求中設定及讀取的全域變數。啟用並行處理後,單一執行個體會開始同時處理多個要求,這可能會在函式中產生錯誤,因為並行要求會同時設定及讀取全域變數。

升級時,您可以將函式的 CPU 設為 gcf_gen1,並將 concurrency 設為 1,還原第 1 代行為:

const {onRequest} = require("firebase-functions/v2/https");

exports.date = onRequest({
    // TEMPORARY FIX: remove concurrency
    cpu: "gcf_gen1",
    concurrency: 1
  },
  (req, res) => {
    // ...
});

不過,我們不建議長期採用這種做法,因為這樣會失去第 2 代函式的效能優勢。請改為稽核函式中的全域變數使用情形,並在準備就緒時移除這些暫時設定。

將流量遷移至新的第 2 代函式

如同變更函式的區域或觸發類型時,您需要為第 2 代函式提供新名稱,並逐步將流量遷移至該函式。

您無法將名稱相同的第 1 代函式升級至第 2 代,並執行 firebase deploy。否則會發生以下錯誤:

Upgrading from GCFv1 to GCFv2 is not yet supported. Please delete your old function or wait for this feature to be ready.

遷移策略取決於函式使用的觸發條件類型。

適用於可呼叫、工作佇列和 HTTP 觸發條件

這些觸發條件是直接呼叫。由於第 2 代函式會有新名稱 (以及 HTTP 觸發條件的新網址),您可以更新用戶端來遷移流量。

  1. 重新命名程式碼中的函式 (例如將 myCallable 重新命名為 myCallableV2)。
  2. 部署函式。第 1 代和第 2 代函式現在都會執行。
  3. 更新用戶端程式碼或呼叫端,指向新的第 2 代函式名稱或網址。
  4. 所有流量都轉移至新函式後,請使用 Firebase CLI 的 firebase functions:delete 指令刪除第 1 代函式。

適用於背景觸發條件 (Pub/SubCloud FirestoreCloud Storage 等)

背景觸發條件會對專案中的事件做出回應。為避免在轉換期間遺漏任何事件,您必須暫時並行執行第 1 代和第 2 代函式。

在轉換期間,這兩個函式會針對相同事件觸發。這表示您的業務邏輯會針對每個事件執行兩次。請先確認函式是等冪,再繼續操作。

步驟 1:在第 1 代函式旁新增第 2 代函式

在程式碼中保留現有的第 1 代函式,並新增監聽相同事件來源的第 2 代函式。

專業提示:使用傳遞進行驗證 為避免在轉換期間重複程式碼集中的商家邏輯,或是在完全信任第 2 代函式前,先驗證第 2 代函式是否正確接收事件,請使用 run 方法,將第 2 代函式設為呼叫第 1 代函式的傳遞。

import * as functions from "firebase-functions/v1";
import { onMessagePublished } from "firebase-functions/v2/pubsub";

// --- Existing 1st gen function ---
export const myPubSub = functions.pubsub.topic("my-topic").onPublish((message, context) => {
  console.log("V1 handler running for event:", context.eventId);
  // ... existing v1 function logic ...
});

// --- New v2 passthrough function ---
export const myPubSubV2 = onMessagePublished("my-topic", async ({ message, context }) => {
   console.log("v2 handler triggering V1 for event:", context.eventId);
   // Call the v1 function's handler
   await myPubSub.run(message, context);
});

步驟 2:部署這兩個函式

執行 firebase deploy。 這兩個函式現在都處於啟用狀態,且會監聽相同的事件。

步驟 3:確認第 2 代函式是否收到流量

監控這兩個函式的記錄,確保所有事件都會叫用第 2 代函式,且呼叫成功。

步驟 4:將完整邏輯移至第 2 代函式

確認無誤後,請將實際的商業邏輯從第 1 代函式移至第 2 代函式的主體。如果您使用直通方法,請移除對 myPubSub.run() 的呼叫。

import * as functions from "firebase-functions/v1";
import { onMessagePublished } from "firebase-functions/v2/pubsub";

// --- Existing v1 function (to be removed next) ---
export const myPubSub = functions.pubsub.topic("my-topic").onPublish((message, context) => {
  console.log("v1 handler running for event:", context.eventId);
  // ... existing v1 function logic ...
});

// --- New v2 function with full logic ---
export const myPubSubV2 = onMessagePublished("my-topic", ({ message, context }) => {
   console.log("v2 handler running for event:", context.eventId);
   // ... existing v1 function logic WAS MOVED HERE ...
});

部署這項變更。

步驟 5:取消部署第 1 代函式

從程式碼中移除第 1 代函式定義,然後重新部署。CLI 會提示您從 Google Cloud 刪除第 1 代函式。

import { onMessagePublished } from "firebase-functions/v2/pubsub";

// --- V1 function definition REMOVED ---

// --- New v2 function with full logic ---
export const myPubSubV2 = onMessagePublished("my-topic", ({ message, context }) => {
   console.log("v2 handler running for event:", context.eventId);
   // ... existing v1 function logic ...
});