איך להתקין פרויקט Ruby on Rails v7 עם חזית React על Ubuntu 20.04

המחבר בחר ב-Electronic Frontier Foundation לקבל תרומה כחלק מתוכנית Write for DOnations.

הקדמה

Ruby on Rails הוא פריימוורק פופולרי לפיתוח אפליקציות אינטרנט בצד השרת. הוא מפעיל אפליקציות רבות ומוכרות שקיימות באינטרנט היום, כמו GitHub, Basecamp, SoundCloud, Airbnb ו-Twitch. עם הדגש על חוויית המפתח והקהילה המסורתית שבנויה סביבו, Ruby on Rails יעניק לך את הכלים שאתה צריך לבניית ותחזוקת אפליקציית האינטרנט המודרנית שלך.

React הוא ספריית JavaScript המשמשת ליצירת ממשקי משתמש קדמיים. בתמיכת Facebook, זו אחת מהספריות הקדמיות הפופולריות ביותר באינטרנט היום. React מציע יכולות כמו Virtual Document Object Model (DOM) ומבנה הרכיב, ו-ניהול מצב, ההפקדה והפיתוח של חזית הפיתוח.

עם הממשק החזיתי של הרשת מתקדם למסגרות נפרדות מהקוד בצד השרת, לשלב את האלגנטיות של Rails עם היעילות של React יאפשר לך לבנות יישומים חזקים ומודרניים המושפעים מטרנדים נוכחיים. באמצעות השימוש ב־React לעיבוד מרכיבים מתוך תצורת Rails (במקום מנוע התבניות של Rails), היישום שלך ירוויח מההתקדמויות האחרונות ב־JavaScript ובפיתוח חזיתי בעוד הוא מנצל את היצירתיות של Ruby on Rails.

במדריך זה, תיצור אפליקציית Ruby on Rails המאחסנת את מתכוני המזונות המועדפים עליך ומציגה אותם עם ממשק חזיתי של React. כאשר תסיים, תוכל ליצור, להציג ולמחוק מתכונים באמצעות ממשק React המעוצב עם Bootstrap:

דרישות מוקדמות

כדי לעקוב אחרי המדריך הזה, תצטרך:

הערה: גרסת Rails 7 אינה תואמת לאחור. אם אתה משתמש בגרסת Rails 5, אנא בקר במדריך ל־איך להגדיר פרויקט Ruby on Rails v5 עם חזית React על Ubuntu 18.04.

שלב 1 — יצירת אפליקציית Rails חדשה

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

Rails מספקת מספר תסריטים נקראים מפיקי שיוצרים הכל הנדרש לבניית אפליקציה רשת מודרנית. כדי לקרוא את רשימת הפקודות המלאה ומה הן עושות, הפעל את הפקודה הבאה בטרמינל שלך:

  1. rails -h

פקודה זו תחזיר רשימה מקיפה של אפשרויות, שמאפשרת לך להגדיר את פרמטרי היישום שלך. אחת הפקודות שמפורטות היא פקודת new, שיוצרת יישום חדש של Rails.

עכשיו, תיצור יישום חדש של Rails באמצעות מחולל הקוד new. הרץ את הפקודה הבאה בטרמינל שלך:

  1. rails new rails_react_recipe -d postgresql -j esbuild -c bootstrap -T

הפקודה הקודמת תיצור יישום חדש של Rails בתיקייה בשם rails_react_recipe, תתקין את תלות ה-Ruby וה-JavaScript הנדרשות, ותגדיר את Webpack. הדגלים המשויכים לפקודת מחולל הקוד new כוללים את הדברים הבאים:

  • הדגל -d מציין את מנוע מסד הנתונים המועדף, שבמקרה זה הוא PostgreSQL.
  • הדגל -j מציין את הגישת ה-JavaScript של היישום. Rails מציעה כמה דרכים שונות לטיפול בקוד JavaScript ביישומי Rails. האפשרות esbuild שמועברת לפקודת הדגל -j מורה ל-Rails להגדיר מראש את esbuild כאוסף ה-JavaScript המועדף.
  • הדגל -c מציין את מעבד ה-CSS של היישום. Bootstrap היא האפשרות המועדפת במקרה זה.
  • הדגל -T מורה ל-Rails לדלג על יצירת קבצי בדיקה מאחר ואינך מתכוון לכתוב בדיקות עבור המדריך הזה. הפקודה גם מוצעת אם ברצונך להשתמש בכלי בדיקה שונה מהכלי ש-Rails מספקת.

כאשר הפקודה תסיים, עבור לתיקייה rails_react_recipe, שהיא התיקייה הראשית של היישום שלך:

  1. cd rails_react_recipe

לבסוף, רשום את תוכן התיקייה:

  1. ls

התוכן יודפס בדיוק כמו זה:

Output
Gemfile README.md bin db node_modules storage yarn.lock Gemfile.lock Rakefile config lib package.json tmp Procfile.dev app config.ru log public vendor

תיקיית השורש הזו מכילה מספר קבצים ותיקיות שנוצרו אוטומטית שמהווים את מבנה היישום של Rails, כולל קובץ package.json המכיל תלויות עבור יישום React.

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

שלב 2 — התקנת הבסיס נתונים

לפני שתפעילו את האפליקציה החדשה שלכם של Rails, עליכם תחילה לחבר אותה לבסיס נתונים. בשלב זה, תחברו את האפליקציה שנוצרה לאחרונה של Rails לבסיס נתונים של PostgreSQL כך שניתן יהיה לאחסן ולקבל נתונים על פי הצורך.

הקובץ database.yml שנמצא בתיקיית config/database.yml מכיל פרטי בסיס נתונים כמו שמות הבסיס נתונים לסביבות פיתוח שונות. Rails מציין שם של בסיס נתונים לסביבות פיתוח שונות על ידי הוספת קו תחתון (_) ואחריו שם הסביבה. במדריך זה, תשתמשו בערכי הגדרת בסיס נתונים ברירת המחדל, אך תוכלו לשנות את ערכי ההגדרה שלכם אם נדרש.

הערה: בנקודה זו, ניתן לשנות את config/database.yml כדי להגדיר איזה תפקיד PostgreSQL תרצה ש- Rails ישתמש בו כדי ליצור את מסד הנתונים שלך. במהלך הנדרשים, יצרת תפקיד שמאובטח על ידי סיסמה בתוך המדריך איך להשתמש ב-PostgreSQL עם אפליקציית Ruby on Rails שלך. אם עדיין לא הגדרת את המשתמש, תוכל כעת לעקוב אחר ההוראות עבור שלב 4 — הגדרת ויצירת מסד הנתונים שלך באותו מדריך נדרש.

Rails מציעה הרבה פקודות שמקלות על פיתוח אפליקציות אינטרנט, כולל פקודות לעבוד עם מסדי נתונים כמו create, drop, ו- reset. כדי ליצור מסד נתונים עבור האפליקציה שלך, הפעל את הפקודה הבאה בטרמינל שלך:

  1. rails db:create

פקודה זו יוצרת מסד נתונים development ו- test, ומחזירה את הפלט הבא:

Output
Created database 'rails_react_recipe_development' Created database 'rails_react_recipe_test'

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

  1. bin/dev

Rails מספקת סקריפט bin/dev אלטרנטיבי שמפעיל אפליקציית Rails על ידי ביצוע הפקודות בקובץ Procfile.dev בתיקיית השורש של האפליקציה באמצעות הג'ם Foreman.

לאחר הרצת הפקודה הזו, סמל הפקודה שלך יעלם, ובמקוםו יודפס הפלט הבא:

Output
started with pid 70099 started with pid 70100 started with pid 70101 yarn run v1.22.10 yarn run v1.22.10 $ esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets --watch $ sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules --watch => Booting Puma => Rails 7.0.4 application starting in development => Run `bin/rails server --help` for more startup options [watch] build finished, watching for changes... Puma starting in single mode... * Puma version: 5.6.5 (ruby 3.1.2-p20) ("Birdie's Version") * Min threads: 5 * Max threads: 5 * Environment: development * PID: 70099 * Listening on http://127.0.0.1:3000 * Listening on http://[::1]:3000 Use Ctrl-C to stop Sass is watching for changes. Press Ctrl-C to stop.

כדי לגשת ליישום שלך, פתח חלון דפדפן ונווט אל http://localhost:3000. עמוד הברכה ברילס יטען, שזה אומר שהגדרת את יישום הרילס שלך בצורה נכונה:

כדי לעצור את שרת האינטרנט, לחץ על CTRL+C בטרמינל שבו השרת פועל. תקבל הודעת פרידה מ־Puma:

Output
^C SIGINT received, starting shutdown - Gracefully stopping, waiting for requests to finish === puma shutdown: 2019-07-31 14:21:24 -0400 === - Goodbye! Exiting sending SIGTERM to all processes terminated by SIGINT terminated by SIGINT exited with code 0

לאחר מכן יופיע מחדש סימן התווית של הטרמינל שלך.

התקנת בהצלחה מסד נתונים עבור יישום מתכוני המזון שלך. בשלב הבא, תתקין את תלותי ה־JavaScript שנדרשים לבניית החזית של React שלך.

שלב 3 — התקנת תלותי החזית

בשלב זה, תתקין את תלותי ה־JavaScript הנדרשים בחזית של יישום מתכוני המזון שלך. הם כוללים:

  • React לבניית ממשקי משתמש.
  • React DOM כדי לאפשר ל־React לפעול בקשר עם DOM של הדפדפן.
  • React Router לטיפול בניווט ביישום React.

הפעל את הפקודה הבאה כדי להתקין את החבילות הללו עם מנהל החבילות Yarn:

  1. yarn add react react-dom react-router-dom

הפקודה הזו משתמשת ב־Yarn כדי להתקין את החבילות הספציפיות ולהוסיף אותן לקובץ package.json. כדי לאמת זאת, פתח את קובץ package.json שנמצא בתיקיית השורש של הפרויקט:

  1. nano package.json

החבילות שהותקנו יופיעו ברשימה תחת המפתח dependencies:

~/rails_react_recipe/package.json
{
  "name": "app",
  "private": "true",
  "dependencies": {
    "@hotwired/stimulus": "^3.1.0",
    "@hotwired/turbo-rails": "^7.1.3",
    "@popperjs/core": "^2.11.6",
    "bootstrap": "^5.2.1",
    "bootstrap-icons": "^1.9.1",
    "esbuild": "^0.15.7",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.3.0",
    "sass": "^1.54.9"
  },
  "scripts": {
    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets",
    "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
  }
}

סגור את הקובץ על ידי לחיצה על CTRL+X.

התקנת כמה תלות קדמה לממשק המשתמש שלך. לבסוף, תכין דף הבית עבור אפליקציית המתכונים שלך.

שלב 4 — הגדרת הדף הראשי

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

Rails עוקבת אחר תבנית הארכיטקטורה Model-View-Controller (MVC) לאפליקציות. בתבנית ה־MVC, מטרת ה־controller היא לקבל בקשות ספציפיות ולהעביר אותן ל־model או ל־view המתאימים. כרגע, האפליקציה מציגה את דף הברכה של Rails כאשר כתובת ה־URL הראשית נטענת בדפדפן. כדי לשנות זאת, תיצור controller ו־view עבור הדף הראשי ותתאים אותו לנתיב.

Rails מספק גנרטור controller ליצירת נווט. הגנרטור controller מקבל שם של נווט ופעולה תואמת. למידע נוסף, ניתן לעיין ב-תיעוד של Rails.

במדריך זה ישמש הנווט בשם Homepage. הריצו את הפקודה הבאה כדי ליצור נווט בשם Homepage עם פעולת index:

  1. rails g controller Homepage index

הערה:
ב-Linux, ייתכן שהשגיאה FATAL: Listen error: unable to monitor directories for changes. תתקבל ממגבלת המערכת על מספר הקבצים שניתן לצפות בהם לשם שינויים. הריצו את הפקודה הבאה כדי לתקן זאת:

  1. echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

פקודה זו תגדיל באופן קבוע את מספר התיקיות שניתן לצפות בהן עם Listen ל-524288. ניתן לשנות זאת שוב על ידי הרצת אותה פקודה והחלפת 524288 במספר הרצוי.

הרצת הפקודה controller יוצרת את הקבצים הבאים:

  • A homepage_controller.rb file for receiving all homepage-related requests. This file contains the index action you specified in the command.
  • A homepage_helper.rb file for adding helper methods related to the Homepage controller.
  • קובץ index.html.erb כעמוד תצוגה להצגת דברים הקשורים לדף הבית.

בנוסף לעמודים החדשים הללו שנוצרו על ידי פקודת Rails, היא גם מעדכנת את קובץ הנתיבים שלך שנמצא ב-config/routes.rb, ומוסיפה מסלול get עבור דף הבית, שתשנה כדי להיות המסלול הראשי שלך.

A root route in Rails specifies what will show up when users visit the root URL of your application. In this case, you want your users to see your homepage. Open the routes file located at config/routes.rb in your favorite editor:

  1. nano config/routes.rb

בקובץ זה, החליפו get 'homepage/index' ב-root 'homepage#index' כך שהקובץ יתאים למטה:

~/rails_react_recipe/config/routes.rb
Rails.application.routes.draw do
  root 'homepage#index'
  # עבור פרטים על DSL הזמין בקובץ זה, ראה http://guides.rubyonrails.org/routing.html
end

השינוי הזה מורה ל־Rails למפתח בקשות אל שורש היישום אל הפעולה index של הבקר ה־Homepage, שבתור תוצאה מציג בדפדפן כל מה שנמצא בקובץ index.html.erb השייך לנתיב app/views/homepage/index.html.erb.

שמור וסגור את הקובץ.

כדי לוודא שהכל עובד, התחל את היישום שלך:

  1. bin/dev

כאשר אתה פותח או מרענן את היישום בדפדפן, דף נחיתה חדש יעלה עבור היישום שלך:

כאשר אתה מוודא שהיישום שלך עובד, לחץ על CTRL+C כדי לעצור את השרת.

לאחר מכן, פתח את הקובץ ~/rails_react_recipe/app/views/homepage/index.html.erb:

  1. nano ~/rails_react_recipe/app/views/homepage/index.html.erb

הסר את הקוד מתוך הקובץ, ושמור אותו כריק. בכך אתה מבטיח שתוכן ה־index.html.erb לא יתערב בעיבוד ה־React של החזית שלך.

עכשיו שהגדרת את דף הבית של היישום שלך, תוכל לעבור לחלק הבא, בו תגדיר את חזית היישום שלך להשתמש ב־React.

שלב 5 — הגדרת React כחזית היישום שלך ב־Rails

בשלב זה, תגדיר את Rails להשתמש ב-React בחזית היישום, במקום במנוע התבנית שלו. ההגדרה החדשה תאפשר לך ליצור דף הבית יותר מוצלח ומושך עין בעזרת React.

עם עזרת האפשרות esbuild שצוינה בעת יצירת היישום של Rails, רוב ההגדרות הדרושות לאפשר עבודה חלקה של ג'אווה סקריפט עם Rails כבר מוכנות. הכל שנותר הוא לטעון את נקודת הכניסה של האפליקציה של React לנקודת הכניסה של esbuild לקבצי ג'אווה סקריפט. כדי לעשות זאת, התחל על ידי יצירת ספריית components בספריית app/javascript:

  1. mkdir ~/rails_react_recipe/app/javascript/components

הספרייה components תאחסן את הרכיב לדף הבית, יחד עם רכיבי React נוספים באפליקציה, כולל הקובץ הכניסה לאפליקציה של React.

באמצעות פתיחת קובץ application.js שנמצא במיקום app/javascript/application.js:

  1. nano ~/rails_react_recipe/app/javascript/application.js

הוסף את שורת הקוד המודגשת לקובץ:

~/rails_react_recipe/app/javascript/application.js
// נקודת הכניסה לתסריט הבנייה ב-package.json שלך
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
import "./components"

השורה שנוספה לקובץ application.js תייבא את הקוד בקובץ הכניסה index.jsx, ותגריל אותו ל-esbuild לצורך אריזה. עם ספריית /components מיובאת לנקודת הכניסה של ג'אווה סקריפט של Rails, תוכל ליצור רכיב React עבור דף הבית שלך. הדף הבית יכיל טקסטים ולחצן לפעולה לצפייה בכל המתכונים.

שמור וסגור את הקובץ.

לאחר מכן, צור קובץ Home.jsx בתוך ספריית components:

  1. nano ~/rails_react_recipe/app/javascript/components/Home.jsx

הוסף את הקוד הבא לקובץ:

~/rails_react_recipe/app/javascript/components/Home.jsx
import React from "react";
import { Link } from "react-router-dom";

export default () => (
  <div className="vw-100 vh-100 primary-color d-flex align-items-center justify-content-center">
    <div className="jumbotron jumbotron-fluid bg-transparent">
      <div className="container secondary-color">
        <h1 className="display-4">Food Recipes</h1>
        <p className="lead">
          A curated list of recipes for the best homemade meal and delicacies.
        </p>
        <hr className="my-4" />
        <Link
          to="/recipes"
          className="btn btn-lg custom-button"
          role="button"
        >
          View Recipes
        </Link>
      </div>
    </div>
  </div>
);

בקוד זה, אתה מייבא את React ואת רכיב ה־`Link` מראקט ראוטר. רכיב ה־`Link` יוצר קישור היפרטקסט לניווט מדף אחד לדף אחר. לאחר מכן, אתה יוצר ומייצא רכיב פונקציונלי המכיל מספר שפת סימון לדף הבית שלך, העצוב באמצעות קלאסים של Bootstrap.

שמור וסגור את הקובץ.

עם רכיב ה־`Home` שלך מוגדר, עכשיו תגדיר את הניתוב באמצעות ראקט ראוטר. צור ספריית `routes` בתוך הספרייה `app/javascript`:

  1. mkdir ~/rails_react_recipe/app/javascript/routes

הספרייה `routes` תכיל מספר נתיבים עם הרכיבים המתאימים להם. בכל פעם שנתיב מסוים מוזן, הוא יעביר לדפדפן את הרכיב המתאים לו.

בתוך הספרייה `routes`, צור קובץ בשם `index.jsx`:

  1. nano ~/rails_react_recipe/app/javascript/routes/index.jsx

הוסף את הקוד הבא לתוך הקובץ:

~/rails_react_recipe/app/javascript/routes/index.jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";

export default (
  <Router>
    <Routes>
      <Route path="/" element={<Home />} />
    </Routes>
  </Router>
);

בקובץ נתיב `index.jsx` הזה, אתה מייבא את המודולים הבאים: מודול `React` שמאפשר לך להשתמש בריאקט, כמו גם מודולי `BrowserRouter`, `Routes`, ו־`Route` מראקט ראוטר, שיעזרו לך לנווט מנתיב אחד לאחר. לבסוף, אתה מייבא את רכיב ה־`Home` שלך, שיעבור להיות מרונדר כאשר בקשת מתאימה מתאימה לנתיב השורש (/). כאשר אתה רוצה להוסיף עוד דפים לאפליקציה שלך, תוכל להכריז על נתיב בקובץ זה ולהתאים אותו לרכיב שברצונך להציג באותו דף.

שמור וצא מהקובץ.

יש לך כעת הגדיר מסלולים באמצעות React Router. כדי ש-React יהיה מודע למסלולים הזמינים וישתמש בהם, עליהם להיות זמינים בנקודת הכניסה ליישום. כדי להשיג זאת, יש להציג את המסלולים שלך ברכיב ש-React יציג בקובץ הכניסה שלך.

צור קובץ App.jsx בתיקייה app/javascript/components:

  1. nano ~/rails_react_recipe/app/javascript/components/App.jsx

הוסף את הקוד הבא לקובץ App.jsx:

~/rails_react_recipe/app/javascript/components/App.jsx
import React from "react";
import Routes from "../routes";

export default props => <>{Routes}</>;

בקובץ App.jsx, יש לייבא את React וקבצי המסלול שיצרת כעת. לאחר מכן, יש לייצא רכיב להצגת המסלולים בתוך fragments. רכיב זה יתווסף בנקודת הכניסה של היישום, מה שיביא להצגת המסלולים בכל פעם שהיישום נטען.

שמור וסגור את הקובץ.

עכשיו שיש לך את הקובץ App.jsx שלך, ניתן להציג אותו בקובץ הכניסה שלך. צור קובץ index.jsx בתיקייה components:

  1. nano ~/rails_react_recipe/app/javascript/components/index.jsx

הוסף את הקוד הבא לקובץ index.js:

~/rails_react_recipe/app/javascript/components/index.jsx
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

document.addEventListener("turbo:load", () => {
  const root = createRoot(
    document.body.appendChild(document.createElement("div"))
  );
  root.render(<App />);
});

בשורות ה-import, יש לייבא את ספריית React, את הפונקציה createRoot מתוך ReactDOM, ואת הרכיב App שלך. באמצעות הפונקציה createRoot של ReactDOM, יש ליצור אלמנט שורש כאלמנט div המוסף לדף, ולהציג את רכיב App שלך בתוכו. כאשר היישום נטען, React יציג את תוכן רכיב App בתוך אלמנט div בעמוד.

שמור וצא מהקובץ.

לבסוף, יש להוסיף כמה סגנונות CSS לדף הבית שלך.

פתח את קובץ application.bootstrap.scss בתיקיית ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss שלך:

  1. nano ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss

לאחר מכן, החלף את תוכן קובץ application.bootstrap.scss בקוד הבא:

~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
@import 'bootstrap/scss/bootstrap';
@import 'bootstrap-icons/font/bootstrap-icons';

.bg_primary-color {
  background-color: #FFFFFF;
}
.primary-color {
  background-color: #FFFFFF;
}
.bg_secondary-color {
  background-color: #293241;
}
.secondary-color {
  color: #293241;
}
.custom-button.btn {
  background-color: #293241;
  color: #FFF;
  border: none;
}
.hero {
  width: 100vw;
  height: 50vh;
}
.hero img {
  object-fit: cover;
  object-position: top;
  height: 100%;
  width: 100%;
}
.overlay {
  height: 100%;
  width: 100%;
  opacity: 0.4;
}

קבע צבעים אישיים עבור העמוד. הקטע .hero יצור את המסגרת עבור דימוי גיבור, או באנר רשת גדול בדף הקדמי של האתר שלך, שתוסיף מאוחר יותר. בנוסף, סגנון custom-button.btn עוצב עבור הכפתור שהמשתמש ישתמש בו כדי להיכנס ליישום.

עם הסגנונות של CSS במקום, שמור וצא מהקובץ.

לאחר מכן, הפעל מחדש את שרת האינטרנט של היישום שלך:

  1. bin/dev

לאחר מכן, טען מחדש את היישום בדפדפן שלך. דף הבית החדש יטען:

עצור את שרת האינטרנט עם CTRL+C.

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

שלב 6 — יצירת בקר מתכונים ומודל

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

התחל על ידי יצירת מודל Recipe באמצעות פקודת העזר generate model שמספקת ריילס, וביטוי בשם המודל יחד עם עמודותיו וסוגי הנתונים שלהן. הרץ את הפקודה הבאה:

  1. rails generate model Recipe name:string ingredients:text instruction:text image:string

הפקודה הקודמת מזמינה לריילס ליצור מודל Recipe יחד עם עמודת name מסוג string, עמודות ingredients ו־instruction מסוג text, ועמודת image מסוג string. בתור הדרכה זו נקרא למודל Recipe, מכיוון שמודלים בריילס משתמשים בשם יחיד בעוד שטבלאות מסד הנתונים המתאימות שלהם משתמשות בשם רבים.

הרצת הפקודה generate model יוצרת שני קבצים ומדפיסה את הפלט הבא:

Output
invoke active_record create db/migrate/20221017220817_create_recipes.rb create app/models/recipe.rb

השני קבצים שנוצרים הם:

  • A recipe.rb file that holds all the model-related logic.
  • A 20221017220817_create_recipes.rb file (the number at the beginning of the file may differ depending on the date when you run the command). This migration file contains the instruction for creating the database structure.

הבא, עליך לערוך את קובץ מודל המתכון כדי לוודא שרק נתונים תקינים יישמרו במסד הנתונים. באפשרותך להשיג זאת על ידי הוספת ולידציה של בסיס הנתונים למודל שלך.

פתח את קובץ מודל המתכון שלך הממוקם ב- app/models/recipe.rb:

  1. nano ~/rails_react_recipe/app/models/recipe.rb

הוסף את השורות המודגשות הבאות לקובץ:

~/rails_react_recipe/app/models/recipe.rb
class Recipe < ApplicationRecord
  validates :name, presence: true
  validates :ingredients, presence: true
  validates :instruction, presence: true
end

בקוד זה, אתה מוסיף אימות של מודל שבודק את קיומם של שדות name, ingredients, ו־instruction. בלעדי השדות הללו, מתכון אינו חוקי ולא יישמר במסד הנתונים.

שמור וסגור את הקובץ.

כדי ש־Rails ייצור את טבלת recipes במסד הנתונים שלך, עליך להפעיל הגירסה, שהיא אמצעי לבצע שינויים במסד הנתונים שלך באופן תכנותי. כדי לוודא שהגירסה עובדת עם המסד הנתונים שהגדרת, עליך לבצע שינויים בקובץ 20221017220817_create_recipes.rb.

פתח את הקובץ הזה בעורך שלך:

  1. nano ~/rails_react_recipe/db/migrate/20221017220817_create_recipes.rb

הוסף את החומרים המודגשים כך שהקובץ שלך מתאים למטה:

db/migrate/20221017220817_create_recipes.rb
class CreateRecipes < ActiveRecord::Migration[5.2]
  def change
    create_table :recipes do |t|
      t.string :name, null: false
      t.text :ingredients, null: false
      t.text :instruction, null: false
      t.string :image, default: 'https://raw.githubusercontent.com/do-community/react_rails_recipe/master/app/assets/images/Sammy_Meal.jpg'

      t.timestamps
    end
  end
end

קובץ הגירסה הזה מכיל מחלקת Ruby עם שיטת change ופקודה ליצירת טבלה בשם recipes יחד עם העמודות וסוגי הנתונים שלהם. אתה גם מעדכן את 20221017220817_create_recipes.rb עם אילוצי NOT NULL על העמודות name, ingredients, ו־instruction על ידי הוספת null: false, מבטיח שיש לעמודות אלו ערך לפני שאתה משנה את מסד הנתונים. לבסוף, אתה מוסיף כתובת URL ברירת מחדל לתמונתך; זו יכולה להיות כתובת URL נוספת אם ברצונך להשתמש בתמונה שונה.

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

  1. rails db:migrate

אתה משתמש בפקודת העברת מסד נתונים כדי להריץ את ההוראות בקובץ ההעברה שלך. לאחר שהפקודה רצה בהצלחה, תקבל פלט דומה לזה:

Output
== 20190407161357 CreateRecipes: migrating ==================================== -- create_table(:recipes) -> 0.0140s == 20190407161357 CreateRecipes: migrated (0.0141s) ===========================

עם המודל של המתכון שלך במקום, תיצור באמצעות פקודת הבנייה את בקר המתכונים שלך להוספת הלוגיקה ליצירה, קריאה ומחיקת מתכונים. רוץ את הפקודה הבאה:

  1. rails generate controller api/v1/Recipes index create show destroy --skip-template-engine --no-helper

בפקודה זו, תיצור בקר Recipes בספרייה api/v1 עם פעולות index, create, show ו- destroy. הפעולה index תעסוק באחזור כל המתכונים שלך; הפעולה create תהיה אחראית ליצירת מתכונים חדשים; הפעולה show תאחזר מתכון יחיד, והפעולה destroy תחזיק את הלוגיקה למחיקת מתכון.

אתה גם מעביר כמה דגלים כדי להפוך את הבקר לקל משקל, כולל:

  • --skip-template-engine, שמוריד ל-Rails להתעלם מיצירת קבצי תצוגה של Rails מאחר ש-React טופל את צרכי הקדמה שלך.
  • --no-helper, שמוריד ל-Rails להתעלם מיצירת קובץ עזר עבור הבקר שלך.

הרצת הפקודה גם מעדכנת את קובץ הנתיבים שלך עם נתיב עבור כל פעולה בבקר Recipes.

כאשר הפקודה רצה, היא תדפיס פלט כזה:

Output
create app/controllers/api/v1/recipes_controller.rb route namespace :api do namespace :v1 do get 'recipes/index' get 'recipes/create' get 'recipes/show' get 'recipes/destroy' end end

כדי להשתמש בנתיבים אלה, תערוך את קובץ config/routes.rb שלך. פתח את קובץ routes.rb שלך בעורך טקסט:

  1. nano ~/rails_react_recipe/config/routes.rb

עדכן את קובץ זה כדי שיראה כמו הקוד הבא, על ידי שינוי או הוספת השורות המודגשות:

~/rails_react_recipe/config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      get 'recipes/index'
      post 'recipes/create'
      get '/show/:id', to: 'recipes#show'
      delete '/destroy/:id', to: 'recipes#destroy'
    end
  end
  root 'homepage#index'
  get '/*path' => 'homepage#index'
  # קבע את נתיבי היישום שלך על פי DSL ב- https://guides.rubyonrails.org/routing.html

  # מגדיר את הנתיב לדף הראשי ("/")
  # root "articles#index"
end

בקובץ הנתיבים הזה, אתה משנה את פעולת ה-HTTP verb של הנתיבים create ו־destroy כך שהם יכולים להיות post ו־delete לנתונים. גם אתה משנה את הנתיבים עבור הפעולות show ו־destroy על ידי הוספת פרמטר :id לנתיב. :id יחזיק את מספר הזיהוי של המתכון שברצונך לקרוא או למחוק.

אתה מוסיף נתיב catch-all עם get '/*path' שיפנה כל בקשה אחרת שאינה תואמת לנתיבים הקיימים לפעולת index של ממשק הבית. הניתוב בצד הלקוח יטפל בבקשות שאינן קשורות ליצירה, קריאה או מחיקת מתכונים.

שמור וצא מהקובץ.

כדי לאמות רשימת נתיבים הזמינים ביישום שלך, הרץ את הפקודה הבאה:

  1. rails routes

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

בשלב הבא, תוסיף את הלוגיקה לקבלת כל המתכונים בו זמנית. Rails משתמש בספריית ActiveRecord לטיפול במשימות הקשורות לבסיס הנתונים כמו זו. ActiveRecord מחברת בין מחלקות לטבלאות בסיס נתונים ומספקת API עשיר לעבודה עם כן.

כדי לקבל את כל המתכונים, תשתמש ב-ActiveRecord כדי לשאול את טבלת המתכונים ולקבל את כל המתכונים בבסיס הנתונים.

פתח את קובץ recipes_controller.rb עם הפקודה הבאה:

  1. nano ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb

הוסף את השורות שמסומנות בצבע צהוב לבקר המתכונים:

~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
class Api::V1::RecipesController < ApplicationController
  def index
    recipe = Recipe.all.order(created_at: :desc)
    render json: recipe
  end

  def create
  end

  def show
  end

  def destroy
  end
end

בפעולת ה-index שלך, אתה משתמש בשיטת all של ActiveRecord כדי לקבל את כל המתכונים בבסיס הנתונים שלך. באמצעות השיטה order, אתה מסדר אותם בסדר יורד לפי תאריך היצירה שלהם, כלומר, תמקם את המתכונים החדשים ביותר תחילה. לבסוף, אתה שולח את רשימת המתכונים שלך כתגובה JSON עם render.

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

~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
class Api::V1::RecipesController < ApplicationController
  def index
    recipe = Recipe.all.order(created_at: :desc)
    render json: recipe
  end

  def create
    recipe = Recipe.create!(recipe_params)
    if recipe
      render json: recipe
    else
      render json: recipe.errors
    end
  end

  def show
  end

  def destroy
  end

  private

  def recipe_params
    params.permit(:name, :image, :ingredients, :instruction)
  end
end

בפעולת create, אתה משתמש בשיטת create של ActiveRecord כדי ליצור מתכון חדש. שיטת create יכולה להקצות את כל הפרמטרים של הבקר פרובייקט פעם אחת למודל. שיטה זו עושה את התהליך של יצירת רשומות פשוט אך פותחת את האפשרות לשימוש זדוני. שימוש זדוני ניתן למנוע באמצעות השימוש בתכונת פרמטרים חזקים הזמינה על ידי Rails. בדרך זו, פרמטרים לא יכולים להיות מוקצים אלא אם כן הותרו. אתה מעביר פרמטר recipe_params לשיטת create בקוד שלך. recipe_params היא שיטה private שבה אתה מאפשר לפרמטרי הבקר שלך למנוע תוכן שגוי או זדוני מהנכנס למסד הנתונים שלך. במקרה זה, אתה מאפשר לפרמטרים name, image, ingredients, ו־instruction לשימוש תקין של שיטת create.

בקר המתכונים שלך כעת יכול לקרוא וליצור מתכונים. כל מה שנשאר הוא הלוגיקה לקריאה ומחיקה של מתכון יחיד. עדכן את בקר המתכונים שלך עם הקוד המודגש הבא:

~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
class Api::V1::RecipesController < ApplicationController
  before_action :set_recipe, only: %i[show destroy]

  def index
    recipe = Recipe.all.order(created_at: :desc)
    render json: recipe
  end

  def create
    recipe = Recipe.create!(recipe_params)
    if recipe
      render json: recipe
    else
      render json: recipe.errors
    end
  end

  def show
    render json: @recipe
  end

  def destroy
    @recipe&.destroy
    render json: { message: 'Recipe deleted!' }
  end

  private

  def recipe_params
    params.permit(:name, :image, :ingredients, :instruction)
  end

  def set_recipe
    @recipe = Recipe.find(params[:id])
  end
end

בשורות הקוד החדשות, אתה יוצר שיטה פרטית בשם set_recipe שנקראת על ידי before_action רק כאשר פעולות show ו- delete תואמות בקשה. השיטה set_recipe משתמשת בשיטת find של ActiveRecord כדי למצוא מתכון שמספר ה- id שלו תואם ל- id הניתן ב- params ומש assign אותו למשתנה מופע @recipe. בפעולת show, אתה מחזיר את אובייקט ה- @recipe שנקבע על ידי שיטת set_recipe כתגובת JSON.

בפעולת destroy, עשית משהו דומה באמצעות אופרטור הניווט הבטוח של Ruby &., הממנה ניתן להימנע משגיאות nil בזמן קריאת שיטה. התוספת הזו מאפשרת לך למחוק מתכון רק אם קיים, ולאחר מכן לשלוח הודעה כתגובה.

לאחר שעשית את השינויים הללו ב- recipes_controller.rb, שמור וסגור את הקובץ.

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

שלב 7 — צפייה במתכונים

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

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

פתח את קובץ הזרע הנקרא seeds.rb לעריכה:

  1. nano ~/rails_react_recipe/db/seeds.rb

החלף את התוכן הראשוני של קובץ הזרע עם הקוד הבא:

~/rails_react_recipe/db/seeds.rb
9.times do |i|
  Recipe.create(
    name: "Recipe #{i + 1}",
    ingredients: '227g tub clotted cream, 25g butter, 1 tsp cornflour,100g parmesan, grated nutmeg, 250g fresh fettuccine or tagliatelle, snipped chives or chopped parsley to serve (optional)',
    instruction: 'In a medium saucepan, stir the clotted cream, butter, and cornflour over a low-ish heat and bring to a low simmer. Turn off the heat and keep warm.'
  )
end

בקוד זה, אתה משתמש בלולאה שמורה ל-Rails ליצור תשעה מתכונים עם קטעים עבור name, ingredients, ו־instruction. שמור וצא מהקובץ.

כדי להזריע את בסיס הנתונים עם הנתונים האלו, הרץ את הפקודה הבאה בטרמינל שלך:

  1. rails db:seed

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

הרכיב להצגת כל המתכונים יבצע בקשת HTTP לפעולת index ב-RecipesController כדי לקבל רשימה של כל המתכונים. המתכונים האלה יוצגו לאחר מכן בכרטיסיות על העמוד.

צור קובץ Recipes.jsx בתיקיית app/javascript/components:

  1. nano ~/rails_react_recipe/app/javascript/components/Recipes.jsx

לאחר שהקובץ נפתח, יבא את המודולים React, useState, useEffect, Link, ו־useNavigate על ידי הוספת השורות הבאות:

~/rails_react_recipe/app/javascript/components/Recipes.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";

לבסוף, הוסף את השורות המודגשות כדי ליצור ולייצא רכיב React פונקציונלי בשם Recipes:

~/rails_react_recipe/app/javascript/components/Recipes.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";

const Recipes = () => {
  const navigate = useNavigate();
  const [recipes, setRecipes] = useState([]);
};

export default Recipes;

בתוך רכיב ה־Recipe, תישאר ממשק הניווט של React Router יקרא ל־הוק useNavigate. ה־הוק useState של React יאתחל את המצב recipes, שהוא מערך ריק ([]), ופונקצית setRecipes לעדכון המצב recipes.

לבאר, ב־הוק useEffect, תבצע בקשת HTTP כדי לאחזר את כל המתכונים שלך. כדי לעשות זאת, הוסף את השורות המודגשות:

~/rails_react_recipe/app/javascript/components/Recipes.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";

const Recipes = () => {
  const navigate = useNavigate();
  const [recipes, setRecipes] = useState([]);

  useEffect(() => {
    const url = "/api/v1/recipes/index";
    fetch(url)
      .then((res) => {
        if (res.ok) {
          return res.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((res) => setRecipes(res))
      .catch(() => navigate("/"));
  }, []);
};

export default Recipes;

בתוך ה־useEffect שלך, תבצע בקשת HTTP לאחזור כל המתכונים באמצעות עזרת ה־Fetch API. אם התגובה מוצלחת, היישום ישמור את מערך המתכונים במצב recipes. אם אירעה שגיאה, היישום יפנה את המשתמש לדף הבית.

לבסוף, החזר את הסימונים עבור האלמנטים שיוערכו ויתצגו בדף הדפדפן כאשר הרכיב יתווסף. במקרה זה, הרכיב יציג כרטיס מתכונים ממצב recipes. הוסף את השורות המודגשות ל־Recipes.jsx:

~/rails_react_recipe/app/javascript/components/Recipes.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";

const Recipes = () => {
  const navigate = useNavigate();
  const [recipes, setRecipes] = useState([]);

  useEffect(() => {
    const url = "/api/v1/recipes/index";
    fetch(url)
      .then((res) => {
        if (res.ok) {
          return res.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((res) => setRecipes(res))
      .catch(() => navigate("/"));
  }, []);

  const allRecipes = recipes.map((recipe, index) => (
    <div key={index} className="col-md-6 col-lg-4">
      <div className="card mb-4">
        <img
          src={recipe.image}
          className="card-img-top"
          alt={`${recipe.name} image`}
        />
        <div className="card-body">
          <h5 className="card-title">{recipe.name}</h5>
          <Link to={`/recipe/${recipe.id}`} className="btn custom-button">
            View Recipe
          </Link>
        </div>
      </div>
    </div>
  ));
  const noRecipe = (
    <div className="vw-100 vh-50 d-flex align-items-center justify-content-center">
      <h4>
        No recipes yet. Why not <Link to="/new_recipe">create one</Link>
      </h4>
    </div>
  );

  return (
    <>
      <section className="jumbotron jumbotron-fluid text-center">
        <div className="container py-5">
          <h1 className="display-4">Recipes for every occasion</h1>
          <p className="lead text-muted">
            We’ve pulled together our most popular recipes, our latest
            additions, and our editor’s picks, so there’s sure to be something
            tempting for you to try.
          </p>
        </div>
      </section>
      <div className="py-5">
        <main className="container">
          <div className="text-end mb-3">
            <Link to="/recipe" className="btn custom-button">
              Create New Recipe
            </Link>
          </div>
          <div className="row">
            {recipes.length > 0 ? allRecipes : noRecipe}
          </div>
          <Link to="/" className="btn btn-link">
            Home
          </Link>
        </main>
      </div>
    </>
  );
};

export default Recipes;

שמור וצא מ־Recipes.jsx.

עכשיו שיצרת רכיב להצגת כל המתכונים, תיצור מסלול עבורו. פתח את קובץ המסלולים של הקצה הקדמי app/javascript/routes/index.jsx:

  1. nano app/javascript/routes/index.jsx

הוסף את השורות המודגשות לקובץ:

~/rails_react_recipe/app/javascript/routes/index.jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";

export default (
  <Router>
    <Routes>
      <Route path="/" exact component={Home} />
      <Route path="/recipes" element={<Recipes />} />
    </Routes>
  </Router>
);

שמור וצא מהקובץ.

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

  1. bin/dev

ואז פתח את האפליקציה בדפדפן שלך. לחץ על לחצן הצג מתכון בדף הבית כדי לגשת לדף תצוגה עם המתכונים שלך:

השתמש ב־CTRL+C בטרמינל שלך כדי לעצור את השרת ולחזור להזמנה שלך.

כעת שאתה יכול לראות את כל המתכונים באפליקציה שלך, זה הזמן ליצור רכיב שני להצגת מתכונים אישיים. צור קובץ Recipe.jsx בתיקיית app/javascript/components:

  1. nano app/javascript/components/Recipe.jsx

כמו עם הרכיב Recipes, יבא את המודולים React, useState, useEffect, Link, useNavigate, ו־useParam על ידי הוספת השורות הבאות:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

לבסוף, הוסף את השורות שמודגשות ליצירת רכיב React פונקציונלי בשם Recipe וייצא אותו:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

const Recipe = () => {
  const params = useParams();
  const navigate = useNavigate();
  const [recipe, setRecipe] = useState({ ingredients: "" });
};

export default Recipe;

כמו ברכיב Recipes, אתה מאתחל את ניווט React Router באמצעות ה־useNavigate hook. יש לך משתנה recipe ופונקציה setRecipe שיעדכנו את המצב עם ה־useState hook. בנוסף, אתה קורא ל־useParams hook, שמחזיר אובייקט שבו שם/ערך הם פרמטרים של כתובת ה־URL.

כדי למצוא מתכון ספציפי, האפליקציה שלך צריכה לדעת את ה־id של המתכון, וזה אומר שהרכיב שלך Recipe מצפה ל־param בשם id ב־URL. תוכל לגשת אליו דרך אובייקט ה־params שמחזיק בערך המוחזר של ה־useParams hook.

הבא, הכרז על hook של useEffect בו תגיש ל- id param מאובייקט ה- params. לאחר שאתה משיג את ה- id param של המתכון, יש לך לבצע בקשת HTTP כדי לשלוף את המתכון. הוסף את השורות המודגשות הבאות לקובץ שלך:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

const Recipe = () => {
  const params = useParams();
  const navigate = useNavigate();
  const [recipe, setRecipe] = useState({ ingredients: "" });

  useEffect(() => {
    const url = `/api/v1/show/${params.id}`;
    fetch(url)
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((response) => setRecipe(response))
      .catch(() => navigate("/recipes"));
  }, [params.id]);
};

export default Recipe;

ב- useEffect hook, עליך להשתמש בערך ה- params.id כדי לבצע בקשת HTTP GET לשלוף את המתכון שברשות ה- id ולשמור אותו במצב הרכיב באמצעות הפונקציה setRecipe. האפליקציה מפנה את המשתמש לדף המתכונים אם המתכון אינו קיים.

בנוסף, הוסף פונקציה addHtmlEntities, שתשמש להחליף יריעות תווים עם יישות HTML ברכיב. פונקצית ה- addHtmlEntities תקבל מחרוזת ותחליף את כל הסוגריים פתוחות וסגורות של escape עם יישות ה- HTML שלהם. פונקציה זו תעזור לך להמיר את התו המבורר שנשמר בהוראות המתכון שלך. הוסף את השורות המודגשות הבאות:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

const Recipe = () => {
  const params = useParams();
  const navigate = useNavigate();
  const [recipe, setRecipe] = useState({ ingredients: "" });

  useEffect(() => {
    const url = `/api/v1/show/${params.id}`;
    fetch(url)
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((response) => setRecipe(response))
      .catch(() => navigate("/recipes"));
  }, [params.id]);

  const addHtmlEntities = (str) => {
    return String(str).replace(/&lt;/g, "<").replace(/&gt;/g, ">");
  };
};

export default Recipe;

לבסוף, החזר את הסימון כדי להציג את המתכון במצב הרכיב בעמוד על ידי הוספת השורות המודגשות הבאות:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

const Recipe = () => {
  const params = useParams();
  const navigate = useNavigate();
  const [recipe, setRecipe] = useState({ ingredients: "" });

  useEffect(() => {
    const url = `/api/v1/show/${params.id}`;
    fetch(url)
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((response) => setRecipe(response))
      .catch(() => navigate("/recipes"));
  }, [params.id]);

  const addHtmlEntities = (str) => {
    return String(str).replace(/&lt;/g, "<").replace(/&gt;/g, ">");
  };

  const ingredientList = () => {
    let ingredientList = "No ingredients available";

    if (recipe.ingredients.length > 0) {
      ingredientList = recipe.ingredients
        .split(",")
        .map((ingredient, index) => (
          <li key={index} className="list-group-item">
            {ingredient}
          </li>
        ));
    }

    return ingredientList;
  };

  const recipeInstruction = addHtmlEntities(recipe.instruction);
  
  return (
    <div className="">
      <div className="hero position-relative d-flex align-items-center justify-content-center">
        <img
          src={recipe.image}
          alt={`${recipe.name} image`}
          className="img-fluid position-absolute"
        />
        <div className="overlay bg-dark position-absolute" />
        <h1 className="display-4 position-relative text-white">
          {recipe.name}
        </h1>
      </div>
      <div className="container py-5">
        <div className="row">
          <div className="col-sm-12 col-lg-3">
            <ul className="list-group">
              <h5 className="mb-2">Ingredients</h5>
              {ingredientList()}
            </ul>
          </div>
          <div className="col-sm-12 col-lg-7">
            <h5 className="mb-2">Preparation Instructions</h5>
            <div
              dangerouslySetInnerHTML={{
                __html: `${recipeInstruction}`,
              }}
            />
          </div>
          <div className="col-sm-12 col-lg-2">
            <button
              type="button"
              className="btn btn-danger"
            >
              Delete Recipe
            </button>
          </div>
        </div>
        <Link to="/recipes" className="btn btn-link">
          Back to recipes
        </Link>
      </div>
    </div>
  );
};

export default Recipe;

עם פונקציית ingredientList, אתה מפריד את רשימת המרכיבים של המתכון שלך שמופרדים בפסיקים למערך ומפענח אותו על מנת ליצור רשימת מרכיבים. אם אין מרכיבים, האפליקציה מציגה הודעה שאומרת אין מרכיבים זמינים. אתה גם מחליף את כל הסוגריים הפותחים והסוגריים הסוגרים בהוראות המתכון על ידי עברתו דרך הפונקציה addHtmlEntities. לבסוף, הקוד מציג את תמונת המתכון כתמונת גיבור, מוסיף לו ליד הוראות המתכון לחצן מחק מתכון ומוסיף כפתור המפנה חזרה לדף המתכונים.

הערה: שימוש במאפיין dangerouslySetInnerHTML של React הוא פוטנציאלית מסוכן מכיוון שהוא חושף את האפליקציה שלך לתקיפות סקריפטים חצי מצב. הסיכון נמוך על ידי הבטחת החלפת התווים המיוחדים שנכנסים בעת יצירת מתכונים באמצעות פונקציית stripHtmlEntities שנמצאת ברכיב NewRecipe.

שמור וצא מהקובץ.

כדי להציג את רכיב Recipe בעמוד, עליך להוסיף אותו לקובץ הנתיבים שלך. פתח את קובץ הנתיבים שלך לעריכה:

  1. nano app/javascript/routes/index.jsx

הוסף את השורות המודגשות הבאות לקובץ:

~/rails_react_recipe/app/javascript/routes/index.jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";

export default (
  <Router>
    <Routes>
      <Route path="/" exact component={Home} />
      <Route path="/recipes" exact component={Recipes} />
      <Route path="/recipe/:id" element={<Recipe />} />
    </Routes>
  </Router>
);

אתה מייבא את רכיב ה-Recipe שלך בקובץ זה ומוסיף נתיב. הנתיב שלו מכיל param שיחליף את ה-id של המתכון שברצונך להציג.

שמור וסגור את הקובץ.

השתמש בסקריפט bin/dev כדי להתחיל שוב את השרת שלך, ואז בקר בכתובת http://localhost:3000 בדפדפן שלך. לחץ על כפתור צפה במתכונים כדי לנווט לעמוד המתכונים. בעמוד המתכונים, גש לכל מתכון על ידי לחיצה על כפתור צפה במתכון. תתקבל בעמוד המזוהה בנתונים ממסד הנתונים שלך:

ניתן לעצור את השרת עם CTRL+C.

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

שלב 8 — יצירת מתכונים

השלב הבא ליצירת אפליקצית מתכונים שימושית הוא היכולת ליצור מתכונים חדשים. בשלב זה, תיצור רכיב עבור תכונה זו. הרכיב יכיל טופס לאיסוף הפרטים הנדרשים למתכון מהמשתמש ולאחר מכן לבצע בקשה לפעולת create בבקר Recipe כדי לשמור את נתוני המתכון.

צור קובץ NewRecipe.jsx בתיקיית app/javascript/components:

  1. nano app/javascript/components/NewRecipe.jsx

בקובץ החדש, ייבא את המודולים React, useState, Link, ו־useNavigate שהשתמשת בהם ברכיבים אחרים:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

לבסוף, צור וייצא רכיב פונקציונלי NewRecipe על ידי הוספת השורות שמסומנות:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

const NewRecipe = () => {
  const navigate = useNavigate();
  const [name, setName] = useState("");
  const [ingredients, setIngredients] = useState("");
  const [instruction, setInstruction] = useState("");
};

export default NewRecipe;

כמו עם רכיבים קודמים, אתה מאתחל את ניוויגציה ה-React Router עם ה- hook useNavigate, ואז אתה משתמש ב- hook useState כדי לאתחל מצב name, ingredients, ו- instruction, כל אחד עם פונקציות העדכון המתאימות שלהם. אלו השדות שתצטרך ליצירת מתכון תקין.

לאחר מכן, צור פונקציה stripHtmlEntities שתמיר תווים מיוחדים (כמו <) לערכים המוצפנים שלהם (כמו &lt;), בהתאם. כדי לעשות זאת, הוסף את השורות המודגשות לרכיב NewRecipe:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

const NewRecipe = () => {
  const navigate = useNavigate();
  const [name, setName] = useState("");
  const [ingredients, setIngredients] = useState("");
  const [instruction, setInstruction] = useState("");

  const stripHtmlEntities = (str) => {
    return String(str)
      .replace(/\n/g, "<br> <br>")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
  };
};

export default NewRecipe;

בפונקציה stripHtmlEntities, החלף את התווים < ו- > בערכים המוצפנים שלהם. בכך אתה מבטיח שלא תאחסן HTML גולמי במסד הנתונים שלך.

לאחר מכן, הוסף את השורות המודגשות כדי להוסיף את הפונקציות onChange ו- onSubmit לרכיב NewRecipe כדי לטפל בעריכה והגשה של הטופס:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

const NewRecipe = () => {
  const navigate = useNavigate();
  const [name, setName] = useState("");
  const [ingredients, setIngredients] = useState("");
  const [instruction, setInstruction] = useState("");

  const stripHtmlEntities = (str) => {
    return String(str)
      .replace(/\n/g, "<br> <br>")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
  };

  const onChange = (event, setFunction) => {
    setFunction(event.target.value);
  };

  const onSubmit = (event) => {
    event.preventDefault();
    const url = "/api/v1/recipes/create";

    if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
      return;

    const body = {
      name,
      ingredients,
      instruction: stripHtmlEntities(instruction),
    };

    const token = document.querySelector('meta[name="csrf-token"]').content;
    fetch(url, {
      method: "POST",
      headers: {
        "X-CSRF-Token": token,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    })
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((response) => navigate(`/recipe/${response.id}`))
      .catch((error) => console.log(error.message));
  };
};

export default NewRecipe;

הפונקציה onChange מקבלת את קלט המשתמש event ואת פונקציית הסטייט, ואז מעדכנת את הסטייט בערך שנקלט מהמשתמש. בפונקציה onSubmit, אתה בודק שאף אחד מהקלטים הנדרשים אינם ריקים. לאחר מכן, אתה בונה אובייקט המכיל את הפרמטרים הנדרשים ליצירת מתכון חדש. באמצעות הפונקציה stripHtmlEntities, אתה מחליף את התווים < ו־> בערך המוחזר שלהם באמצעות קווים אחוריים, ומחליף כל תו שורה חדשה בתגית של שבירה, וכך שומר על התבנית הטקסטואלית שהוזנה על ידי המשתמש. לבסוף, אתה שולח בקשת HTTP POST כדי ליצור את המתכון החדש ומפנה לדף שלו במקרה של תגובה מוצלחת.

כדי להגן מפני התקפות של גניבת בקשת מקור-צל (CSRF), Rails מצרפת טוקן אבטחה למסמך HTML. הטוקן הזה דרוש כאשר בוצעת בקשה שאינה מסוג GET. עם הקבוע token בקוד שלך, היישום שלך מאמת את הטוקן בשרת ומזריק חריגה אם הטוקן האבטחה לא תואם את הערך שציפית. בפונקציה onSubmit, היישום מביא את הטוקן CSRF המשובץ במסמך HTML שלך על ידי Rails ואז מבצע בקשת HTTP עם מחרוזת JSON. אם המתכון נוצר בהצלחה, היישום מפנה את המשתמש לדף המתכון כאשר הוא יכול להציג את המתכון החדש שנוצר.

לבסוף, החזר את הסימון המצייר טופס שבו המשתמש יכול להזין את הפרטים למתכון שהוא רוצה ליצור. הוסף את השורות המודגשות:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

const NewRecipe = () => {
  const navigate = useNavigate();
  const [name, setName] = useState("");
  const [ingredients, setIngredients] = useState("");
  const [instruction, setInstruction] = useState("");

  const stripHtmlEntities = (str) => {
    return String(str)
      .replace(/\n/g, "<br> <br>")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
  };

  const onChange = (event, setFunction) => {
    setFunction(event.target.value);
  };

  const onSubmit = (event) => {
    event.preventDefault();
    const url = "/api/v1/recipes/create";

    if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
      return;

    const body = {
      name,
      ingredients,
      instruction: stripHtmlEntities(instruction),
    };

    const token = document.querySelector('meta[name="csrf-token"]').content;
    fetch(url, {
      method: "POST",
      headers: {
        "X-CSRF-Token": token,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    })
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((response) => navigate(`/recipe/${response.id}`))
      .catch((error) => console.log(error.message));
  };

  return (
    <div className="container mt-5">
      <div className="row">
        <div className="col-sm-12 col-lg-6 offset-lg-3">
          <h1 className="font-weight-normal mb-5">
            Add a new recipe to our awesome recipe collection.
          </h1>
          <form onSubmit={onSubmit}>
            <div className="form-group">
              <label htmlFor="recipeName">Recipe name</label>
              <input
                type="text"
                name="name"
                id="recipeName"
                className="form-control"
                required
                onChange={(event) => onChange(event, setName)}
              />
            </div>
            <div className="form-group">
              <label htmlFor="recipeIngredients">Ingredients</label>
              <input
                type="text"
                name="ingredients"
                id="recipeIngredients"
                className="form-control"
                required
                onChange={(event) => onChange(event, setIngredients)}
              />
              <small id="ingredientsHelp" className="form-text text-muted">
                Separate each ingredient with a comma.
              </small>
            </div>
            <label htmlFor="instruction">Preparation Instructions</label>
            <textarea
              className="form-control"
              id="instruction"
              name="instruction"
              rows="5"
              required
              onChange={(event) => onChange(event, setInstruction)}
            />
            <button type="submit" className="btn custom-button mt-3">
              Create Recipe
            </button>
            <Link to="/recipes" className="btn btn-link mt-3">
              Back to recipes
            </Link>
          </form>
        </div>
      </div>
    </div>
  );
};

export default NewRecipe;

הסימון שהוחזר כולל טופס המכיל שלוש שדות קלט: אחד לשם המתכון, אחד לרשימת הרכיבים, ואחד להוראות. לכל שדה קלט יש סדרת פעולה onChange הקוראת לפונקציה onChange. טיפול באירוע onSubmit מחובר גם לכפתור השליחה וקורא לפונקציה onSubmit ששולחת את נתוני הטופס.

שמור וצא מהקובץ.

כדי לגשת לרכיב זה בדפדפן, עדכן את קובץ הנתיב שלך עם הנתיב שלו:

  1. nano app/javascript/routes/index.jsx

עדכן את קובץ הנתיב שלך כדי לכלול את השורות המודגשות הללו:

~/rails_react_recipe/app/javascript/routes/index.jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";
import NewRecipe from "../components/NewRecipe";

export default (
  <Router>
    <Routes>
      <Route path="/" exact component={Home} />
      <Route path="/recipes" exact component={Recipes} />
      <Route path="/recipe/:id" exact component={Recipe} />
      <Route path="/recipe" element={<NewRecipe />} />
    </Routes>
  </Router>
);

עם הנתיב במקום, שמור וצא מהקובץ שלך.

אתחל את שרת הפיתוח שלך ובקר בכתובת http://localhost:3000 בדפדפן שלך. נווט אל עמוד המתכונים ולחץ על הכפתור צור מתכון חדש. תמצא עמוד עם טופס להוספת מתכונים לבסיס הנתונים שלך:

הזן את פרטי המתכון הנדרשים ולחץ על הכפתור צור מתכון. המתכון שנוצר יופיע אז על העמוד. כשאתה מוכן, סגור את השרת.

בשלב זה, הוספת את היכולת ליצירת מתכונים לאפליקציית המתכונים שלך. בשלב הבא, תוסיף את היכולת למחוק מתכונים.

שלב 9 — מחיקת מתכונים

במקטע זה, תשנה את רכיב המתכון שלך כך שיכלול אפשרות למחיקת מתכונים. כאשר תלחץ על כפתור המחיקה בדף המתכון, היישום ישלח בקשה למחיקת מתכון מהמסד נתונים.

תחילה, פתח את קובץ ה־Recipe.jsx שלך לעריכה:

  1. nano app/javascript/components/Recipe.jsx

ברכיב ה־Recipe, הוסף פונקציה deleteRecipe עם השורות המודגשות:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

const Recipe = () => {
  const params = useParams();
  const navigate = useNavigate();
  const [recipe, setRecipe] = useState({ ingredients: "" });

  useEffect(() => {
    const url = `/api/v1/show/${params.id}`;
    fetch(url)
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((response) => setRecipe(response))
      .catch(() => navigate("/recipes"));
  }, [params.id]);

  const addHtmlEntities = (str) => {
    return String(str).replace(/&lt;/g, "<").replace(/&gt;/g, ">");
  };

  const deleteRecipe = () => {
    const url = `/api/v1/destroy/${params.id}`;
    const token = document.querySelector('meta[name="csrf-token"]').content;

    fetch(url, {
      method: "DELETE",
      headers: {
        "X-CSRF-Token": token,
        "Content-Type": "application/json",
      },
    })
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(() => navigate("/recipes"))
      .catch((error) => console.log(error.message));
  };

  const ingredientList = () => {
    let ingredientList = "No ingredients available";

    if (recipe.ingredients.length > 0) {
      ingredientList = recipe.ingredients
        .split(",")
        .map((ingredient, index) => (
          <li key={index} className="list-group-item">
            {ingredient}
          </li>
        ));
    }

    return ingredientList;
  };

  const recipeInstruction = addHtmlEntities(recipe.instruction);

  return (
    <div className="">
...

בפונקציית deleteRecipe, אתה מקבל את id של המתכון שיש למחוק, מבנה את כתובת ה־URL שלך ומשיג את אסימון ה־CSRF. לאחר מכן, אתה בוחן בקשת DELETE לבקר המתכונים כדי למחוק את המתכון. היישום מפנה את המשתמש לדף המתכונים אם המתכון נמחק בהצלחה.

כדי להריץ את הקוד בפונקציית deleteRecipe בכל פעם שנלחץ כפתור המחיקה, עבור אותה כמנהל אירועי לחיצה לכפתור. הוסף אירוע onClick לאלמנט כפתור המחיקה ברכיב:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
...
return (
    <div className="">
      <div className="hero position-relative d-flex align-items-center justify-content-center">
        <img
          src={recipe.image}
          alt={`${recipe.name} image`}
          className="img-fluid position-absolute"
        />
        <div className="overlay bg-dark position-absolute" />
        <h1 className="display-4 position-relative text-white">
          {recipe.name}
        </h1>
      </div>
      <div className="container py-5">
        <div className="row">
          <div className="col-sm-12 col-lg-3">
            <ul className="list-group">
              <h5 className="mb-2">Ingredients</h5>
              {ingredientList()}
            </ul>
          </div>
          <div className="col-sm-12 col-lg-7">
            <h5 className="mb-2">Preparation Instructions</h5>
            <div
              dangerouslySetInnerHTML={{
                __html: `${recipeInstruction}`,
              }}
            />
          </div>
          <div className="col-sm-12 col-lg-2">
            <button
              type="button"
              className="btn btn-danger"
              onClick={deleteRecipe}
            >
              Delete Recipe
            </button>
          </div>
        </div>
        <Link to="/recipes" className="btn btn-link">
          Back to recipes
        </Link>
      </div>
    </div>
  );
...

בנקודה זו במדריך, הקובץ השלם שלך Recipe.jsx צריך להיות תואם לקובץ זה:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

const Recipe = () => {
  const params = useParams();
  const navigate = useNavigate();
  const [recipe, setRecipe] = useState({ ingredients: "" });

  useEffect(() => {
    const url = `/api/v1/show/${params.id}`;
    fetch(url)
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then((response) => setRecipe(response))
      .catch(() => navigate("/recipes"));
  }, [params.id]);

  const addHtmlEntities = (str) => {
    return String(str).replace(/&lt;/g, "<").replace(/&gt;/g, ">");
  };

  const deleteRecipe = () => {
    const url = `/api/v1/destroy/${params.id}`;
    const token = document.querySelector('meta[name="csrf-token"]').content;

    fetch(url, {
      method: "DELETE",
      headers: {
        "X-CSRF-Token": token,
        "Content-Type": "application/json",
      },
    })
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(() => navigate("/recipes"))
      .catch((error) => console.log(error.message));
  };

  const ingredientList = () => {
    let ingredientList = "No ingredients available";

    if (recipe.ingredients.length > 0) {
      ingredientList = recipe.ingredients
        .split(",")
        .map((ingredient, index) => (
          <li key={index} className="list-group-item">
            {ingredient}
          </li>
        ));
    }

    return ingredientList;
  };

  const recipeInstruction = addHtmlEntities(recipe.instruction);

  return (
    <div className="">
      <div className="hero position-relative d-flex align-items-center justify-content-center">
        <img
          src={recipe.image}
          alt={`${recipe.name} image`}
          className="img-fluid position-absolute"
        />
        <div className="overlay bg-dark position-absolute" />
        <h1 className="display-4 position-relative text-white">
          {recipe.name}
        </h1>
      </div>
      <div className="container py-5">
        <div className="row">
          <div className="col-sm-12 col-lg-3">
            <ul className="list-group">
              <h5 className="mb-2">Ingredients</h5>
              {ingredientList()}
            </ul>
          </div>
          <div className="col-sm-12 col-lg-7">
            <h5 className="mb-2">Preparation Instructions</h5>
            <div
              dangerouslySetInnerHTML={{
                __html: `${recipeInstruction}`,
              }}
            />
          </div>
          <div className="col-sm-12 col-lg-2">
            <button
              type="button"
              className="btn btn-danger"
              onClick={deleteRecipe}
            >
              Delete Recipe
            </button>
          </div>
        </div>
        <Link to="/recipes" className="btn btn-link">
          Back to recipes
        </Link>
      </div>
    </div>
  );
};

export default Recipe;

שמור וצא מהקובץ.

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

עם פעולת המחיקה עובדת, כעת יש לך אפליקציית מתכונים פועלת לחלוטין!

סיכום

במדריך זה, יצרת אפליקציה למתכונים עם Ruby on Rails וממשק משתמש ב-React, בשילוב של PostgreSQL כמסד נתונים ו-Bootstrap לעיצוב. אם ברצונך להמשיך לבנות עם Ruby on Rails, שקול לעקוב אחרי המדריך שלנו ל איך לאבטח תקשורת באמצעות SSH Tunnels באפליקציית Rails בשלושה שלבים או לבקר בסדרת המדריכים שלנו ל כיצד לכתוב ב-Ruby כדי לרענן את המיומנויות שלך ב-Ruby. כדי לחקוף עמוק יותר ב-React, נסה את המדריך שלנו ל איך להציג נתונים מ-API של DigitalOcean באמצעות React.

Source:
https://www.digitalocean.com/community/tutorials/how-to-set-up-a-ruby-on-rails-v7-project-with-a-react-frontend-on-ubuntu-20-04