اختار الكاتب مؤسسة الحدود الإلكترونية لتلقي تبرع كجزء من برنامج كتابة من أجل التبرعات.
المقدمة
روبي على القضبان هو إطار عمل شهير لتطبيقات الويب من الخادم. يشغل العديد من التطبيقات الشهيرة الموجودة على الويب اليوم، مثل GitHub، Basecamp، SoundCloud، Airbnb، و Twitch. بفضل تركيزه على تجربة المبرمج والمجتمع المتحمس المبني حوله، سيوفر لك روبي على القضبان الأدوات التي تحتاجها لبناء وصيانة تطبيق الويب الحديث الخاص بك.
رياكت هو مكتبة JavaScript تستخدم لإنشاء واجهات المستخدم الأمامية. بدعم من فيسبوك، إنها واحدة من أكثر المكتبات الأمامية شيوعًا المستخدمة على الويب اليوم. تقدم رياكت ميزات مثل نموذج الكائن الظاهري (DOM)، هندسة المكونات، و إدارة الحالة، مما يجعل عملية تطوير الواجهة الأمامية أكثر تنظيمًا وفعالية.
باقتراب واجهة الويب الأمامية نحو إطارات عمل منفصلة عن كود الخادم، ستمكنك دمج أناقة Rails مع كفاءة React من بناء تطبيقات قوية وحديثة تستفيد من التوجهات الحالية. باستخدام React لعرض المكونات من داخل عرض Rails (بدلاً من محرك القوالب Rails)، سيستفيد تطبيقك من أحدث التطورات في جافا سكريبت وتطوير الواجهة الأمامية بينما يستغل تعبيرية Ruby on Rails.
في هذا البرنامج التعليمي، ستقوم بإنشاء تطبيق Ruby on Rails يخزن وصفاتك المفضلة ثم يعرضها بواجهة أمامية React. عند الانتهاء، ستتمكن من إنشاء، عرض، وحذف الوصفات باستخدام واجهة React مُنسّقة بـ
Bootstrap:
المتطلبات المسبقة
لمتابعة هذا البرنامج التعليمي، تحتاج:
-
تم تثبيت Node.js و npm على جهاز التطوير الخاص بك. يستخدم هذا البرنامج التعليمي إصدار Node.js 16.14.0 وإصدار npm 8.3.1. Node.js هو بيئة تشغيل JavaScript تسمح لك بتشغيل الشيفرة الخاصة بك خارج المتصفح. يأتي مع مدير حزم مثبت مسبقًا يسمى npm, الذي يتيح لك تثبيت الحزم وتحديثها. لتثبيت هذه الأدوات على Ubuntu 20.04 أو macOS, اتبع قسم “التثبيت باستخدام PPA” في كيفية تثبيت Node.js على Ubuntu 20.04 أو الخطوات الموجودة في كيفية تثبيت Node.js وإنشاء بيئة تطوير محلية على macOS.
-
مدير حزم Yarn مثبت على جهاز التطوير الخاص بك، الذي سيتيح لك تنزيل إطار العمل React. تم اختبار هذا البرنامج التعليمي على الإصدار 1.22.10؛ لتثبيت هذا التبعية، اتبع دليل تثبيت Yarn الرسمي.
-
تم تثبيت Ruby on Rails. للحصول على ذلك ، اتبع دليلنا على كيفية تثبيت Ruby on Rails مع rbenv على Ubuntu 20.04. إذا كنت ترغب في تطوير هذا التطبيق على macOS ، يمكنك استخدام كيفية تثبيت Ruby on Rails مع rbenv على macOS. تم اختبار هذا البرنامج التعليمي على الإصدار 3.1.2 من Ruby والإصدار 7.0.4 من Rails ، لذا تأكد من تحديد هذه الإصدارات أثناء عملية التثبيت.
ملاحظة: إصدار Rails 7 ليس متوافقًا مع الإصدارات السابقة. إذا كنت تستخدم إصدار Rails 5 ، يرجى زيارة البرنامج التعليمي لـ كيفية إعداد مشروع Ruby on Rails v5 مع واجهة أمامية React على Ubuntu 18.04.
- تم تثبيت PostgreSQL، كما هو موضح في الخطوات 1 و 2 كيفية استخدام PostgreSQL مع تطبيق Ruby on Rails الخاص بك على Ubuntu 20.04 أو كيفية استخدام PostgreSQL مع تطبيق Ruby on Rails الخاص بك على macOS. لمتابعة هذا البرنامج التعليمي، يمكنك استخدام إصدار PostgreSQL 12 أو أحدث. إذا كنت ترغب في تطوير هذا التطبيق على توزيع Linux مختلف أو نظام تشغيل آخر، انظر صفحة تنزيلات PostgreSQL الرسمية. للمزيد من المعلومات حول كيفية استخدام PostgreSQL، انظر كيفية تثبيت واستخدام PostgreSQL.
الخطوة 1 — إنشاء تطبيق Rails جديد
سوف تقوم ببناء تطبيق الوصفات الخاص بك على إطار تطبيقات Rails في هذه الخطوة. أولاً، ستقوم بإنشاء تطبيق Rails جديد، الذي سيتم تكوينه للعمل مع React.
توفر Rails عدة نصوص تسمى مولدات تقوم بإنشاء كل ما يلزم لبناء تطبيق ويب حديث. لمراجعة قائمة كاملة من هذه الأوامر وما تفعله، قم بتشغيل الأمر التالي في الطرفية الخاصة بك:
- rails -h
تعتبر هذه الأمر ستنتج قائمة شاملة من الخيارات، مما يتيح لك تحديد معلمات تطبيقك. أحد الأوامر المدرجة هو أمر new
، الذي يقوم بإنشاء تطبيق Rails جديد.
الآن، ستقوم بإنشاء تطبيق Rails جديد باستخدام مولد new
. قم بتشغيل الأمر التالي في الطرفية الخاصة بك:
- rails new rails_react_recipe -d postgresql -j esbuild -c bootstrap -T
الأمر السابق يقوم بإنشاء تطبيق Rails جديد في دليل يحمل اسم rails_react_recipe
، ويقوم بتثبيت التبعيات اللازمة للروبي والجافا سكريبت، ويكون تكوين Webpack. العلامات المرتبطة بهذا الأمر الأمر new
تتضمن ما يلي:
- تحديد العلامة
-d
المحرك المفضل لقاعدة البيانات، والذي هو في هذه الحالة PostgreSQL. - تحديد العلامة
-j
نهج الجافا سكريبت للتطبيق. يقدم Rails بعض الطرق المختلفة للتعامل مع رمز جافا سكريبت في تطبيقات Rails. الخيارesbuild
الذي يتم تمريره إلى العلامة-j
يوجه Rails لتهيئة esbuild كمجمع جافا سكريبت المفضل. - تحديد العلامة
-c
معالج CSS للتطبيق. Bootstrap هو الخيار المفضل في هذه الحالة. - توجيه العلامة
-T
Rails لتخطي إنشاء ملفات الاختبار لأنك لن تكتب اختبارات لهذا البرنامج التعليمي. يُقترح هذا الأمر أيضًا إذا كنت ترغب في استخدام أداة اختبار Ruby مختلفة عن تلك التي يوفرها Rails.
بمجرد انتهاء الأمر، انتقل إلى الدليل rails_react_recipe
، وهو الدليل الرئيسي لتطبيقك:
- cd rails_react_recipe
ثم، قم بسرد محتويات الدليل:
- ls
OutputGemfile 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
لتحديد الدور الذي ترغب في أن يستخدم Rails لإنشاء قاعدة البيانات الخاصة بك. خلال الخطوات الأولية، قمت بإنشاء دور محمي بكلمة مرور في الدورة التعليمية كيفية استخدام PostgreSQL مع تطبيق Ruby on Rails الخاص بك. إذا لم تقم بتحديد المستخدم بعد، يمكنك الآن اتباع التعليمات في الخطوة 4 — تكوين وإنشاء قاعدة البيانات الخاصة بك في نفس دورة الأولية.
تقدم Rails العديد من الأوامر التي تجعل تطوير تطبيقات الويب سهلة، بما في ذلك الأوامر للعمل مع قواعد البيانات مثل create
، drop
، و reset
. لإنشاء قاعدة بيانات لتطبيقك، قم بتشغيل الأمر التالي في الطرفية الخاصة بك:
- rails db:create
يقوم هذا الأمر بإنشاء قاعدة بيانات development
و test
، مما يؤدي إلى الناتج التالي:
OutputCreated database 'rails_react_recipe_development'
Created database 'rails_react_recipe_test'
الآن بمجرد أن يكون التطبيق متصلاً بقاعدة بيانات، قم بتشغيل التطبيق عبر تشغيل الأمر التالي:
- bin/dev
توفر Rails نصًا بديلاً bin/dev
يبدأ تطبيق Rails عن طريق تنفيذ الأوامر في ملف Procfile.dev
في دليل التطبيق الجذر باستخدام الجوهرة Foreman.
بمجرد تشغيل هذا الأمر، سيختفي موجه الأوامر الخاصة بك، وسيظهر في مكانه الناتج التالي:
Outputstarted 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
. سيتم تحميل صفحة الترحيب الافتراضية لـ Rails، مما يعني أنك قمت بتكوين تطبيق Rails الخاص بك بشكل صحيح:
لإيقاف خادم الويب، اضغط 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
ثم سيعود سطر الأوامر الخاص بك.
لقد نجحت في تكوين قاعدة بيانات لتطبيق وصفات الطعام الخاص بك. في الخطوة التالية، ستقوم بتثبيت الاعتماديات الجافا سكريبت التي تحتاجها لتجميع واجهة الواجهة الأمامية لك.
الخطوة 3 — تثبيت اعتماديات الواجهة الأمامية
في هذه الخطوة، ستقوم بتثبيت الاعتماديات الجافا سكريبت اللازمة على الواجهة الأمامية لتطبيق وصفات الطعام الخاص بك. وتشمل:
- React لبناء واجهات المستخدم.
- React DOM لتمكين React من التفاعل مع DOM المتصفح.
- React Router للتعامل مع التنقل في تطبيق React.
قم بتشغيل الأمر التالي لتثبيت هذه الحزم باستخدام مدير الحزم Yarn:
- yarn add react react-dom react-router-dom
هذا الأمر يستخدم Yarn لتثبيت الحزم المحددة ويضيفها إلى ملف package.json
. للتحقق من ذلك، قم بفتح ملف package.json
الموجود في الدليل الرئيسي للمشروع:
- nano package.json
سيتم سرد الحزم المثبتة تحت مفتاح dependencies
:
{
"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، يكمن هدف المتحكم في استقبال الطلبات المحددة وتمريرها إلى النموذج أو العرض المناسب. يعرض التطبيق حاليًا صفحة ترحيب Rails عند تحميل عنوان URL الأساسي في المتصفح. لتغيير هذا، ستقوم بإنشاء متحكم وعرض للصفحة الرئيسية ثم تطابقه مع مسار.
يوفر Rails مولد controller
لإنشاء متحكم. يستقبل مولد controller
اسم المتحكم والإجراء المطابق. لمزيد من المعلومات حول هذا، يمكنك مراجعة وثائق Rails.
سيتم تسمية المتحكم في هذا البرنامج التعليمي بـ Homepage
. قم بتشغيل الأمر التالي لإنشاء متحكم Homepage
مع إجراء index
:
- rails g controller Homepage index
ملاحظة:
على نظام Linux، قد ينتج عن الخطأ FATAL: Listen error: unable to monitor directories for changes.
قيود النظام على عدد الملفات التي يمكن للجهاز رصدها للتغييرات قد تكون السبب. قم بتشغيل الأمر التالي لإصلاحه:
- 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 theindex
action you specified in the command. - A
homepage_helper.rb
file for adding helper methods related to theHomepage
controller. - ملف
index.html.erb
كصفحة عرض لأي شيء متعلق بالصفحة الرئيسية.
بالإضافة إلى هذه الصفحات الجديدة التي تم إنشاؤها بتشغيل أمر Rails، يقوم 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:
- nano config/routes.rb
في هذا الملف، قم بتعويض get 'homepage/index'
بـ root 'homepage#index'
حتى يتطابق الملف مع ما يلي:
Rails.application.routes.draw do
root 'homepage#index'
# للحصول على تفاصيل حول لغة التوجيه المتاحة في هذا الملف، انظر إلى http://guides.rubyonrails.org/routing.html
end
تقوم هذه التعديلات بتوجيه Rails لمطابقة الطلبات الموجهة إلى جذر التطبيق إلى إجراء index
في تحكم Homepage
، الذي بدوره يقوم بعرض محتوى ملف index.html.erb
الذي يقع في app/views/homepage/index.html.erb
على المتصفح.
احفظ وأغلق الملف.
للتحقق من أن هذا يعمل، قم بتشغيل تطبيقك:
- bin/dev
عند فتح التطبيق في المتصفح أو تحديثه، سيتم تحميل صفحة هبوط جديدة لتطبيقك:
بمجرد التحقق من أن تطبيقك يعمل، اضغط على CTRL+C
لإيقاف الخادم.
بعد ذلك، افتح ملف ~/rails_react_recipe/app/views/homepage/index.html.erb
:
- 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
. للقيام بذلك، ابدأ بإنشاء دليل للمكونات في الدليل app/javascript
:
- mkdir ~/rails_react_recipe/app/javascript/components
سيحتوي دليل components
على المكون للصفحة الرئيسية، إلى جانب المكونات الأخرى لتطبيق React، بما في ذلك ملف الدخول إلى تطبيق React.
ثم، افتح ملف application.js
الموجود في app/javascript/application.js
:
- nano ~/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
:
- nano ~/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
من React Router. يقوم مكون Link
بإنشاء رابط للتنقل من صفحة إلى أخرى. بعد ذلك، تقوم بإنشاء وتصدير مكون وظيفي يحتوي على بعض لغة Markup لصفحتك الرئيسية، مزودة بتنسيق باستخدام فئات Bootstrap.
احفظ وأغلق الملف.
بعد تحديد مكون Home
، ستقوم الآن بإعداد التوجيه باستخدام React Router. قم بإنشاء دليل routes
في دليل app/javascript
:
- mkdir ~/rails_react_recipe/app/javascript/routes
سيحتوي دليل routes
على بعض المسارات مع مكوناتها المقابلة. عند تحميل أي مسار محدد، سيقوم بتقديم مكونه المقابل إلى المتصفح.
في دليل routes
، قم بإنشاء ملف index.jsx
:
- nano ~/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
التي تسمح لك باستخدام React، بالإضافة إلى وحدات BrowserRouter
، Routes
، و Route
من React Router، والتي تساعدك معًا على التنقل من مسار إلى آخر. أخيرًا، تقوم بإستيراد مكون Home
الخاص بك، الذي سيتم تقديمه عندما يتطابق الطلب مع المسار الجذري (/
). عندما ترغب في إضافة صفحات إضافية إلى تطبيقك، يمكنك إعلان مسار في هذا الملف وتطابقه مع المكون الذي ترغب في تقديمه لتلك الصفحة.
احفظ وأغلق الملف.
لقد قمت الآن بإعداد التوجيه باستخدام React Router. لكي يكون React على علم بالمسارات المتاحة واستخدامها، يجب أن تكون المسارات متاحة في نقطة الدخول إلى التطبيق. لتحقيق ذلك، ستقوم بتقديم المسارات الخاصة بك في مكون سيتم تقديمه بواسطة React في ملف الدخول الخاص بالتطبيق.
قم بإنشاء ملف App.jsx
في دليل app/javascript/components
:
- nano ~/rails_react_recipe/app/javascript/components/App.jsx
أضف الكود التالي إلى ملف App.jsx
:
import React from "react";
import Routes from "../routes";
export default props => <>{Routes}</>;
في ملف App.jsx
، استورد React وملفات المسارات التي أنشأتها للتو. ثم قم بتصدير مكون لتقديم المسارات ضمن قطع. سيتم تقديم هذا المكون في نقطة دخول التطبيق، مما يجعل المسارات متاحة في كل مرة يتم تحميل التطبيق.
احفظ وأغلق الملف.
الآن بعد أن قمت بإعداد ملف App.jsx
، يمكنك تقديمه في ملف الدخول الخاص بك. قم بإنشاء ملف index.jsx
في الدليل components
:
- nano ~/rails_react_recipe/app/javascript/components/index.jsx
أضف الكود التالي إلى ملف index.js
:
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 />);
});
في سطور الاستيراد، استورد مكتبة React ووظيفة createRoot
من ReactDOM، ومكون App
الخاص بك. باستخدام وظيفة createRoot
من ReactDOM، قم بإنشاء عنصر جذري كعنصر div
يتم إضافته إلى الصفحة، وقم بتقديم مكون App
الخاص بك فيه. عند تحميل التطبيق، ستقوم React بتقديم محتوى مكون App
داخل عنصر div
على الصفحة.
احفظ وأغلق الملف.
أخيرًا، ستضيف بعض أنماط CSS إلى صفحة البداية الخاصة بك.
Translated text:
افتح ملف application.bootstrap.scss
في دليل ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
الخاص بك:
- nano ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
ثم، استبدل محتويات ملف 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
بإنشاء الهيكل الأساسي لصورة hero، أو صورة كبيرة على الصفحة الرئيسية لموقع الويب الخاص بك، والتي ستضيفها لاحقًا. بالإضافة إلى ذلك، custom-button.btn
ينسق الزر الذي سيستخدمه المستخدم للدخول إلى التطبيق.
مع أنماط CSS الخاصة بك في مكانها، قم بحفظ وإغلاق الملف.
ثم، أعد تشغيل خادم الويب الخاص بتطبيقك:
- bin/dev
ثم أعد تحميل التطبيق في متصفحك. ستظهر صفحة رئيسية جديدة تمامًا:
قم بإيقاف خادم الويب باستخدام CTRL+C
.
لقد قمت بتكوين تطبيقك لاستخدام React كواجهة أمامية في هذه الخطوة. في الخطوة التالية، ستقوم بإنشاء نماذج وتحكمات تمكنك من إنشاء وقراءة وتحديث وحذف الوصفات.
الخطوة 6 — إنشاء تحكم الوصفة والنموذج
الآن بعد إعداد واجهة المستخدم الأمامية باستخدام React لتطبيقك، ستقوم بإنشاء نموذج ومتحكم للوصفة. سيمثل نموذج الوصفة جدول قاعدة البيانات الذي يحتوي على معلومات حول وصفات المستخدم، بينما سيستقبل ويتعامل المتحكم مع الطلبات لإنشاء وقراءة وتحديث أو حذف الوصفات. عندما يطلب المستخدم وصفة، يستقبل المتحكم للوصفة هذا الطلب ويمرره إلى نموذج الوصفة، الذي يسترد البيانات المطلوبة من قاعدة البيانات. ثم يقوم النموذج بإرجاع بيانات الوصفة كاستجابة للمتحكم. وأخيرًا، يتم عرض هذه المعلومات في المتصفح.
ابدأ بإنشاء نموذج “الوصفة” باستخدام أمر “توليد النموذج” المقدم من قبل Rails وحدد اسم النموذج مع أعمدته وأنواع البيانات. قم بتشغيل الأمر التالي:
- rails generate model Recipe name:string ingredients:text instruction:text image:string
يوجّه الأمر السابق Rails لإنشاء نموذج “الوصفة” جنبًا إلى جنب مع عمود “الاسم” من نوع “سلسلة”، وعمود “المكونات” و”التعليمات” من نوع “نص”، وعمود “الصورة” من نوع “سلسلة”. في هذا البرنامج التعليمي، تم أطلاق اسم النموذج “الوصفة”، لأن النماذج في Rails تستخدم اسمًا مفردًا بينما تستخدم جداول قاعدة البيانات المقابلة أسماء جمعية.
تقوم عملية توليد النموذج بإنشاء ملفين وتطبع النتيجة التالية:
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”:
- nano ~/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
. بدون هذه الحقول الثلاثة، يعتبر الوصفة غير صالحة ولن تُحفظ في قاعدة البيانات.
احفظ وأغلق الملف.
لإنشاء جدول recipes
في قاعدة بياناتك باستخدام Rails، يجب عليك تشغيل عملية migration، وهو الطريقة لإجراء تغييرات على قاعدة بياناتك بشكل برمجي. لضمان أن العملية تعمل مع القاعدة التي أنشأتها، يجب عليك إجراء تغييرات على الملف 20221017220817_create_recipes.rb
.
افتح هذا الملف في محرر النصوص:
- nano ~/rails_react_recipe/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 آخر إذا كنت ترغب في استخدام صورة مختلفة.
مع هذه التغييرات، احفظ واخرج من الملف. الآن، أنت جاهز لتشغيل عملية التحرك وإنشاء جدولك. في الطرفية، قم بتشغيل الأمر التالي:
- rails db:migrate
تستخدم أمر ترحيل قاعدة البيانات لتشغيل التعليمات في ملف الترحيل الخاص بك. بمجرد أن ينجح الأمر في التشغيل بنجاح، ستتلقى إخراجًا مشابهًا لما يلي:
Output== 20190407161357 CreateRecipes: migrating ====================================
-- create_table(:recipes)
-> 0.0140s
== 20190407161357 CreateRecipes: migrated (0.0141s) ===========================
بعد وضع نموذج الوصفة الخاص بك في المكان، ستقوم بإنشاء تحكم الوصفات الخاص بك لإضافة المنطق لإنشاء وقراءة وحذف الوصفات. قم بتشغيل الأمر التالي:
- rails generate controller api/v1/Recipes index create show destroy --skip-template-engine --no-helper
في هذا الأمر، ستقوم بإنشاء تحكم “الوصفات” في دليل “api/v1” مع إجراءات “index” و “create” و “show” و “destroy”. سيتم التعامل مع إجراء “index” لجلب جميع وصفاتك؛ سيكون إجراء “create” مسؤولًا عن إنشاء وصفات جديدة؛ سيسترد إجراء “show” وصفة واحدة، وسيحتوي إجراء “destroy” على المنطق لحذف وصفة.
كما تمرر بعض العلامات لجعل التحكم أكثر خفة، بما في ذلك:
--skip-template-engine
، الذي يوجه Rails إلى تخطي إنشاء ملفات عرض Rails لأن React يتعامل مع احتياجات واجهة المستخدم الخاصة بك.--no-helper
، الذي يوجه Rails إلى تخطي إنشاء ملف مساعد لتحكمك.
يقوم تشغيل الأمر أيضًا بتحديث ملف الطرق الخاص بك بطريقة لكل إجراء في تحكم “الوصفات”.
عندما يتم تشغيل الأمر، سيقوم بطباعة إخراج مثل هذا:
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” في محرر النص الخاص بك:
- nano ~/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 لمسارات create
و destroy
بحيث يمكن أن يقوم بـ post
و delete
للبيانات. كما تعدل مسارات show
و destroy
بإضافة معلمة :id
إلى المسار. :id
سيحمل رقم التعريف للوصفة التي تريد قراءتها أو حذفها.
أضف مسارًا يلتقط كل الطلبات باستخدام get '/*path'
التي لا تتطابق مع المسارات الحالية إلى index
لوحدة تحكم homepage
. سيتعامل توجيه الواجهة الأمامية مع الطلبات غير المتعلقة بإنشاء أو قراءة أو حذف الوصفات.
احفظ واخرج من الملف.
لتقييم قائمة المسارات المتاحة في تطبيقك ، قم بتشغيل الأمر التالي:
- rails routes
تعرض تشغيل هذا الأمر قائمة طويلة من أنماط URI وأفعال الأفعال أو وحدات التحكم المطابقة لمشروعك.
فيما يلي ، ستقوم بإضافة المنطق للحصول على جميع الوصفات دفعة واحدة. تستخدم Rails مكتبة ActiveRecord للتعامل مع المهام المتعلقة بقاعدة البيانات مثل هذه. يربط ActiveRecord بين الفئات وجداول قاعدة البيانات العلائقية ويوفر واجهة برمجة تطبيق غنية للعمل معها.
للحصول على جميع الوصفات ، ستستخدم ActiveRecord لاستعلام جدول الوصفات واسترداد جميع الوصفات في قاعدة البيانات.
افتح ملف recipes_controller.rb
باستخدام الأمر التالي:
- nano ~/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 للتحقق وحفظ تفاصيل الوصفة المقدمة. قم بتحديث متحكم الوصفات الخاص بك بالأسطر المحددة التالية من الكود:
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
تخصيص جميع معلمات المتحكم المقدمة إلى النموذج دفعة واحدة. تجعل هذه الطريقة عملية إنشاء السجلات سهلة، ولكنها تفتح الباب أمام احتمال استخدام خبيث. يمكن تجنب الاستخدام الخبيث باستخدام ميزة strong parameters المقدمة من Rails. بهذه الطريقة، لا يمكن تخصيص المعلمات ما لم يتم السماح بها. يتم تمرير معلمة recipe_params
إلى طريقة create
في الشيفرة الخاصة بك. recipe_params
هي طريقة private
حيث تسمح لمعلمات المتحكم بمنع دخول محتوى خاطئ أو خبيث إلى قاعدة البيانات الخاصة بك. في هذه الحالة، تسمح بمعلمات name
، image
، ingredients
، و instruction
لاستخدام صحيح للطريقة create
.
يمكن لمتحكم الوصفة الآن قراءة وإنشاء وصفات. كل ما تبقى هو منطق القراءة والحذف لوصفة واحدة. قم بتحديث متحكم الوصفات الخاص بك بالشيفرة المظللة:
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
المقدم في params
وتخصيصها لمتغير مثيل @recipe
. في إجراء show
، تُرجع كائن @recipe
المُعد بواسطة طريقة set_recipe
كاستجابة JSON.
في إجراء destroy
، قمت بشيء مشابه باستخدام عامل التشغيل الآمن &.
من Ruby، الذي يتجنب أخطاء nil
عند استدعاء طريقة. هذه الإضافة تتيح لك حذف وصفة طعام فقط إذا كانت موجودة، ثم إرسال رسالة كاستجابة.
بعد إجراء هذه التغييرات على recipes_controller.rb
، احفظ الملف وأغلقه.
في هذه الخطوة، قمت بإنشاء نموذج ومتحكم لوصفات الطعام الخاصة بك. لقد كتبت كل المنطق اللازم للعمل مع وصفات الطعام في الخلفية. في القسم التالي، ستقوم بإنشاء مكونات لعرض وصفات الطعام.
الخطوة 7 — عرض الوصفات
في هذا القسم، ستقوم بإنشاء مكونات لعرض الوصفات. ستقوم بإنشاء صفحتين: واحدة لعرض جميع الوصفات الموجودة وأخرى لعرض وصفات فردية.
ستبدأ بإنشاء صفحة لعرض جميع الوصفات. قبل إنشاء الصفحة ، تحتاج إلى وصفات للعمل معها ، حيث أن قاعدة البيانات الخاصة بك فارغة حاليًا. يوفر Rails طريقة لإنشاء بيانات بداية لتطبيقك.
قم بفتح ملف البذور المسمى seeds.rb
للتحرير:
- nano ~/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 إنشاء تسعة وصفات بأقسام للأسماء والمكونات والتعليمات. احفظ وأغلق الملف.
لزراعة قاعدة البيانات بهذه البيانات ، قم بتشغيل الأمر التالي في الطرفية الخاصة بك:
- rails db:seed
بتشغيل هذا الأمر ، ستتم إضافة تسعة وصفات إلى قاعدة البيانات الخاصة بك. يمكنك الآن جلبها وتقديمها على واجهة المستخدم.
سيقوم المكون لعرض جميع الوصفات بإرسال طلب HTTP إلى إجراء index
في RecipesController
للحصول على قائمة بجميع الوصفات. سيتم عرض هذه الوصفات في بطاقات على الصفحة.
قم بإنشاء ملف Recipes.jsx
في الدليل app/javascript/components
:
- nano ~/rails_react_recipe/app/javascript/components/Recipes.jsx
بمجرد فتح الملف ، استورد وحدات React
و useState
و useEffect
و Link
و useNavigate
عن طريق إضافة الأسطر التالية:
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
بعد ذلك ، أضف الأسطر المميزة لإنشاء وتصدير مكون React وظيفي يسمى Recipes
:
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 لاسترجاع جميع الوصفات الخاصة بك. للقيام بذلك، أضف الأسطر المحددة:
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 لاسترجاع جميع الوصفات باستخدام API الاسترجاع. إذا كانت الاستجابة ناجحة، يقوم التطبيق بحفظ مصفوفة الوصفات في حالة recipes
. إذا حدث خطأ، فسيقوم بتوجيه المستخدم إلى الصفحة الرئيسية.
أخيرًا، قم بإرجاع علامات الترميز للعناصر التي سيتم تقييمها وعرضها على صفحة المتصفح عند تقديم المكون. في هذه الحالة، سيقوم المكون بعرض بطاقة للوصفات من حالة recipes
. أضف الأسطر المحددة إلى 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
:
- nano 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>
);
احفظ واخرج من الملف.
في هذه النقطة ، من الجيد التحقق من أن الكود يعمل كما هو متوقع. كما فعلت سابقًا ، استخدم الأمر التالي لبدء الخادم الخاص بك:
- bin/dev
ثم افتح التطبيق في المتصفح الخاص بك. اضغط على زر “عرض الوصفة” في الصفحة الرئيسية للوصول إلى صفحة العرض التي تحتوي على وصفات البذور الخاصة بك:
استخدم CTRL+C
في الطرفية الخاصة بك لإيقاف الخادم والعودة إلى سطر الأوامر.
الآن بما أنه يمكنك عرض جميع الوصفات في التطبيق الخاص بك ، حان الوقت لإنشاء مكون ثاني لعرض الوصفات الفردية. أنشئ ملف Recipe.jsx
في الدليل app/javascript/components
:
- nano app/javascript/components/Recipe.jsx
كما هو الحال مع مكون Recipes
، قم بتوريد وحدات React
و useState
و useEffect
و Link
و useNavigate
و useParam
عن طريق إضافة الأسطر التالية:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
بعد ذلك ، أضف الأسطر المضللة لإنشاء وتصدير مكون React الوظيفي المسمى Recipe
:
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
. ستقوم الحالة recipe
ودالة setRecipe
بتحديث الحالة باستخدام خطاف useState
. بالإضافة إلى ذلك ، تقوم باستدعاء الخطاف useParams
، الذي يعيد كائنًا يحتوي على مفاتيح / قيم المعلمات URL.
للعثور على وصفة محددة ، يحتاج التطبيق الخاص بك إلى معرف الوصفة ، مما يعني أن مكون Recipe
يتوقع وجود id
param
في عنوان URL. يمكنك الوصول إليها عبر كائن params
الذي يحتوي على قيمة إرجاع خطاف useParams
.
ثم، قم بتعريف خطاف useEffect
حيث ستصل إلى معلمة id
من كائن params
. بمجرد الحصول على معلمة id
للوصفة، ستقوم بطلب HTTP لاسترجاع الوصفة. أضف الأسطر المظللة إلى ملفك:
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
، تستخدم قيمة params.id
لإجراء طلب HTTP GET لاسترجاع الوصفة التي تمتلك الـ id
ثم لحفظها في حالة المكون باستخدام دالة setRecipe
. التطبيق يعيد توجيه المستخدم إلى صفحة الوصفات إذا لم تكن الوصفة موجودة.
بعد ذلك، أضف دالة addHtmlEntities
التي ستُستخدم لاستبدال كيانات الأحرف بكيانات HTML في المكون. ستأخذ دالة addHtmlEntities
سلسلة نصية وستقوم بتبديل جميع الأقواس المفتوحة والمغلقة المهربة بكياناتها HTML. ستساعدك هذه الدالة في تحويل أي حرف مهرب تم حفظه في تعليمات وصفتك. أضف الأسطر المظللة:
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(/</g, "<").replace(/>/g, ">");
};
};
export default Recipe;
أخيرًا، قم بإرجاع العلامات لعرض الوصفة في حالة المكون على الصفحة عن طريق إضافة الأسطر المظللة:
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(/</g, "<").replace(/>/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
على صفحة ما، ستقوم بإضافته إلى ملف التوجيهات. افتح ملف التوجيهات للتحرير:
- nano 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
في هذا الملف التوجيهي وإضافة مسار. المسار لديه :id
كمتغير تعريف يتم استبداله بـid
الوصفة التي ترغب في عرضها.
احفظ وأغلق الملف.
استخدم النصيب bin/dev
لبدء الخادم مرة أخرى، ثم قم بزيارة http://localhost:3000
في متصفحك. انقر فوق زر عرض الوصفات للانتقال إلى صفحة الوصفات. في صفحة الوصفات، قم بالوصول إلى أي وصفة عن طريق النقر فوق زر عرض الوصفة. ستُستقبل بصفحة ممتلئة بالبيانات من قاعدة البيانات الخاصة بك:
يمكنك إيقاف الخادم باستخدام CTRL+C
.
في هذه الخطوة، قمت بإضافة تسع وصفات إلى قاعدة بياناتك وأنشأت مكونات لعرض هذه الوصفات، سواء بشكل فردي أو كمجموعة. في الخطوة التالية، ستقوم بإضافة مكون لإنشاء الوصفات.
الخطوة 8 — إنشاء الوصفات
الخطوة التالية للحصول على تطبيق وصفات الطعام القابل للاستخدام هي القدرة على إنشاء وصفات جديدة. في هذه الخطوة، ستقوم بإنشاء مكون لهذه الميزة. سيحتوي المكون على نموذج لجمع تفاصيل الوصفة المطلوبة من المستخدم ثم إرسال طلب إلى إجراء create
في تحكم Recipe
لحفظ بيانات الوصفة.
أنشئ ملف NewRecipe.jsx
في الدليل app/javascript/components
:
- nano app/javascript/components/NewRecipe.jsx
في الملف الجديد، قم بإستيراد وحدات React
، useState
، Link
، و useNavigate
التي استخدمتها في مكونات أخرى:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
ثم، أنشئ وصدر مكون NewRecipe
وظيفيًا عن طريق إضافة الأسطر المظللة:
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 بواسطة خطاف useNavigate
، ثم يتم استخدام خطاف useState
لتهيئة حالة name
، ingredients
، و instruction
، مع وظائف التحديث الخاصة بها على التوالي. هذه هي الحقول التي ستحتاج إليها لإنشاء وصفة صالحة.
بعد ذلك، قم بإنشاء وظيفة stripHtmlEntities
التي ستحول الأحرف الخاصة (مثل <
) إلى قيمها المهربة/المشفرة (مثل <
) على التوالي. للقيام بذلك، أضف السطور المظللة إلى مكون NewRecipe
:
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, "<")
.replace(/>/g, ">");
};
};
export default NewRecipe;
في وظيفة stripHtmlEntities
، قم بتبديل الأحرف <
و >
بقيمها المهربة. بهذه الطريقة، لن تقوم بتخزين HTML الخام في قاعدة البيانات الخاصة بك.
بعد ذلك، أضف السطور المظللة لإضافة وظائف onChange
و onSubmit
إلى مكون NewRecipe
للتعامل مع تحرير وتقديم النموذج:
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, "<")
.replace(/>/g, ">");
};
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
، يتم استبدال الأحرف <
و >
في تعليمات الوصفة بقيمتها المهربة واستبدال كل حرف جديد بوسم فاصل، مما يحافظ على تنسيق النص الذي أدخله المستخدم. أخيرًا، يتم إجراء طلب POST HTTP لإنشاء وصفة جديدة وإعادة التوجيه إلى صفحتها في حالة استجابة ناجحة.
لحماية ضد هجمات Cross-Site Request Forgery (CSRF)، يقوم Rails بإرفاق رمز أمان CSRF بمستند HTML. يُطلب هذا الرمز في كل مرة يتم فيها إجراء طلب غير GET
. باستخدام الثابت token
في الشيفرة السابقة، يتحقق التطبيق الخاص بك من الرمز على الخادم ويُلقي استثناءً إذا لم يتطابق الرمز الأماني مع المتوقع. في وظيفة onSubmit
، يسترد التطبيق الرمز CSRF المضمن في مستند HTML الخاص بك بواسطة Rails ومن ثم يقوم بإجراء طلب HTTP بسلسلة JSON. إذا تم إنشاء الوصفة بنجاح، يقوم التطبيق بإعادة توجيه المستخدم إلى صفحة الوصفة حيث يمكنه مشاهدة الوصفة التي تم إنشاؤها حديثًا.
أخيرًا، قم بإرجاع العلامة التي تقوم بتقديم نموذج للمستخدم لإدخال تفاصيل الوصفة التي يرغب المستخدم في إنشائها. أضف السطور المظللة:
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, "<")
.replace(/>/g, ">");
};
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;
العلامة المُرجَعِيّة المُعادة تتضمن نموذج يحتوي على ثلاث حقول إدخال؛ واحدة لكلٍ من recipeName
، recipeIngredients
، و instruction
. كل حقل إدخال له معالِج حدث onChange
يُستدعى الدالة onChange
. كما يتم تعليق معالِج حدث onSubmit
على زر الإرسال ويُستدعى الدالة onSubmit
التي تُرسل بيانات النموذج.
احفظ وأغلق الملف.
للوصول إلى هذا المكون في المتصفح، قم بتحديث ملف التوجيه الخاص بك بمساره:
- nano 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
الخاص بك للتعديل:
- nano app/javascript/components/Recipe.jsx
في مكون Recipe
، أضف وظيفة deleteRecipe
مع الأسطر المميزة:
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(/</g, "<").replace(/>/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
إلى وحدة التحكم Recipes
لحذف الوصفة. يقوم التطبيق بتوجيه المستخدم إلى صفحة الوصفات إذا تم حذف الوصفة بنجاح.
لتشغيل الكود في وظيفة deleteRecipe
كلما تم النقر على زر الحذف، قم بتمريره كمعالج حدث النقر إلى الزر. أضف حدث onClick
إلى عنصر زر الحذف في المكون:
...
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
الخاص بك مع هذا الملف:
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(/</g, "<").replace(/>/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، فكر في متابعة دروسنا تأمين الاتصالات في تطبيق Rails ثلاثي المستويات باستخدام أنفاق SSH أو زيارة سلسلة كيفية البرمجة بلغة Ruby لتحديث مهاراتك في Ruby. للغوص بعمق في React، جرب كيفية عرض البيانات من API DigitalOcean باستخدام React.