با استفاده از Node.js Admin SDK، یک برنامه Angular پشته کامل را در Cloud Run با Firestore اجرا کنید.

با استفاده از Node.js Admin SDK، یک برنامه Angular پشته کامل را در Cloud Run با Firestore اجرا کنید.

درباره این codelab

subjectآخرین به‌روزرسانی: آوریل ۲, ۲۰۲۵
account_circleنویسنده: Luke Schlangen

1. نمای کلی

Cloud Run یک پلت فرم کاملاً مدیریت شده است که به شما امکان می دهد کد خود را مستقیماً در بالای زیرساخت مقیاس پذیر Google اجرا کنید. این Codelab نحوه اتصال یک برنامه Angular را در Cloud Run به پایگاه داده Firestore با استفاده از Node.js Admin SDK نشان می دهد.

در این آزمایشگاه شما یاد خواهید گرفت که چگونه:

  • یک پایگاه داده Firestore ایجاد کنید
  • برنامه ای را در Cloud Run مستقر کنید که به پایگاه داده Firestore شما متصل می شود

2. پیش نیازها

  1. اگر قبلاً یک حساب Google ندارید، باید یک حساب Google ایجاد کنید .
    • از حساب شخصی به جای حساب کاری یا مدرسه استفاده کنید. حساب‌های محل کار و مدرسه ممکن است محدودیت‌هایی داشته باشند که شما را از فعال کردن APIهای مورد نیاز برای این آزمایشگاه باز می‌دارد.

3. راه اندازی پروژه

  1. به Google Cloud Console وارد شوید.
  2. صورتحساب را در کنسول Cloud فعال کنید .
    • تکمیل این آزمایشگاه باید کمتر از 1 دلار آمریکا در منابع Cloud هزینه داشته باشد.
    • برای جلوگیری از هزینه های بیشتر، می توانید مراحل انتهای این آزمایشگاه را برای حذف منابع دنبال کنید.
    • کاربران جدید واجد شرایط استفاده از نسخه آزمایشی رایگان 300 دلاری هستند.
  3. یک پروژه جدید ایجاد کنید یا استفاده مجدد از یک پروژه موجود را انتخاب کنید.

4. ویرایشگر پوسته ابری را باز کنید

  1. به Cloud Shell Editor بروید
  2. اگر ترمینال در پایین صفحه ظاهر نشد، آن را باز کنید:
    • روی منوی همبرگر کلیک کنید نماد منوی همبرگر
    • روی ترمینال کلیک کنید
    • روی New Terminal کلیک کنید ترمینال جدید را در Cloud Shell Editor باز کنید
  3. در ترمینال، پروژه خود را با این دستور تنظیم کنید:
    • قالب:
      gcloud config set project [PROJECT_ID]
    • مثال:
      gcloud config set project lab-project-id-example
    • اگر نمی توانید شناسه پروژه خود را به خاطر بسپارید:
      • می توانید تمام شناسه های پروژه خود را با موارد زیر فهرست کنید:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
      شناسه پروژه را در ترمینال Cloud Shell Editor تنظیم کنید
  4. اگر از شما خواسته شد که مجوز دهید، برای ادامه روی تأیید کلیک کنید. برای تأیید Cloud Shell کلیک کنید
  5. شما باید این پیام را ببینید:
    Updated property [core/project].
    
    اگر یک WARNING مشاهده کردید و از Do you want to continue (Y/N)? ، پس احتمالاً شناسه پروژه را اشتباه وارد کرده اید. N را فشار دهید، Enter را فشار دهید و دوباره دستور gcloud config set project اجرا کنید.

5. API ها را فعال کنید

در ترمینال، API ها را فعال کنید:

gcloud services enable \
  firestore.googleapis.com \
  run.googleapis.com \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com

اگر از شما خواسته شد که مجوز دهید، برای ادامه روی تأیید کلیک کنید. برای تأیید Cloud Shell کلیک کنید

تکمیل این دستور ممکن است چند دقیقه طول بکشد، اما در نهایت باید یک پیام موفقیت آمیز مشابه این پیام تولید کند:

Operation "operations/acf.p2-73d90d00-47ee-447a-b600" finished successfully.

6. ایجاد پایگاه داده Firestore

  1. برای ایجاد gcloud firestore databases create databases را اجرا کنید
    gcloud firestore databases create --location=nam5

7. برنامه را آماده کنید

یک برنامه Next.js را آماده کنید که به درخواست های HTTP پاسخ دهد.

  1. برای ایجاد یک پروژه Next.js جدید با نام task-app از دستور زیر استفاده کنید:
    npx --yes @angular/cli@19.2.5 new task-app \
        --minimal \
        --inline-template \
        --inline-style \
        --ssr \
        --server-routing \
        --defaults
  2. دایرکتوری را به task-app تغییر دهید:
    cd task-app
  1. برای تعامل با پایگاه داده Firestore firebase-admin را نصب کنید.
    npm install firebase-admin
  1. فایل server.ts را در Cloud Shell Editor باز کنید:
    cloudshell edit src/server.ts
    اکنون یک فایل باید در قسمت بالای صفحه ظاهر شود. اینجا جایی است که می توانید این فایل server.ts را ویرایش کنید. نشان دهید که کد در قسمت بالای صفحه نمایش داده می شود
  2. محتویات موجود فایل server.ts را حذف کنید.
  3. کد زیر را کپی کرده و در فایل server.ts باز شده قرار دهید:
    import {
      AngularNodeAppEngine,
      createNodeRequestHandler,
      isMainModule,
      writeResponseToNodeResponse,
    } from '@angular/ssr/node';
    import express from 'express';
    import { dirname, resolve } from 'node:path';
    import { fileURLToPath } from 'node:url';
    import { initializeApp, applicationDefault, getApps } from 'firebase-admin/app';
    import { getFirestore } from 'firebase-admin/firestore';

    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
      createdAt: number;
    };

    const credential = applicationDefault();

    // Only initialize app if it does not already exist
    if (getApps().length === 0) {
      initializeApp({ credential });
    }

    const db = getFirestore();
    const tasksRef = db.collection('tasks');

    const serverDistFolder = dirname(fileURLToPath(import.meta.url));
    const browserDistFolder = resolve(serverDistFolder, '../browser');

    const app = express();
    const angularApp = new AngularNodeAppEngine();

    app.use(express.json());

    app.get('/api/tasks', async (req, res) => {
      const snapshot = await tasksRef.orderBy('createdAt', 'desc').limit(100).get();
      const tasks: Task[] = snapshot.docs.map(doc => ({
        id: doc.id,
        title: doc.data()['title'],
        status: doc.data()['status'],
        createdAt: doc.data()['createdAt'],
      }));
      res.send(tasks);
    });

    app.post('/api/tasks', async (req, res) => {
      const newTaskTitle = req.body.title;
      if(!newTaskTitle){
        res.status(400).send("Title is required");
        return;
      }
      await tasksRef.doc().create({
        title: newTaskTitle,
        status: 'IN_PROGRESS',
        createdAt: Date.now(),
      });
      res.sendStatus(200);
    });

    app.put('/api/tasks', async (req, res) => {
      const task: Task = req.body;
      if (!task || !task.id || !task.title || !task.status) {
        res.status(400).send("Invalid task data");
        return;
      }
      await tasksRef.doc(task.id).set(task);
      res.sendStatus(200);
    });

    app.delete('/api/tasks', async (req, res) => {
      const task: Task = req.body;
      if(!task || !task.id){
        res.status(400).send("Task ID is required");
        return;
      }
      await tasksRef.doc(task.id).delete();
      res.sendStatus(200);
    });

    /**
    * Serve static files from /browser
    */
    app.use(
      express.static(browserDistFolder, {
        maxAge: '1y',
        index: false,
        redirect: false,
      }),
    );

    /**
    * Handle all other requests by rendering the Angular application.
    */
    app.use('/**', (req, res, next) => {
      angularApp
        .handle(req)
        .then((response) =>
          response ? writeResponseToNodeResponse(response, res) : next(),
        )
        .catch(next);
    });

    /**
    * Start the server if this module is the main entry point.
    * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
    */
    if (isMainModule(import.meta.url)) {
      const port = process.env['PORT'] || 4000;
      app.listen(port, () => {
        console.log(`Node Express server listening on http://localhost:${port}`);
      });
    }

    /**
    * Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions.
    */
    export const reqHandler = createNodeRequestHandler(app);
  4. فایل angular.json را در Cloud Shell Editor باز کنید:
    cloudshell edit angular.json
    اکنون خط "externalDependencies": ["firebase-admin"] را به فایل angular.json اضافه می کنیم.
  5. محتویات موجود فایل angular.json را حذف کنید.
  6. کد زیر را کپی کرده و در فایل باز شده angular.json قرار دهید:
    {
      "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
      "version": 1,
      "newProjectRoot": "projects",
      "projects": {
        "task-app": {
          "projectType": "application",
          "schematics": {
            "@schematics/angular:component": {
              "inlineTemplate": true,
              "inlineStyle": true,
              "skipTests": true
            },
            "@schematics/angular:class": {
              "skipTests": true
            },
            "@schematics/angular:directive": {
              "skipTests": true
            },
            "@schematics/angular:guard": {
              "skipTests": true
            },
            "@schematics/angular:interceptor": {
              "skipTests": true
            },
            "@schematics/angular:pipe": {
              "skipTests": true
            },
            "@schematics/angular:resolver": {
              "skipTests": true
            },
            "@schematics/angular:service": {
              "skipTests": true
            }
          },
          "root": "",
          "sourceRoot": "src",
          "prefix": "app",
          "architect": {
            "build": {
              "builder": "@angular-devkit/build-angular:application",
              "options": {
                "outputPath": "dist/task-app",
                "index": "src/index.html",
                "browser": "src/main.ts",
                "polyfills": [
                  "zone.js"
                ],
                "tsConfig": "tsconfig.app.json",
                "assets": [
                  {
                    "glob": "**/*",
                    "input": "public"
                  }
                ],
                "styles": [
                  "src/styles.css"
                ],
                "scripts": [],
                "server": "src/main.server.ts",
                "outputMode": "server",
                "ssr": {
                  "entry": "src/server.ts"
                },
                "externalDependencies": ["firebase-admin"]
              },
              "configurations": {
                "production": {
                  "budgets": [
                    {
                      "type": "initial",
                      "maximumWarning": "500kB",
                      "maximumError": "1MB"
                    },
                    {
                      "type": "anyComponentStyle",
                      "maximumWarning": "4kB",
                      "maximumError": "8kB"
                    }
                  ],
                  "outputHashing": "all"
                },
                "development": {
                  "optimization": false,
                  "extractLicenses": false,
                  "sourceMap": true
                }
              },
              "defaultConfiguration": "production"
            },
            "serve": {
              "builder": "@angular-devkit/build-angular:dev-server",
              "configurations": {
                "production": {
                  "buildTarget": "task-app:build:production"
                },
                "development": {
                  "buildTarget": "task-app:build:development"
                }
              },
              "defaultConfiguration": "development"
            },
            "extract-i18n": {
              "builder": "@angular-devkit/build-angular:extract-i18n"
            }
          }
        }
      }
    }

"externalDependencies": ["firebase-admin"]

  1. فایل app.component.ts را در Cloud Shell Editor باز کنید:
    cloudshell edit src/app/app.component.ts
    اکنون یک فایل موجود باید در قسمت بالای صفحه ظاهر شود. اینجاست که می توانید این فایل app.component.ts را ویرایش کنید. نشان دهید که کد در قسمت بالای صفحه نمایش داده می شود
  2. محتویات موجود فایل app.component.ts را حذف کنید.
  3. کد زیر را کپی کرده و در فایل app.component.ts باز شده قرار دهید:
    import { afterNextRender, Component, signal } from '@angular/core';
    import { FormsModule } from '@angular/forms';

    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
      createdAt: number;
    };

    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [FormsModule],
      template: `
        <section>
          <input
            type="text"
            placeholder="New Task Title"
            [(ngModel)]="newTaskTitle"
            class="text-black border-2 p-2 m-2 rounded"
          />
          <button (click)="addTask()">Add new task</button>
          <table>
            <tbody>
              @for (task of tasks(); track task) {
                @let isComplete = task.status === 'COMPLETE';
                <tr>
                  <td>
                    <input
                      (click)="updateTask(task, { status: isComplete ? 'IN_PROGRESS' : 'COMPLETE' })"
                      type="checkbox"
                      [checked]="isComplete"
                    />
                  </td>
                  <td>{{ task.title }}</td>
                  <td>{{ task.status }}</td>
                  <td>
                    <button (click)="deleteTask(task)">Delete</button>
                  </td>
                </tr>
              }
            </tbody>
          </table>
        </section>
      `,
      styles: '',
    })
    export class AppComponent {
      newTaskTitle = '';
      tasks = signal<Task[]>([]);

      constructor() {
        afterNextRender({
          earlyRead: () => this.getTasks()
        });
      }

      async getTasks() {
        const response = await fetch(`/api/tasks`);
        const tasks = await response.json();
        this.tasks.set(tasks);
      }

      async addTask() {
        await fetch(`/api/tasks`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            title: this.newTaskTitle,
            status: 'IN_PROGRESS',
            createdAt: Date.now(),
          }),
        });
        this.newTaskTitle = '';
        await this.getTasks();
      }

      async updateTask(task: Task, newTaskValues: Partial<Task>) {
        await fetch(`/api/tasks`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ ...task, ...newTaskValues }),
        });
        await this.getTasks();
      }

      async deleteTask(task: any) {
        await fetch('/api/tasks', {
          method: 'DELETE',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(task),
        });
        await this.getTasks();
      }
    }

برنامه اکنون آماده استقرار است.

8. برنامه را در Cloud Run مستقر کنید

  1. دستور زیر را برای استقرار برنامه خود در Cloud Run اجرا کنید:
    gcloud run deploy helloworld \
      --region=us-central1 \
      --source=.
  2. اگر از شما خواسته شد، Y و Enter را فشار دهید تا تأیید کنید که می‌خواهید ادامه دهید:
    Do you want to continue (Y/n)? Y
    

پس از چند دقیقه، برنامه باید یک URL برای بازدید شما ارائه دهد.

برای مشاهده عملکرد برنامه خود به URL بروید. هر بار که از URL بازدید می کنید یا صفحه را بازخوانی می کنید، برنامه وظیفه را خواهید دید.

9. تبریک میگم

در این آزمایشگاه شما یاد گرفتید که چگونه کارهای زیر را انجام دهید:

  • یک Cloud SQL برای نمونه PostgreSQL ایجاد کنید
  • برنامه ای را در Cloud Run مستقر کنید که به پایگاه داده Cloud SQL شما متصل می شود

تمیز کردن

Cloud SQL لایه رایگان ندارد و در صورت ادامه استفاده از آن هزینه ای از شما دریافت می کند. برای جلوگیری از تحمیل هزینه های اضافی، می توانید پروژه Cloud خود را حذف کنید.

در حالی که Cloud Run زمانی که از سرویس استفاده نمی‌شود شارژ نمی‌شود، ممکن است همچنان برای ذخیره تصویر ظرف در Artifact Registry هزینه دریافت کنید. با حذف پروژه Cloud، صورت‌حساب تمام منابع مورد استفاده در آن پروژه متوقف می‌شود.

در صورت تمایل پروژه را حذف کنید:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

همچنین ممکن است بخواهید منابع غیر ضروری را از دیسک پوسته ابری خود حذف کنید. شما می توانید:

  1. دایرکتوری پروژه codelab را حذف کنید:
    rm -rf ~/task-app
  2. هشدار! این اقدام بعدی قابل واگرد نیست! اگر می‌خواهید همه چیز را در Cloud Shell خود حذف کنید تا فضا آزاد شود، می‌توانید کل فهرست اصلی خود را حذف کنید . مراقب باشید هر چیزی که می خواهید نگه دارید در جای دیگری ذخیره شود.
    sudo rm -rf $HOME