ارتقا دادن توابع Node.js نسل اول به نسل دوم

برنامه‌هایی که از توابع نسل اول استفاده می‌کنند باید بااستفاده از دستورالعمل‌های این راهنما به نسل دوم منتقل شوند. توابع نسل دوم از Cloud Run برای ارائه عملکرد بهتر، پیکربندی بهتر، نظارت بهتر، و غیره استفاده می‌کنند.

نمونه‌های این سند فرض می‌کنند که از جاوا اسکریپت با ماژول‌های CommonJS (واردات سبک require) استفاده می‌کنید، اما همین اصول برای جاوا اسکریپت با ESM (واردات سبک import … from) و TypeScript نیز اعمال می‌شود.

فرایند انتقال

توابع نسل اول و نسل دوم می‌توانند در یک فایل منبع در کنار هم وجود داشته باشند. این کار به شما امکان می‌دهد پایگاه کدتان را هروقت آماده بودید بخش‌بخش انتقال دهید. بااین‌حال، توجه داشته باشید که این ترکیب بسته‌ها در یک تابع مجزای واحد کار نمی‌کند.

توصیه می‌کنیم توابع را یکی‌یکی منتقل کنید و قبل‌از ادامه، آزمایش و درستی‌سنجی را انجام دهید.

درستی‌سنجی نسخه‌های Firebase CLI و firebase-functions

مطمئن شوید که حداقل از نسخه 12.00 از Firebase CLI و نسخه 4.3.0 از firebase-functions استفاده می‌کنید. هر نسخه جدیدتری از نسل دوم و همچنین نسل اول پشتیبانی خواهد کرد.

به‌روزرسانی واردات

توابع نسل دوم از زیربسته v2 در کیت توسعه نرم‌افزار firebase-functions وارد می‌شوند. این مسیر وارد کردن متفاوت تنها چیزی است که Firebase CLI برای تعیین اینکه کد تابع شما را به‌عنوان تابع نسل اول یا دوم مستقر کند نیاز دارد.

زیربسته v2 مدولار است و توصیه می‌کنیم فقط ماژول خاصی را که نیاز دارید وارد کنید.

قبل: نسل اول

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

بعد: نسل دوم

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

به‌روزرسانی تعریف‌های راه‌انداز

ازآنجایی‌که کیت توسعه نرم‌افزار (SDK) نسل دوم از واردات مدولار پشتیبانی می‌کند، تعاریف راه‌انداز را به‌روز کنید تا واردات تغییریافته از مرحله قبلی را منعکس کند.

متغیرهای مستقل ارسال‌شده به فراخوانی‌های برگشتی برای برخی‌از راه‌اندازها تغییر کرده است. در این مثال، توجه داشته باشید که آرگومان‌های مربوط به فراخوانی برگشتی onDocumentCreated در یک شیء event واحد ادغام شده‌اند. علاوه‌براین، برخی‌از راه‌اندازها ویژگی‌های پیکربندی جدید و راحتی دارند، مثل گزینه cors راه‌انداز onRequest.

قبل: نسل اول

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

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

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

بعد: نسل دوم

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) => {
  /* ... */
});

به‌حداقل رساندن تلاش‌های بازنویسی با ساختارشکنی جاوا اسکریپت

اگر توابع شما دارای بدنه‌های پیچیده‌ای هستند که به‌شدت به بافت نسل اول یا پارامترهای خاص ارائه‌دهنده (مانند message یا snapshot) متکی هستند، می‌توانید از کمک‌کننده‌های سازگاری نسل اول که در کیت توسعه نرم‌افزار نسل دوم تعبیه شده‌اند استفاده کنید.

کیت توسعه نرم‌افزار نسل دوم به‌طور خودکار شیء رویداد را با گیرنده‌هایی که با امضاهای نسل اول مطابقت دارند وصله می‌کند. این به شما امکان می‌دهد از ساختارشکنی جاوا اسکریپت برای استخراج این ویژگی‌ها مستقیماً در امضای گرداننده استفاده کنید، که نیاز به بازنویسی منطق تابع شما را به حداقل می‌رساند.

مرجع نگاشت ارائه‌دهنده

ارائه‌دهنده متغیرهای مستقل نسل اول ساختارشکنی رویداد وصله‌شده نسل دوم
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 }) => { ... }
صف کار (data, context) ({ data, context }) => { ... }

قبل (نسل اول):

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

جایگزین جدید (نسل دوم با ساختارشکنی):

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

استفاده از پیکربندی پارامترشده

توابع نسل دوم پشتیبانی از functions.config را به‌نفع میانای امن‌تر برای تعریف کردن پارامترهای پیکربندی به‌صورت اعلانی در پایگاه کد شما متوقف می‌کنند. با ماژول جدید params، «میانای خط فرمان» استقرار را مسدود می‌کند مگراینکه همه پارامترها مقدار معتبری داشته باشند، تا اطمینان حاصل شود که تابع بدون پیکربندی موردنیاز مستقر نمی‌شود.

قبل: نسل اول

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

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

بعد: نسل دوم

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 دارید، این پیکربندی را به‌عنوان بخشی از ارتقا به نسل دوم انتقال دهید.

‫functions.config API منسوخ شده است و در مارس ۲۰۲۷ از رده خارج خواهد شد. پس‌از آن تاریخ، استقرار با functions.config ناموفق خواهد بود.

برای جلوگیری از شکست استقرار، پیکربندی‌تان را بااستفاده از رابط خط فرمان Firebase به 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 در منبع تابع خود استفاده کنید. همچنین، مطمئن شوید که اطلاعات محرمانه به همه توابعی که به آن‌ها نیاز دارند محدود شده باشند.

    قبل

    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>
    

تنظیم گزینه‌های زمان اجرا

پیکربندی گزینه‌های زمان اجرا بین نسل اول و دوم تغییر کرده است. نسل دوم همچنین قابلیت جدیدی برای تنظیم گزینه‌ها برای همه عملکردها اضافه می‌کند.

قبل: نسل اول

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) => {
    // ...
  });

بعد: نسل دوم

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) => {
  /* ... */
});

به‌روزرسانی حساب سرویس پیش‌فرض (اختیاری)

درحالی‌که توابع نسل اول از حساب سرویس پیش‌فرض Google App Engine برای مجاز کردن دسترسی به میاناهای برنامه‌سازی کاربردی Firebase استفاده می‌کنند، توابع نسل دوم از حساب سرویس پیش‌فرض Compute Engine استفاده می‌کنند. این تفاوت می‌تواند منجر به مشکلات اجازه‌ها برای عملکردهای منتقل‌شده به نسل دوم شود، در مواردی که اجازه‌های خاصی به حساب سرویس نسل اول اعطا کرده‌اید. اگر هیچ‌کدام از اجازه‌های حساب سرویس را تغییر نداده‌اید، می‌توانید از این مرحله رد شوید.

راهکار توصیه‌شده این است که App Engine حساب سرویس پیش‌فرض نسل اول موجود را به‌طور صریح به توابعی که می‌خواهید به نسل دوم منتقل کنید اختصاص دهید و پیش‌فرض نسل دوم را لغو کنید. برای این کار، مطمئن شوید هر تابع منتقل‌شده مقدار درستی برای 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 (برای نسل اول) و حساب سرویس پیش‌فرض Compute Engine (برای نسل دوم) مطابقت داشته باشد.

استفاده از هم‌رس‌ها

مزیت مهم توابع نسل دوم این است که یک نمونه تابع می‌تواند به‌طور هم‌زمان به بیش‌از یک درخواست پاسخ دهد. این کار می‌تواند تعداد راه‌اندازی‌های سردی را که کاربران نهایی تجربه می‌کنند به‌طور چشمگیری کاهش دهد. به‌طور پیش‌فرض، همزمانی روی ۸۰ تنظیم شده است، اما می‌توانید آن را روی هر مقداری از ۱ تا ۱۰۰۰ تنظیم کنید:

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

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

تنظیم هم‌زمانی می‌تواند عملکرد را بهبود بخشد و هزینه توابع را کاهش دهد. درباره هم‌زمانی در مجاز کردن درخواست‌های هم‌زمان بیشتر بدانید.

ممیزی استفاده از متغیر سراسری

توابع نسل اول که بدون درنظر گرفتن همزمانی نوشته شده‌اند ممکن است از متغیرهای سراسری استفاده کنند که در هر درخواست تنظیم و خوانده می‌شوند. وقتی هم‌زمانی فعال باشد و یک نمونه واحد شروع به رسیدگی به چند درخواست به‌طور هم‌زمان کند، ممکن است اشکالاتی در تابع شما ایجاد شود زیرا درخواست‌های هم‌زمان شروع به تنظیم و خواندن متغیرهای سراسری به‌طور هم‌زمان می‌کنند.

هنگام ارتقا دادن، می‌توانید CPU تابع را روی gcf_gen1 تنظیم کنید و concurrency را روی ۱ تنظیم کنید تا رفتار نسل اول بازیابی شود:

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

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

بااین‌حال، این کار به‌عنوان راه‌حل بلندمدت توصیه نمی‌شود زیرا مزایای عملکردی توابع نسل دوم را ازدست می‌دهد. درعوض، استفاده از متغیرهای سراسری را در توابع خود ممیزی کنید و وقتی آماده بودید، این تنظیمات موقت را بردارید.

انتقال ترافیک به توابع نسل دوم جدید

درست مانند زمانی که منطقه یا نوع راه‌انداز تابع را تغییر می‌دهید، باید نام جدیدی به تابع نسل دوم بدهید و به‌آرامی ترافیک را به آن منتقل کنید.

امکان ارتقا دادن تابع از نسل ۱ به نسل ۲ با همان نام و اجرای firebase deploy وجود ندارد. انجام این کار منجر به این خطا می‌شود:

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

راهبرد انتقال به نوع راه‌اندازی که تابع شما استفاده می‌کند بستگی دارد.

انتقال راه‌اندازهای Callable،‏ Task Queue، و HTTP

این راه‌اندازها فراخوانی‌های مستقیم هستند. ازآنجایی‌که تابع نسل دوم نام جدیدی (و نشانی وب جدیدی برای راه‌اندازهای HTTP) خواهد داشت، می‌توانید با به‌روزرسانی کارخواه‌ها، ترافیک را منتقل کنید.

  1. نام تابع را در کدتان تغییر دهید (برای مثال، نام myCallable را به myCallableV2 تغییر دهید).
  2. تابع را مستقر کنید. اکنون هر دو عملکرد نسل اول و نسل دوم درحال اجرا هستند.
  3. کد کارخواه یا فراخواننده را به‌روز کنید تا به نام یا نشانی وب تابع نسل دوم جدید اشاره کند.
  4. وقتی همه ترافیک به تابع جدید منتقل شد، تابع نسل اول را بااستفاده از فرمان firebase functions:delete در Firebase CLI حذف کنید.

انتقال راه‌اندازهای پس‌زمینه

راه‌اندازهای پس‌زمینه (مانند راه‌اندازهای Pub/Sub، Cloud Firestore، و Cloud Storage) به رویدادهای پروژه‌تان پاسخ می‌دهند. برای جلوگیری از ازدست رفتن هرگونه رویداد درطول انتقال، باید موقتاً هر دو تابع نسل اول و نسل دوم را به‌طور هم‌زمان اجرا کنید.

درطول دوره انتقال، هر دو تابع در یک رویداد یکسان راه‌اندازی خواهند شد. این یعنی منطق کسب‌وکارتان دو بار در هر رویداد اجرا خواهد شد. قبل‌از ادامه دادن، مطمئن شوید تابع شما هم‌پایا است.

  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. تأیید کنید که تابع نسل دوم ترافیک دریافت می‌کند. گزارش‌های هر دو تابع را پایش کنید. مطمئن شوید که تابع نسل دوم برای همه رویدادها فراخوانی می‌شود و تماس‌ها موفقیت‌آمیز است.

  4. وقتی مطمئن شدید که تابع به‌درستی کار می‌کند، منطق کسب‌وکار واقعی را از تابع نسل اول به بدنه تابع نسل دوم منتقل کنید. اگر از روش عبور استفاده کردید، فراخوانی 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. تعریف تابع نسل اول را از کدتان بردارید و دوباره مستقر کنید. رابط خط فرمان از شما می‌خواهد تابع نسل اول را از Google Cloud حذف کنید.