نشر تطبيق Next.js متكامل على Cloud Run باستخدام Firestore باستخدام حزمة تطوير البرامج (SDK) المخصّصة للمشرفين في Node.js

1. نظرة عامة

Cloud Run هي منصة مُدارة بالكامل تتيح لك تشغيل الرموز البرمجية مباشرةً على بنية Google الأساسية القابلة للتوسّع. سيوضّح هذا الدرس التطبيقي حول الترميز كيفية ربط تطبيق Next.js على Cloud Run بقاعدة بيانات Firestore باستخدام حزمة تطوير البرامج (SDK) الخاصة بمشرف Node.js.

في هذه الميزة الاختبارية، ستتعرّف على كيفية:

  • إنشاء قاعدة بيانات Firestore
  • نشر تطبيق على Cloud Run يتصل بقاعدة بيانات Firestore

2. المتطلبات الأساسية

  1. إذا لم يكن لديك حساب على Google، عليك إنشاء حساب على Google.
    • استخدام حساب شخصي بدلاً من حساب تديره المؤسسة التعليمية أو حساب تابع للعمل. قد تتضمّن حسابات العمل والحسابات المُدارة من المؤسسات التعليمية قيودًا تمنعك من تفعيل واجهات برمجة التطبيقات اللازمة لهذا الدرس التطبيقي.

3- إعداد المشروع

  1. سجِّل الدخول إلى Google Cloud Console.
  2. فعِّل الفوترة في Cloud Console.
    • يجب أن تكلّف إكمال هذا المختبر أقل من دولار أمريكي واحد من موارد السحابة الإلكترونية.
    • يمكنك اتّباع الخطوات في نهاية هذا المختبر لحذف الموارد وتجنُّب المزيد من الرسوم.
    • يمكن للمستخدمين الجدد الاستفادة من فترة تجريبية مجانية بقيمة 300 دولار أمريكي.
  3. أنشِئ مشروعًا جديدًا أو اختَر إعادة استخدام مشروع حالي.

4. فتح "محرّر Cloud Shell"

  1. الانتقال إلى محرّر Cloud Shell
  2. إذا لم تظهر المحطة الطرفية في أسفل الشاشة، افتحها باتّباع الخطوات التالية:
    • انقر على قائمة الهامبرغر رمز قائمة الخطوط الثلاثة
    • انقر على Terminal.
    • انقر على نافذة طرفية جديدةفتح نافذة طرفية جديدة في "محرِّر Cloud Shell".
  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- تفعيل واجهات برمجة التطبيقات

في الوحدة الطرفية، فعِّل واجهات برمجة التطبيقات:

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 لإنشاء قاعدة بيانات Firestore.
    gcloud firestore databases create --location=nam5
    

7. تجهيز التطبيق

إعداد تطبيق Next.js يستجيب لطلبات HTTP

  1. لإنشاء مشروع Next.js جديد باسم task-app، استخدِم الأمر:
    npx --yes create-next-app@15 task-app \
      --ts \
      --eslint \
      --tailwind \
      --no-src-dir \
      --turbopack \
      --app \
      --no-import-alias
    
  2. غيِّر الدليل إلى task-app:
    cd task-app
    
  1. ثبِّت firebase-admin للتفاعل مع قاعدة بيانات Firestore.
    npm install firebase-admin
    
  1. افتح الملف actions.ts في "محرِّر Cloud Shell":
    cloudshell edit app/actions.ts
    
    من المفترض أن يظهر ملف فارغ الآن في الجزء العلوي من الشاشة. هذا هو المكان الذي يمكنك فيه تعديل ملف actions.ts هذا. عرض أنّ الرمز يظهر في القسم العلوي من الشاشة
  2. انسخ الرمز التالي والصِقه في ملف actions.ts الذي تم فتحه:
    'use server'
    import { initializeApp, applicationDefault, getApps } from 'firebase-admin/app';
    import { getFirestore } from 'firebase-admin/firestore';
    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');
    
    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
      createdAt: number;
    };
    
    // CREATE
    export async function addNewTaskToDatabase(newTask: string) {
      await tasksRef.doc().create({
        title: newTask,
        status: 'IN_PROGRESS',
        createdAt: Date.now(),
      });
      return;
    }
    
    // READ
    export async function getTasksFromDatabase() {
      const snapshot = await tasksRef.orderBy('createdAt', 'desc').limit(100).get();
      const tasks = await snapshot.docs.map(doc => ({
        id: doc.id,
        title: doc.data().title,
        status: doc.data().status,
        createdAt: doc.data().createdAt,
      }));
      return tasks;
    }
    
    // UPDATE
    export async function updateTaskInDatabase(task: Task) {
      await tasksRef.doc(task.id).set(task);
      return;
    }
    
    // DELETE
    export async function deleteTaskFromDatabase(taskId: string) {
      await tasksRef.doc(taskId).delete();
      return;
    }
    
  1. افتح الملف page.tsx في "محرِّر Cloud Shell":
    cloudshell edit app/page.tsx
    
    من المفترض أن يظهر ملف حالي في الجزء العلوي من الشاشة. هذا هو المكان الذي يمكنك فيه تعديل ملف page.tsx هذا. عرض أنّ الرمز يظهر في القسم العلوي من الشاشة
  2. احذف المحتوى الحالي لملف page.tsx.
  3. انسخ الرمز التالي والصِقه في ملف page.tsx الذي تم فتحه:
    'use client'
    import React, { useEffect, useState } from "react";
    import { addNewTaskToDatabase, getTasksFromDatabase, deleteTaskFromDatabase, updateTaskInDatabase } from "./actions";
    
    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
      createdAt: number;
    };
    
    export default function Home() {
      const [newTaskTitle, setNewTaskTitle] = useState('');
      const [tasks, setTasks] = useState<Task[]>([]);
    
      async function getTasks() {
        const updatedListOfTasks = await getTasksFromDatabase();
        setTasks(updatedListOfTasks);
      }
    
      useEffect(() => {
        getTasks();
      }, []);
    
      async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
        e.preventDefault();
        await addNewTaskToDatabase(newTaskTitle);
        await getTasks();
        setNewTaskTitle('');
      };
    
      async function updateTask(task: Task, newTaskValues: Partial<Task>) {
        await updateTaskInDatabase({ ...task, ...newTaskValues });
        await getTasks();
      }
    
      async function deleteTask(taskId: string) {
        await deleteTaskFromDatabase(taskId);
        await getTasks();
      }
    
      return (
        <main className="p-4">
          <h2 className="text-2xl font-bold mb-4">To Do List</h2>
          <div className="flex mb-4">
            <form onSubmit={handleSubmit} className="flex mb-8">
              <input
                type="text"
                placeholder="New Task Title"
                value={newTaskTitle}
                onChange={(e) => setNewTaskTitle(e.target.value)}
                className="flex-grow border border-gray-400 rounded px-3 py-2 mr-2 bg-inherit"
              />
              <button
                type="submit"
                className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-nowrap"
              >
                Add New Task
              </button>
            </form>
          </div>
          <table className="w-full">
            <tbody>
              {tasks.map(function (task) {
                const isComplete = task.status === 'COMPLETE';
                return (
                  <tr key={task.id} className="border-b border-gray-200">
                    <td className="py-2 px-4">
                      <input
                        type="checkbox"
                        checked={isComplete}
                        onChange={() => updateTask(task, { status: isComplete ? 'IN_PROGRESS' : 'COMPLETE' })}
                        className="transition-transform duration-300 ease-in-out transform scale-100 checked:scale-125 checked:bg-green-500"
                      />
                    </td>
                    <td className="py-2 px-4">
                      <span
                        className={`transition-all duration-300 ease-in-out ${isComplete ? 'line-through text-gray-400 opacity-50' : 'opacity-100'}`}
                      >
                        {task.title}
                      </span>
                    </td>
                    <td className="py-2 px-4">
                      <button
                        onClick={() => deleteTask(task.id)}
                        className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded float-right"
                      >
                        Delete
                      </button>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </main>
      );
    }
    

أصبح التطبيق الآن جاهزًا للنشر.

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 for PostgreSQL
  • نشر تطبيق على Cloud Run يتصل بقاعدة بيانات Cloud SQL

تَنظيم

لا تتوفّر طبقة مجانية في Cloud SQL، وسيتم تحصيل رسوم منك إذا واصلت استخدامها. يمكنك حذف مشروعك على السحابة الإلكترونية لتجنُّب تحمّل رسوم إضافية.

على الرغم من أنّ Cloud Run لا تفرض رسومًا عندما لا تكون الخدمة قيد الاستخدام، قد يتم تحصيل رسوم منك مقابل تخزين صورة الحاوية في Artifact Registry. يؤدي حذف مشروعك على السحابة الإلكترونية إلى إيقاف الفوترة لجميع الموارد المستخدَمة في هذا المشروع.

إذا أردت، يمكنك حذف المشروع باتّباع الخطوات التالية:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

يمكنك أيضًا حذف الموارد غير الضرورية من قرص cloudshell. يمكنك إجراء ما يلي:

  1. احذف دليل مشروع الدرس البرمجي:
    rm -rf ~/task-app
    
  2. تحذير! لا يمكن التراجع عن الإجراء التالي. إذا أردت حذف كل المحتوى على Cloud Shell لإخلاء بعض المساحة، يمكنك حذف دليل منزلك بأكمله. يجب التأكّد من حفظ كل ما تريد الاحتفاظ به في مكان آخر.
    sudo rm -rf $HOME
    

مواصلة التعلّم