המערכת FARM היא מערכת פולחן פייסבוק מודרנית שמשלבת שלושה טכנולוגיות חזקות: FastAPI, React ו MongoDB. פתרון מערך שלם זה מספק למפתחים עם ערכת כלים חזקה כדי לבנות אפליקציות רשת מקוונת סקאלבלים, יעילים, ובעלי ביצועים גבוהים.

במאמר זה, אני אתן לכם הצגה לכל אחד מהטכנולוגיות המפתחיות, ואחר כך נבנה פרוייקט בעזרת המערכת FARM ו-Docker, כך שתוכלו לראות איך הכל עובד ביחד.

המאמר מבוסס על קורס שאני יצרתי בקנה מידה של הערוץ YouTube של freeCodeCamp.org. תצפו בו כאן:

הקדמה למערכת FARM

האות FARM במערכת FARM מתייחסת ל:

  • F: FastAPI (חלק האחור)

  • R: React (חלק הקדמה)

  • M: MongoDB (מאגר נתונים)

המערכת FARM מעוצבת כדי להשתמש בחזקות של כל רכיב, מאפשר למפתחים ליצור אפליקציות עם תכונות עשירות וחוויה פיתוח נוחה.

רכיבים של המערכת FARM

  1. FastAPI: FastAPI היא רשת פייסבוק מודרנית וגבוה בביצועים עבור בניית API. היא מעוצבת כדי להיות קל לשימוש, מהירה בכתיבה, ומוכנה לאחסן בסביבות הפרודוקציה. FastAPI מבוססת על סטארלט
  2. React: React היא ספריית JavaScript פופולרית לבניית ממשקי משתמש. התפתחה ומתוחזקת על ידי פייסבוק, ריאקט מאפשרת למפתחים ליצור רכיבי ממשק משתמש ניתנים לשימוש מחדש שמתעדכנים ומתציירים ביעילות עם שינויים בנתונים. הארכיטקטורה המבוססת רכיבים וה-DOM הווירטואלי שלה מפכחים אותה לבחירה מצוינת לבניית אפליקציות חזית דינמיות ותגובתיות.

  3. MongoDB: MongoDB היא מסד נתונים NoSQL מבוסס מסמכים. היא אוחסנת נתונים במסמכים גמישים בפורמט דומה ל-JSON, המשמעות היא ששדות יכולים להשתנות ממסמך למסמך ובניית המבנה יכולה להשתנות לאורך הזמן. הגמישות הזו הופכת את MongoDB לבחירה אידיאלית ליישומים שצריכים להתפתח במהירות ולטפל בסוגי נתונים שונים.

יתרונות של שימוש בערימת FARM

  1. ביצועים גבוהים: FastAPI היא אחת מהפריימוורקים בפייתון המהירים ביותר, בעוד ש-React's virtual DOM מבטיח עדכוני UI יעילים. מודל המסמכים של MongoDB מאפשר קריאות וכתיבות מהירות.

  2. קידמה: כל הרכיבים של ערימת FARM מיועדים להתרחבות. FastAPI יכולה לטפל בבקשות סותמות ביעילות, אפליקציות React יכולות לנהל UI מורכבות, ו-MongoDB יכולה להפיץ נתונים על מספר שרתים.

  3. קהילה ואקוסיסטם: כל שלושת הטכנולוגיות כוללות קהילות גדולות ופעילות ואקוסיסטמות בעלות ספריות וכלים עשירים.

  4. גמישות: ערימת ה-FARM גמישה מספיק כדי לאכול מגוון סוגי אפליקציות אינטרנט, מאפליקציות CRUD פשוטות עד מערכות מורכבות ומוצקות בנתונים.

על ידי שילוב טכנולוגיות אלה, ערימת ה-FARM מספקת פתרון מקיף לבניית אפליקציות אינטרנט מודרניות. זה מאפשר למפתחים ליצור מערכות צד שרת מהירות וקלות משקל עם FastAPI, חזיתות רגישות ורספונסיביות עם React, ואחסון נתונים גמיש ויעיל עם MongoDB. ערימה זו מתאימה במיוחד לאפליקציות שדורשות עדכונים בזמן אמת, מודלים מורכבים של נתונים וביצועים גבוהים.

סקירת הפרויקט: אפליקציית משימות

בקורס הווידאו, אני מכסה עוד יותר על כל טכנולוגיה יחידה בערימת ה-FARM. אבל במאמר זה, נדבר על פרויקט כדי לשלב הכל יחד.

ניצור אפליקציית משימות כדי לעזור לנו להבין את ערימת ה-FARM. לפני שנתחיל ליצור את האפליקציה, בואו נדבר עוד על התכונות וארכיטקטורת התוכנה.

תכונות של אפליקציית המשימות

אפליקציית משימות בערימת ה-FARM שלנו תכלול את התכונות הבאות:

  1. רשימות משימות מרובות:

    • משתמשים יכולים ליצור, להציג, לעדכן ולמחוק רשימות משימות מרובות.

    • כל רשימה מכילה שם ומכילה משימות מרובות.

  2. פריטי משימות:

    • בתוך כל רשימה, משתמשים יכולים להוסיף, להציג, לעדכן ולמחוק פריטי משימות.

    • כל פריט מכיל תווית, מצב מסומן/לא מסומן, ושייך לרשימה ספציפית.

  3. עדכונים בזמן אמת:

    • ממשק המשתמש מתעדכן בזמן אמת כאשר נעשים שינויים ברשימות או בפריטים.
  4. עיצוב רספונסיבי:

    • היישום יהיה רספונסיבי ויעבוד בצורה טובה על המחשב הנייח והנייד.

ארכיטקטורת מערכת

יישום המשימות שלנו יעקוב אחר ארכיטקטורת מחסנית טיפולית רגילה:

  1. קדמי (React):

    • מספק את ממשק המשתמש לפעולה עם רשימות המשימות והפריטים.

    • תקשורת עם הצד השרת באמצעות שיחות RESTful API.

  2. אחורי (FastAPI):

    • מתמודד עם בקשות ה- API מהקדמי.

    • מיישם לוגיקת עסקים לניהול רשימות משימות ופריטים.

    • מתקשר עם מסד הנתונים MongoDB לשמירת הנתונים.

  3. מסד נתונים (MongoDB):

    • מאחסן רשימות משימות ופריטים.

    • מספק שאילתות ועדכונים יעילים של נתוני המשימות.

  4. Docker:

    • מקפיא כל רכיב (קדמי, אחורי, מסד נתונים) לפיתוח ופרסום קלים.

עיצוב מודל נתונים

מודל הנתונים שלנו ב-MongoDB יכיל שתי מבנים ראשיים:

  1. רשימת משימות:
   {
     "_id": ObjectId,
     "name": String,
     "items": [
       {
         "id": String,
         "label": String,
         "checked": Boolean
       }
     ]
   }
  1. סיכום רשימה (להצגה ברשימה של כל רשימות המשימות):
   {
     "_id": ObjectId,
     "name": String,
     "item_count": Integer
   }

עיצוב נקודת קצה API

ה-Backend שלנו ב-FastAPI יחשוף את נקודות הקצה ה-RESTful הבאות:

  1. רשימות משימות:

    • GET /api/lists: אחזור כל רשימות משימות (תצוגת סיכום)

    • POST /api/lists: יצירת רשימת משימות חדשה

    • GET /api/lists/{list_id}: אחזור רשימת משימה ספציפית עם כל הפריטים שלה

    • DELETE /api/lists/{list_id}: מחיקת רשימת משימה ספציפית

  2. פריטי משימות:

    • POST /api/lists/{list_id}/items: הוסף פריט חדש לרשימה ספציפית

    • PATCH /api/lists/{list_id}/checked_state: עדכן מצב המסמנה של פריט

    • DELETE /api/lists/{list_id}/items/{item_id}: מחק פריט ספציפי מהרשימה

הפרוייקט הזה יספק בסיס מוצק לפיתוח שקעת FARM וקיבולת Docker, ותוכל להרחיב עליו בהתאמה ליישומים מורכבים יותר בעתיד.

אז בואו נתחיל את הפרוייקט.

הדרכה לפרוייקט

הקמת הפרוייקט ופיתוח השרת האחורי

שלב 1: הקמת מבנה הפרוייקט

צור ספרייה חדשה עבור הפרוייקט שלך:

   mkdir farm-stack-todo
   cd farm-stack-todo

צור תת־ספריות עבור השרת האחורי והקדמה:

   mkdir backend frontend

שלב 2: הקמת סביבת השרת האחורי

תכנס לתוך הספרייה של השרת האחורי:

   cd backend

צור סביבת וירטואל

   python -m venv venv
   source venv/bin/activate  # On Windows, use: venv\Scripts\activate

ויצירה של הקבצים הבאים בתוך התוך האופקן האחורי:

    • דוקרפיל

      • pyproject.toml

בתוכנה שלך, התקנו את החבילות הדרושות:

pip install "fastapi[all]" "motor[srv]" beanie aiostream

הוסף את הערך הבא לדוקרפיל:

pip freeze > requirements.txt

אחרי יצירת הקבצים requirements.txt (באמצעות pip-compile או באופן ידני), ניתן להתקין את התלויות בעזרת:

   pip install -r requirements.txt

הוסף את התוכן הבא לדוקרפיל:

   FROM python:3

   WORKDIR /usr/src/app
   COPY requirements.txt ./

   RUN pip install --no-cache-dir --upgrade -r ./requirements.txt

   EXPOSE 3001

   CMD [ "python", "./src/server.py" ]

הוסף את התוכן הבא ל pyproject.toml:

   [tool.pytest.ini_options]
   pythonpath = "src"

שלב 4: הגדרת המבנה של האחורי

יצירה של תיבת src בתוך האופקן האחורי:

   mkdir src

יצירה של הקבצים הבאים בתוך תיבת src:

שלב 5: יישמה של השכלת גישה למידע (DAL)

פתיח את src/dal.py והוסף את התוכן הבא:

from bson import ObjectId
from motor.motor_asyncio import AsyncIOMotorCollection
from pymongo import ReturnDocument

from pydantic import BaseModel

from uuid import uuid4

class ListSummary(BaseModel):
  id: str
  name: str
  item_count: int

  @staticmethod
  def from_doc(doc) -> "ListSummary":
      return ListSummary(
          id=str(doc["_id"]),
          name=doc["name"],
          item_count=doc["item_count"],
      )

class ToDoListItem(BaseModel):
  id: str
  label: str
  checked: bool

  @staticmethod
  def from_doc(item) -> "ToDoListItem":
      return ToDoListItem(
          id=item["id"],
          label=item["label"],
          checked=item["checked"],
      )

class ToDoList(BaseModel):
  id: str
  name: str
  items: list[ToDoListItem]

  @staticmethod
  def from_doc(doc) -> "ToDoList":
      return ToDoList(
          id=str(doc["_id"]),
          name=doc["name"],
          items=[ToDoListItem.from_doc(item) for item in doc["items"]],
      )

class ToDoDAL:
  def __init__(self, todo_collection: AsyncIOMotorCollection):
      self._todo_collection = todo_collection

  async def list_todo_lists(self, session=None):
      async for doc in self._todo_collection.find(
          {},
          projection={
              "name": 1,
              "item_count": {"$size": "$items"},
          },
          sort={"name": 1},
          session=session,
      ):
          yield ListSummary.from_doc(doc)

  async def create_todo_list(self, name: str, session=None) -> str:
      response = await self._todo_collection.insert_one(
          {"name": name, "items": []},
          session=session,
      )
      return str(response.inserted_id)

  async def get_todo_list(self, id: str | ObjectId, session=None) -> ToDoList:
      doc = await self._todo_collection.find_one(
          {"_id": ObjectId(id)},
          session=session,
      )
      return ToDoList.from_doc(doc)

  async def delete_todo_list(self, id: str | ObjectId, session=None) -> bool:
      response = await self._todo_collection.delete_one(
          {"_id": ObjectId(id)},
          session=session,
      )
      return response.deleted_count == 1

  async def create_item(
      self,
      id: str | ObjectId,
      label: str,
      session=None,
  ) -> ToDoList | None:
      result = await self._todo_collection.find_one_and_update(
          {"_id": ObjectId(id)},
          {
              "$push": {
                  "items": {
                      "id": uuid4().hex,
                      "label": label,
                      "checked": False,
                  }
              }
          },
          session=session,
          return_document=ReturnDocument.AFTER,
      )
      if result:
          return ToDoList.from_doc(result)

  async def set_checked_state(
      self,
      doc_id: str | ObjectId,
      item_id: str,
      checked_state: bool,
      session=None,
  ) -> ToDoList | None:
      result = await self._todo_collection.find_one_and_update(
          {"_id": ObjectId(doc_id), "items.id": item_id},
          {"$set": {"items.$.checked": checked_state}},
          session=session,
          return_document=ReturnDocument.AFTER,
      )
      if result:
          return ToDoList.from_doc(result)

  async def delete_item(
      self,
      doc_id: str | ObjectId,
      item_id: str,
      session=None,
  ) -> ToDoList | None:
      result = await self._todo_collection.find_one_and_update(
          {"_id": ObjectId(doc_id)},
          {"$pull": {"items": {"id": item_id}}},
          session=session,
          return_document=ReturnDocument.AFTER,
      )
      if result:
          return ToDoList.from_doc(result)

זה מסתיים חלק 1 מההדרכה, בו אנחנו מציבים את מבנה הפרוייקט ומיישמים את ממשק הגישה למידע עבור היישומנו של FARM stack todo. בחלק הבא, אנחנו יישמים את השרת FastAPI ויוצרים את נקודות הAPI.

יישום שרת FastAPI

שלב 6: יישום שרת FastAPI

פתיח את src/server.py והוסף את התוכן הבא:

from contextlib import asynccontextmanager
from datetime import datetime
import os
import sys

from bson import ObjectId
from fastapi import FastAPI, status
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseModel
import uvicorn

from dal import ToDoDAL, ListSummary, ToDoList

COLLECTION_NAME = "todo_lists"
MONGODB_URI = os.environ["MONGODB_URI"]
DEBUG = os.environ.get("DEBUG", "").strip().lower() in {"1", "true", "on", "yes"}


@asynccontextmanager
async def lifespan(app: FastAPI):
    # התחלה:
    client = AsyncIOMotorClient(MONGODB_URI)
    database = client.get_default_database()

    # וודא שהמסדר הבנוי הזה זמין:
    pong = await database.command("ping")
    if int(pong["ok"]) != 1:
        raise Exception("Cluster connection is not okay!")

    todo_lists = database.get_collection(COLLECTION_NAME)
    app.todo_dal = ToDoDAL(todo_lists)

    # שיחזור חזרה אל יישום FastAPI:
    yield

    # כיבוי:
    client.close()


app = FastAPI(lifespan=lifespan, debug=DEBUG)


@app.get("/api/lists")
async def get_all_lists() -> list[ListSummary]:
    return [i async for i in app.todo_dal.list_todo_lists()]


class NewList(BaseModel):
    name: str


class NewListResponse(BaseModel):
    id: str
    name: str


@app.post("/api/lists", status_code=status.HTTP_201_CREATED)
async def create_todo_list(new_list: NewList) -> NewListResponse:
    return NewListResponse(
        id=await app.todo_dal.create_todo_list(new_list.name),
        name=new_list.name,
    )


@app.get("/api/lists/{list_id}")
async def get_list(list_id: str) -> ToDoList:
    """Get a single to-do list"""
    return await app.todo_dal.get_todo_list(list_id)


@app.delete("/api/lists/{list_id}")
async def delete_list(list_id: str) -> bool:
    return await app.todo_dal.delete_todo_list(list_id)


class NewItem(BaseModel):
    label: str


class NewItemResponse(BaseModel):
    id: str
    label: str


@app.post(
    "/api/lists/{list_id}/items/",
    status_code=status.HTTP_201_CREATED,
)
async def create_item(list_id: str, new_item: NewItem) -> ToDoList:
    return await app.todo_dal.create_item(list_id, new_item.label)


@app.delete("/api/lists/{list_id}/items/{item_id}")
async def delete_item(list_id: str, item_id: str) -> ToDoList:
    return await app.todo_dal.delete_item(list_id, item_id)


class ToDoItemUpdate(BaseModel):
    item_id: str
    checked_state: bool


@app.patch("/api/lists/{list_id}/checked_state")
async def set_checked_state(list_id: str, update: ToDoItemUpdate) -> ToDoList:
    return await app.todo_dal.set_checked_state(
        list_id, update.item_id, update.checked_state
    )


class DummyResponse(BaseModel):
    id: str
    when: datetime


@app.get("/api/dummy")
async def get_dummy() -> DummyResponse:
    return DummyResponse(
        id=str(ObjectId()),
        when=datetime.now(),
    )


def main(argv=sys.argv[1:]):
    try:
        uvicorn.run("server:app", host="0.0.0.0", port=3001, reload=DEBUG)
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()

היישום זה מציב שרת FastAPI עם מרכיב CORS, מתחבר אל מונגוDB, ומגדיר נקודות הAPI עבור היישומנו של todo.

שלב 7: הגדרת משתנים סביבתיים

יציר קובץ .env בתוך המחשבה העיקרית עם התוכן הבא. ובו בודק והוסף את שמו המסדר (״todo״) בסוף ״.mongodb.net/״.

MONGODB_URI='mongodb+srv://beau:codecamp@cluster0.ji7hu.mongodb.net/todo?retryWrites=true&w=majority&appName=Cluster0'

שלב 8: יצירת קובץ docker-compose

בתוך מחשבה העיקרית שלך (farm-stack-todo), יציר קובץ בשם compose.yml עם התוכן הבא:

name: todo-app
services:
  nginx:
    image: nginx:1.17
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
    ports:
      - 8000:80
    depends_on:
      - backend
      - frontend
  frontend:
    image: "node:22"
    user: "node"
    working_dir: /home/node/app
    environment:
      - NODE_ENV=development
      - WDS_SOCKET_PORT=0
    volumes:
      - ./frontend/:/home/node/app
    expose:
      - "3000"
    ports:
      - "3000:3000"
    command: "npm start"
  backend:
    image: todo-app/backend
    build: ./backend
    volumes:
      - ./backend/:/usr/src/app
    expose:
      - "3001"
    ports:
      - "8001:3001"
    command: "python src/server.py"
    environment:
      - DEBUG=true
    env_file:
      - path: ./.env
        required: true

שלב 9: הגדרת ההגדרות של Nginx

יציר תוך מחשבה העיקרית שלך תיקייה בשם nginx.

mkdir nginx

יצירה של קובץ בשם nginx.conf בתוך המידע של nginx עם התוכן הבא:

server {
    listen 80;
    server_name farm_intro;

    location / {
        proxy_pass http://frontend:3000;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    location /api {
        proxy_pass http://backend:3001/api;
    }
}

זה מסתיים חלק 2 מההדרכה, בו יישמנו את השרת FastAPI, הגדרנו משתני סביבה, יצרנו קובץ docker-compose והגדרנו Nginx. בחלק הבא נרכש את הקדמה של האפליקציה של הערך הזה.

הגדרת הקדמה של האפליקציה של React

שלב 10: יצירת האפליקציה של React

עבור תחום הקדמה:

cd ../frontend

יצירת אפליקציה חדשה של React בעזרת Create React App:

npx create-react-app .

התקנת תלויות נוספות:

   npm install axios react-icons

שלב 11: הגדרת הרכב העיקרי של App

החלף את התוכן של src/App.js בעזרת התוכן הבא:

import { useEffect, useState } from "react";
import axios from "axios";
import "./App.css";
import ListToDoLists from "./ListTodoLists";
import ToDoList from "./ToDoList";

function App() {
  const [listSummaries, setListSummaries] = useState(null);
  const [selectedItem, setSelectedItem] = useState(null);

  useEffect(() => {
    reloadData().catch(console.error);
  }, []);

  async function reloadData() {
    const response = await axios.get("/api/lists");
    const data = await response.data;
    setListSummaries(data);
  }

  function handleNewToDoList(newName) {
    const updateData = async () => {
      const newListData = {
        name: newName,
      };

      await axios.post(`/api/lists`, newListData);
      reloadData().catch(console.error);
    };
    updateData();
  }

  function handleDeleteToDoList(id) {
    const updateData = async () => {
      await axios.delete(`/api/lists/${id}`);
      reloadData().catch(console.error);
    };
    updateData();
  }

  function handleSelectList(id) {
    console.log("Selecting item", id);
    setSelectedItem(id);
  }

  function backToList() {
    setSelectedItem(null);
    reloadData().catch(console.error);
  }

  if (selectedItem === null) {
    return (
      <div className="App">
        <ListToDoLists
          listSummaries={listSummaries}
          handleSelectList={handleSelectList}
          handleNewToDoList={handleNewToDoList}
          handleDeleteToDoList={handleDeleteToDoList}
        />
      </div>
    );
  } else {
    return (
      <div className="App">
        <ToDoList listId={selectedItem} handleBackButton={backToList} />
      </div>
    );
  }
}

export default App;

שלב 12: יצירת הרכב ListTodoLists

יצירת קובץ חדש בשם src/ListTodoLists.js עם התוכן הבא:

import "./ListTodoLists.css";
import { useRef } from "react";
import { BiSolidTrash } from "react-icons/bi";

function ListToDoLists({
  listSummaries,
  handleSelectList,
  handleNewToDoList,
  handleDeleteToDoList,
}) {
  const labelRef = useRef();

  if (listSummaries === null) {
    return <div className="ListToDoLists loading">Loading to-do lists ...</div>;
  } else if (listSummaries.length === 0) {
    return (
      <div className="ListToDoLists">
        <div className="box">
        <label>
          New To-Do List:&nbsp;
          <input id={labelRef} type="text" />
        </label>
        <button
          onClick={() =>
            handleNewToDoList(document.getElementById(labelRef).value)
          }
        >
          New
        </button>
        </div>
        <p>There are no to-do lists!</p>
      </div>
    );
  }
  return (
    <div className="ListToDoLists">
      <h1>All To-Do Lists</h1>
      <div className="box">
        <label>
          New To-Do List:&nbsp;
          <input id={labelRef} type="text" />
        </label>
        <button
          onClick={() =>
            handleNewToDoList(document.getElementById(labelRef).value)
          }
        >
          New
        </button>
      </div>
      {listSummaries.map((summary) => {
        return (
          <div
            key={summary.id}
            className="summary"
            onClick={() => handleSelectList(summary.id)}
          >
            <span className="name">{summary.name} </span>
            <span className="count">({summary.item_count} items)</span>
            <span className="flex"></span>
            <span
              className="trash"
              onClick={(evt) => {
                evt.stopPropagation();
                handleDeleteToDoList(summary.id);
              }}
            >
              <BiSolidTrash />
            </span>
          </div>
        );
      })}
    </div>
  );
}

export default ListToDoLists;

יצירת קובץ חדש בשם src/ListTodoLists.css עם התוכן הבא:

.ListToDoLists .summary {
    border: 1px solid lightgray;
    padding: 1em;
    margin: 1em;
    cursor: pointer;
    display: flex;
}

.ListToDoLists .count {
    padding-left: 1ex;
    color: blueviolet;
    font-size: 92%;
}

שלב 13: יצירת הרכב ToDoList

יצירת קובץ חדש בשם src/ToDoList.js עם התוכן הבא:

import "./ToDoList.css";
import { useEffect, useState, useRef } from "react";
import axios from "axios";
import { BiSolidTrash } from "react-icons/bi";

function ToDoList({ listId, handleBackButton }) {
  let labelRef = useRef();
  const [listData, setListData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await axios.get(`/api/lists/${listId}`);
      const newData = await response.data;
      setListData(newData);
    };
    fetchData();
  }, [listId]);

  function handleCreateItem(label) {
    const updateData = async () => {
      const response = await axios.post(`/api/lists/${listData.id}/items/`, {
        label: label,
      });
      setListData(await response.data);
    };
    updateData();
  }

  function handleDeleteItem(id) {
    const updateData = async () => {
      const response = await axios.delete(
        `/api/lists/${listData.id}/items/${id}`
      );
      setListData(await response.data);
    };
    updateData();
  }

  function handleCheckToggle(itemId, newState) {
    const updateData = async () => {
      const response = await axios.patch(
        `/api/lists/${listData.id}/checked_state`,
        {
          item_id: itemId,
          checked_state: newState,
        }
      );
      setListData(await response.data);
    };
    updateData();
  }

  if (listData === null) {
    return (
      <div className="ToDoList loading">
        <button className="back" onClick={handleBackButton}>
          Back
        </button>
        Loading to-do list ...
      </div>
    );
  }
  return (
    <div className="ToDoList">
      <button className="back" onClick={handleBackButton}>
        Back
      </button>
      <h1>List: {listData.name}</h1>
      <div className="box">
        <label>
          New Item:&nbsp;
          <input id={labelRef} type="text" />
        </label>
        <button
          onClick={() =>
            handleCreateItem(document.getElementById(labelRef).value)
          }
        >
          New
        </button>
      </div>
      {listData.items.length > 0 ? (
        listData.items.map((item) => {
          return (
            <div
              key={item.id}
              className={item.checked ? "item checked" : "item"}
              onClick={() => handleCheckToggle(item.id, !item.checked)}
            >
              <span>{item.checked ? "✅" : "⬜️"} </span>
              <span className="label">{item.label} </span>
              <span className="flex"></span>
              <span
                className="trash"
                onClick={(evt) => {
                  evt.stopPropagation();
                  handleDeleteItem(item.id);
                }}
              >
                <BiSolidTrash />
              </span>
            </div>
          );
        })
      ) : (
        <div className="box">There are currently no items.</div>
      )}
    </div>
  );
}

export default ToDoList;

יצירת קובץ חדש בשם src/ToDoList.css עם התוכן הבא:

.ToDoList .back {
    margin: 0 1em;
    padding: 1em;
    float: left;
}

.ToDoList .item {
    border: 1px solid lightgray;
    padding: 1em;
    margin: 1em;
    cursor: pointer;
    display: flex;
}

.ToDoList .label {
    margin-left: 1ex;
}

.ToDoList .checked .label {
    text-decoration: line-through;
    color: lightgray;
}

שלב 14: עדכון הקובץ העיקרי של CSS

החלף את התוכן של src/index.css בעזרת התוכן הבא:

html, body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-size: 12pt;
}

input, button {
  font-size: 1em;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

.box {
    border: 1px solid lightgray;
    padding: 1em;
    margin: 1em;
}

.flex {
  flex: 1;
}

זה מסתיים חלק 3 מההדרכה, בו הוגדרה הקדמה של האפליקציה של ערך ה FARM stack todo. יצרנו את הרכב העיקרי App, את הרכב ListTodoLists עבור הציגה של כל רשימות העיוות ואת הרכב ToDoList עבור רשימות העיוות הפופולריות. בחלק הבא נרכש

להריץ ולבדוק את היישום

שלב 18: הרץ את היישום בעזרת Docker Compose

  1. וודא שיש לך Docker ו-Docker Compose מותקנים על המערכת שלך

  2. פתח טרמינל בתוך הספרייה הראשית של הפרויקט שלך (farm-stack-todo)

  3. בנה והתחל את הקונטיינרים:

docker-compose up --build
  1. לאחר שהקונטיינרים פעילים, פתח את הדפדפן שלך ולך לhttp://localhost:8000

שלב 19: עצירת היישום

  1. אם אתה מריץ את היישום ללא Docker:

    • עצור את שרת הפיתוח של React על ידי לחיצה על Ctrl+C בטרמינל שלו

    • עצור את שרת FastAPI על ידי לחיצה על Ctrl+C בטרמינל שלו

    • עצור את שרת MongoDB על ידי לחיצה על Ctrl+C בטרמינל שלו

  2. אם אתה מפעיל את היישום עם Docker Compose:

    • לחץ על Ctrl+C בטרמינל בו ביצעת docker-compose up

    • הפעל את הפקודה הבאה כדי לעצור ולהסיר את המיכונים:

     docker-compose down

"`

מזל טוב! בנית ובדקת בהצלחה אפליקציה מלאה עם מערך טכנולוגי FARM עבור משימות מערכת מתוך שימוש ב־FastAPI, React ו־MongoDB.

הנה כמה שלבים נוספים אפשריים לשיפור היישום שלך:

  1. הוסף אימות משתמש והרשאות

  2. הטמע אימות נתונים וטיפול בשגיאות

  3. הוסף יכולות נוספות כמו תאריכי סיום, עדיפויות או קטגוריות עבור המשימות

  4. שפר את ממשק המשתמש עם עיצוב יותר מקודם

  5. כתוב בדיקות יחידה והתאמות עבור שני הצדדים, הקדמה והאחורה

  6. הגדר אינטגרציית יציבות ושידור (CI/CD) עבור היישומך

זכר לעד לעדד את התלויות שלך ולעומד על המידע הטוב ביותר עבור הבטיחות והביצועים בהתמדה בעודך ממשיך לפתח את היישומך.

סיכום ושלבים הבאים

ברכים על שיגמרת המדריך המקיף להדהד FARM! על-ידי בניית אפליקציית המשימות הזו, הרווחת יד-על חוויה ידנית עם חלק מהטכנולוגיות החזקות והפופולריות ביותר בפיתוח הרשת המודרני. למדת איך ליצור אחד קיר חסן עם FastAPI, לבנות קידמת וקידמת פעילה עם React, לשמר נתונים עם MongoDB, ולארגן את כל היישומך בעזרת Docker. הפרוייקט הזה הדגיש איך הטכנולוגיות האלה עובדות יחד בשלווה ליצירת יישום רשת מושלם ומסוגל לגדול.