Nâng cấp các hàm Node.js thế hệ 1 lên thế hệ 2

Các ứng dụng sử dụng hàm thế hệ thứ 1 nên cân nhắc việc di chuyển sang thế hệ thứ 2 theo hướng dẫn trong tài liệu này. Các hàm thế hệ thứ 2 sử dụng Cloud Run để mang lại hiệu suất tốt hơn, cấu hình tốt hơn, khả năng giám sát tốt hơn và nhiều lợi ích khác.

Các ví dụ trong tài liệu này giả định rằng bạn đang sử dụng JavaScript với các mô-đun CommonJS (nhập kiểu require), nhưng các nguyên tắc tương tự cũng áp dụng cho JavaScript với ESM (nhập kiểu import … from) và TypeScript.

Quy trình di chuyển

Các hàm thế hệ thứ 1 và thế hệ thứ 2 có thể cùng tồn tại trong cùng một tệp nguồn. Nhờ đó, bạn có thể di chuyển từng phần của cơ sở mã khi đã sẵn sàng. Tuy nhiên, lưu ý rằng việc kết hợp các gói này không hoạt động trong một hàm riêng biệt.

Bạn nên di chuyển từng hàm một, thực hiện kiểm thử và xác minh trước khi tiếp tục.

Xác minh phiên bản Firebase CLI và firebase-functions

Đảm bảo bạn đang sử dụng ít nhất phiên bản Firebase CLI 12.00 và phiên bản firebase-functions 4.3.0. Mọi phiên bản mới hơn sẽ hỗ trợ cả thế hệ thứ 2 và thế hệ thứ 1.

Cập nhật các lệnh nhập

Nhập các hàm thế hệ thứ 2 từ gói con v2 trong SDK firebase-functions. Đường dẫn nhập khác này là tất cả những gì CLI Firebase cần để xác định xem có nên triển khai mã hàm của bạn dưới dạng hàm thế hệ thứ nhất hay thế hệ thứ 2 hay không.

Gói con v2 có tính mô-đun và bạn chỉ nên nhập mô-đun cụ thể mà bạn cần.

Trước: Thế hệ thứ 1

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

Sau: Thế hệ thứ 2

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

Cập nhật định nghĩa về điều kiện kích hoạt

Vì SDK thế hệ thứ 2 ưu tiên các lần nhập theo mô-đun, hãy cập nhật các định nghĩa về điều kiện kích hoạt để phản ánh các lần nhập đã thay đổi ở bước trước.

Các đối số được truyền đến lệnh gọi lại cho một số điều kiện kích hoạt đã thay đổi. Trong ví dụ này, xin lưu ý rằng các đối số cho lệnh gọi lại onDocumentCreated đã được hợp nhất thành một đối tượng event duy nhất. Ngoài ra, một số điều kiện kích hoạt có các tính năng cấu hình mới thuận tiện, chẳng hạn như lựa chọn cors của điều kiện kích hoạt onRequest.

Trước: Thế hệ thứ 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) => {
    // ...
  });

Sau: Thế hệ thứ 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) => {
  /* ... */
});

Giảm thiểu nỗ lực viết lại bằng cách huỷ cấu trúc JavaScript

Nếu các hàm của bạn có phần nội dung phức tạp, phụ thuộc nhiều vào ngữ cảnh thế hệ thứ 1 hoặc các tham số dành riêng cho nhà cung cấp (như message hoặc snapshot), bạn có thể sử dụng các trình trợ giúp tương thích thế hệ thứ 1 được tích hợp vào SDK thế hệ thứ 2.

SDK thế hệ thứ 2 sẽ tự động vá đối tượng sự kiện bằng các phương thức truy xuất khớp với chữ ký thế hệ thứ 1. Điều này cho phép bạn sử dụng tính năng phân tách JavaScript để trích xuất trực tiếp các thuộc tính này trong chữ ký trình xử lý, giảm thiểu nhu cầu viết lại logic hàm.

Thông tin tham khảo về mối liên kết của nhà cung cấp

Nhà cung cấp Đối số thế hệ thứ 1 Phân tách sự kiện được vá thế hệ thứ 2
Pub/Sub (message, context) ({ message, context }) => { ... }
Cloud Firestore (snapshot, context) ({ snapshot, context }) => { ... }
Cloud Storage (object, context) ({ object, context }) => { ... }
Realtime Database (snapshot, context) ({ snapshot, context }) => { ... }
Remote Config (version, context) ({ version, context }) => { ... }
Cloud Scheduler (context) ({ context }) => { ... }
Hàng đợi tác vụ (data, context) ({ data, context }) => { ... }

Trước (thế hệ thứ 1):

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

Cách thay thế mới (thế hệ thứ 2 có tính năng Phân tách cấu trúc):

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
});

Sử dụng cấu hình có tham số

Các hàm thế hệ thứ 2 không còn hỗ trợ functions.config mà thay vào đó là một giao diện an toàn hơn để xác định các tham số cấu hình một cách khai báo bên trong cơ sở mã của bạn. Với mô-đun params mới, CLI sẽ chặn việc triển khai trừ phi tất cả các tham số đều có giá trị hợp lệ, đảm bảo rằng một hàm không được triển khai khi thiếu cấu hình.

Trước: Thế hệ thứ 1

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

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

Sau: Thế hệ thứ 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());
    // ...
  }
);

Nếu bạn có cấu hình môi trường hiện tại bằng functions.config, hãy di chuyển cấu hình này trong quá trình nâng cấp lên thế hệ thứ 2.

API functions.config không được dùng nữa và sẽ ngừng hoạt động vào tháng 3 năm 2027. Sau ngày đó, các hoạt động triển khai có functions.config sẽ không thành công.

Để tránh lỗi triển khai, hãy di chuyển cấu hình của bạn sang Cloud Secret Manager bằng cách sử dụng CLI Firebase. Bạn nên sử dụng cách này vì đây là cách hiệu quả và an toàn nhất để di chuyển cấu hình.

  1. Xuất cấu hình bằng giao diện dòng lệnh Firebase

    Sử dụng lệnh config export để xuất cấu hình môi trường hiện có sang một khoá bí mật mới trong 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. Cập nhật mã hàm để liên kết các khoá bí mật

    Để sử dụng cấu hình được lưu trữ trong khoá bí mật mới trong Cloud Secret Manager, hãy dùng API defineJsonSecret trong nguồn hàm của bạn. Ngoài ra, hãy đảm bảo rằng các khoá bí mật được liên kết với tất cả các hàm cần đến chúng.

    Trước

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

    Sau

    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. Triển khai hàm

    Triển khai các hàm đã cập nhật để áp dụng các thay đổi và liên kết các quyền truy cập bí mật.

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

Đặt các lựa chọn về thời gian chạy

Cấu hình của các lựa chọn thời gian chạy đã thay đổi giữa thế hệ thứ nhất và thế hệ thứ 2. Thế hệ thứ 2 cũng bổ sung một khả năng mới để đặt các lựa chọn cho tất cả các hàm.

Trước: Thế hệ thứ 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) => {
    // ...
  });

Sau: Thế hệ thứ 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) => {
  /* ... */
});

Cập nhật tài khoản dịch vụ mặc định (không bắt buộc)

Mặc dù các hàm thế hệ thứ 1 sử dụng tài khoản dịch vụ mặc định Google App Engine để uỷ quyền truy cập vào các API Firebase, nhưng các hàm thế hệ thứ 2 lại sử dụng tài khoản dịch vụ mặc định Compute Engine. Sự khác biệt này có thể dẫn đến các vấn đề về quyền đối với các hàm được di chuyển sang thế hệ thứ 2 trong trường hợp bạn đã cấp các quyền đặc biệt cho tài khoản dịch vụ thế hệ thứ 1. Nếu chưa thay đổi bất kỳ quyền nào của tài khoản dịch vụ, bạn có thể bỏ qua bước này.

Giải pháp được đề xuất là chỉ định rõ ràng tài khoản dịch vụ mặc định App Engine thế hệ thứ 1 hiện có cho các hàm mà bạn muốn di chuyển sang thế hệ thứ 2, ghi đè giá trị mặc định của thế hệ thứ hai. Bạn có thể thực hiện việc này bằng cách đảm bảo mỗi hàm đã di chuyển đều đặt giá trị chính xác cho 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) => {
  // ...
});

Ngoài ra, bạn có thể đảm bảo sửa đổi thông tin tài khoản dịch vụ để phù hợp với tất cả các quyền cần thiết trên cả tài khoản dịch vụ mặc định App Engine (cho Thế hệ thứ nhất) và tài khoản dịch vụ mặc định Compute Engine (cho Thế hệ thứ hai).

Sử dụng tính đồng thời

Một ưu điểm đáng kể của các hàm thế hệ thứ 2 là khả năng của một phiên bản hàm duy nhất có thể xử lý nhiều yêu cầu cùng một lúc. Điều này có thể giảm đáng kể số lần khởi động nguội mà người dùng cuối gặp phải. Theo mặc định, mức độ đồng thời được đặt ở 80, nhưng bạn có thể đặt thành bất kỳ giá trị nào từ 1 đến 1000:

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

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

Việc điều chỉnh mức độ đồng thời có thể cải thiện hiệu suất và giảm chi phí của các hàm. Tìm hiểu thêm về tính đồng thời trong phần Cho phép các yêu cầu đồng thời.

Kiểm tra việc sử dụng biến toàn cục

Các hàm thế hệ thứ nhất được viết mà không cần quan tâm đến tính đồng thời có thể sử dụng các biến chung được đặt và đọc trên mỗi yêu cầu. Khi tính đồng thời được bật và một phiên bản duy nhất bắt đầu xử lý nhiều yêu cầu cùng lúc, điều này có thể gây ra lỗi trong hàm của bạn khi các yêu cầu đồng thời bắt đầu thiết lập và đọc các biến toàn cục cùng lúc.

Trong khi nâng cấp, bạn có thể đặt CPU của hàm thành gcf_gen1 và đặt concurrency thành 1 để khôi phục hành vi của thế hệ thứ 1:

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

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

Tuy nhiên, bạn không nên dùng cách này làm giải pháp lâu dài vì nó sẽ làm mất đi những lợi thế về hiệu suất của các hàm thế hệ thứ 2. Thay vào đó, hãy kiểm tra việc sử dụng các biến toàn cục trong các hàm của bạn và xoá những chế độ cài đặt tạm thời này khi bạn đã sẵn sàng.

Di chuyển lưu lượng truy cập sang các hàm thế hệ thứ 2 mới

Giống như khi thay đổi khu vực hoặc loại trình kích hoạt của một hàm, bạn sẽ cần đặt tên mới cho hàm thế hệ thứ 2 và di chuyển lưu lượng truy cập sang hàm đó một cách từ từ.

Bạn không thể nâng cấp một hàm từ thế hệ thứ nhất lên thế hệ thứ 2 có cùng tên và chạy firebase deploy. Làm như vậy sẽ dẫn đến lỗi:

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

Chiến lược di chuyển phụ thuộc vào loại điều kiện kích hoạt mà hàm của bạn sử dụng.

Di chuyển các trình kích hoạt có thể gọi, hàng đợi tác vụ và HTTP

Đây là các điều kiện kích hoạt trực tiếp. Vì hàm thế hệ thứ 2 sẽ có tên mới (và URL mới cho các trình kích hoạt HTTP), nên bạn có thể di chuyển lưu lượng truy cập bằng cách cập nhật các ứng dụng.

  1. Đổi tên hàm trong mã của bạn (ví dụ: đổi tên myCallable thành myCallableV2).
  2. Triển khai hàm. Cả chức năng thế hệ thứ 1 và thế hệ thứ 2 hiện đang chạy.
  3. Cập nhật mã ứng dụng hoặc mã gọi để trỏ đến tên hoặc URL của hàm thế hệ thứ 2 mới.
  4. Sau khi tất cả lưu lượng truy cập đã chuyển sang hàm mới, hãy xoá hàm thế hệ thứ 1 bằng lệnh firebase functions:delete của CLI Firebase.

Di chuyển các trình kích hoạt ở chế độ nền

Các trình kích hoạt ở chế độ nền (chẳng hạn như trình kích hoạt Pub/Sub, Cloud FirestoreCloud Storage) sẽ phản hồi các sự kiện trong dự án của bạn. Để không bỏ lỡ bất kỳ sự kiện nào trong quá trình chuyển đổi, bạn phải tạm thời chạy song song cả hàm thế hệ thứ nhất và thế hệ thứ hai.

Trong giai đoạn chuyển đổi, cả hai hàm sẽ kích hoạt cùng một sự kiện. Điều này có nghĩa là logic kinh doanh của bạn sẽ chạy hai lần cho mỗi sự kiện. Đảm bảo hàm của bạn là đẳng cấu trước khi tiếp tục.

  1. Thêm hàm thế hệ thứ 2 cùng với hàm thế hệ thứ 1, giữ nguyên hàm thế hệ thứ 1 hiện có trong mã của bạn và thêm hàm thế hệ thứ 2 để theo dõi cùng một nguồn sự kiện.

    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. Chạy firebase deploy. Cả hai hàm hiện đang hoạt động và lắng nghe cùng một sự kiện.

  3. Xác minh rằng hàm thế hệ thứ 2 đang nhận lưu lượng truy cập. Theo dõi nhật ký cho cả hai hàm. Đảm bảo rằng hàm thế hệ thứ 2 được gọi cho tất cả các sự kiện và các lệnh gọi đều thành công.

  4. Khi bạn chắc chắn rằng hàm đang hoạt động chính xác, hãy di chuyển logic nghiệp vụ thực tế từ hàm thế hệ thứ 1 vào phần nội dung của hàm thế hệ thứ 2. Nếu bạn đã sử dụng phương thức truyền qua, hãy xoá lệnh gọi đến 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 ...
    });
    

    Triển khai thay đổi này.

  5. Xoá định nghĩa hàm thế hệ thứ 1 khỏi mã của bạn rồi triển khai lại. CLI sẽ nhắc bạn xoá hàm thế hệ thứ 1 khỏi Google Cloud.