De auteur heeft de Electronic Frontier Foundation geselecteerd om een donatie te ontvangen als onderdeel van het Write for DOnations-programma.
Introductie
Ruby on Rails is een populair server-side webapplicatieframework. Het drijft veel populaire applicaties aan die vandaag op het web bestaan, zoals GitHub, Basecamp, SoundCloud, Airbnb en Twitch. Met de nadruk op de ervaring van programmeurs en de gepassioneerde gemeenschap eromheen, geeft Ruby on Rails je de tools die je nodig hebt om je moderne webapplicatie te bouwen en te onderhouden.
React is een JavaScript-bibliotheek die wordt gebruikt om front-end gebruikersinterfaces te maken. Ondersteund door Facebook, is het een van de meest populaire front-end bibliotheken die vandaag op het web worden gebruikt. React biedt functies zoals een virtueel Document Object Model (DOM), componentarchitectuur en state management, die het proces van front-end ontwikkeling georganiseerder en efficiënter maken.
Met de frontend van het web die zich richt op frameworks die losstaan van de server-side code, het combineren van de elegantie van Rails met de efficiëntie van React stelt je in staat krachtige en moderne applicaties te bouwen die worden geïnformeerd door huidige trends. Door React te gebruiken om componenten weer te geven vanuit een Rails-weergave (in plaats van de Rails-template-engine), profiteert jouw applicatie van de nieuwste ontwikkelingen op het gebied van JavaScript en frontend-ontwikkeling, terwijl de expressiviteit van Ruby on Rails wordt benut.
In deze tutorial maak je een Ruby on Rails-toepassing die je favoriete recepten opslaat en deze vervolgens weergeeft met een React-frontend. Wanneer je klaar bent, kun je recepten maken, bekijken en verwijderen met een React-interface gestyled met Bootstrap:
Vereisten
Om deze tutorial te volgen, heb je nodig:
-
Node.js en npm zijn geïnstalleerd op uw ontwikkelingsmachine. Deze tutorial gebruikt Node.js versie 16.14.0 en npm versie 8.3.1. Node.js is een JavaScript runtime-omgeving waarmee u uw code buiten de browser kunt uitvoeren. Het wordt geleverd met een vooraf geïnstalleerde Package Manager genaamd npm, waarmee u pakketten kunt installeren en bijwerken. Om deze te installeren op Ubuntu 20.04 of macOS, volgt u de “Installeren met behulp van een PPA” sectie van Hoe Node.js te installeren op Ubuntu 20.04 of de stappen in Hoe Node.js te installeren en een lokale ontwikkelingsomgeving te maken op macOS.
-
De Yarn-pakketbeheerder is geïnstalleerd op uw ontwikkelingsmachine, waarmee u het React-framework kunt downloaden. Deze tutorial is getest op versie 1.22.10; om deze afhankelijkheid te installeren, volgt u de officiële Yarn-installatiegids.
-
Ruby on Rails geïnstalleerd. Volg hiervoor onze handleiding op Hoe Ruby on Rails te installeren met rbenv op Ubuntu 20.04. Als je deze applicatie wilt ontwikkelen op macOS, kun je Hoe Ruby on Rails te installeren met rbenv op macOS gebruiken. Deze tutorial is getest op versie 3.1.2 van Ruby en versie 7.0.4 van Rails, dus zorg ervoor dat je deze versies tijdens het installatieproces specificeert.
Opmerking: Rails-versie 7 is niet achterwaarts compatibel. Als je Rails-versie 5 gebruikt, bezoek dan de tutorial voor Hoe een Ruby on Rails v5-project op te zetten met een React-frontend op Ubuntu 18.04.
- PostgreSQL geïnstalleerd, zoals beschreven in stappen 1 en 2 Hoe PostgreSQL te gebruiken met je Ruby on Rails applicatie op Ubuntu 20.04 of Hoe PostgreSQL te gebruiken met je Ruby on Rails applicatie op macOS. Om deze handleiding te volgen, kun je PostgreSQL versie 12 of hoger gebruiken. Als je deze applicatie wilt ontwikkelen op een andere Linux-distributie of een ander besturingssysteem, zie de officiële PostgreSQL downloads pagina. Voor meer informatie over hoe PostgreSQL te gebruiken, zie Hoe PostgreSQL te installeren en gebruiken.
Stap 1 — Een nieuwe Rails applicatie creëren
Je gaat je receptenapplicatie bouwen op het Rails applicatiekader in deze stap. Eerst maak je een nieuwe Rails applicatie, die zal worden ingesteld om te werken met React.
Rails biedt verschillende scripts genaamd generators die alles creëren wat nodig is om een moderne webapplicatie te bouwen. Om een volledige lijst van deze commando’s en wat ze doen te bekijken, voer het volgende commando uit in je terminal:
- rails -h
Deze opdracht zal een uitgebreide lijst met opties opleveren, waarmee je de parameters van je applicatie kunt instellen. Een van de vermelde opdrachten is de new
-opdracht, waarmee een nieuwe Rails-applicatie wordt aangemaakt.
Je zult nu een nieuwe Rails-applicatie aanmaken met behulp van de new
-generator. Voer de volgende opdracht uit in je terminal:
- rails new rails_react_recipe -d postgresql -j esbuild -c bootstrap -T
De bovenstaande opdracht maakt een nieuwe Rails-applicatie aan in een map met de naam rails_react_recipe
, installeert de vereiste Ruby- en JavaScript-afhankelijkheden, en configureert Webpack. De vlaggen die zijn gekoppeld aan deze new
-generatoropdracht omvatten het volgende:
- De
-d
-vlag specificeert de voorkeursdatabase-engine, die in dit geval PostgreSQL is. - De
-j
-vlag specificeert de benadering van de JavaScript van de applicatie. Rails biedt een paar verschillende manieren om JavaScript-code te verwerken in Rails-applicaties. De optieesbuild
die aan de-j
-vlag wordt doorgegeven, instrueert Rails om esbuild voor te configureren als de voorkeurs-JavaScript-bundler. - De
-c
-vlag specificeert de CSS-processor van de applicatie. Bootstrap is de voorkeurskeuze in dit geval. - De
-T
-vlag instrueert Rails om het genereren van testbestanden over te slaan, aangezien je geen tests zult schrijven voor deze tutorial. Deze opdracht wordt ook voorgesteld als je een Ruby-testtool wilt gebruiken die verschilt van degene die Rails biedt.
Zodra de opdracht is voltooid, ga je naar de rails_react_recipe
-map, wat de hoofdmap van je app is:
- cd rails_react_recipe
Vervolgens, lijst de inhoud van de map op:
- ls
De inhoud wordt afgedrukt als volgt:
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
Deze hoofdmap bevat verschillende automatisch gegenereerde bestanden en mappen die de structuur van een Rails-toepassing vormen, waaronder een package.json
-bestand met afhankelijkheden voor een React-toepassing.
Nu je succesvol een nieuwe Rails-toepassing hebt gemaakt, ga je het in de volgende stap koppelen aan een database.
Stap 2 — Database instellen
Voordat je je nieuwe Rails-toepassing uitvoert, moet je deze eerst koppelen aan een database. In deze stap koppel je de nieuw gemaakte Rails-toepassing aan een PostgreSQL-database, zodat receptgegevens kunnen worden opgeslagen en opgehaald indien nodig.
Het bestand database.yml
in config/database.yml
bevat databasegegevens zoals databasenamen voor verschillende ontwikkelomgevingen. Rails specificeert een databasenaam voor de verschillende ontwikkelomgevingen door een underscore (_
) toe te voegen gevolgd door de omgevingsnaam. In deze handleiding gebruik je de standaard databaseconfiguratie, maar je kunt je configuratiewaarden indien nodig wijzigen.
Opmerking: Op dit punt kunt u config/database.yml
aanpassen om in te stellen welke PostgreSQL-rol u wilt dat Rails gebruikt om uw database te maken. Tijdens de voorbereidingen heeft u een rol gemaakt die beveiligd is met een wachtwoord in de tutorial Hoe PostgreSQL te gebruiken met uw Ruby on Rails-toepassing. Als u de gebruiker nog niet heeft ingesteld, kunt u nu de instructies volgen voor Stap 4 – Configureren en maken van uw database in dezelfde voorbereidende tutorial.
Rails biedt veel opdrachten die het ontwikkelen van webtoepassingen eenvoudig maken, waaronder opdrachten om met databases te werken zoals create
, drop
en reset
. Om een database voor uw toepassing te maken, voert u de volgende opdracht uit in uw terminal:
- rails db:create
Deze opdracht maakt een development
en test
database aan, met het volgende resultaat:
OutputCreated database 'rails_react_recipe_development'
Created database 'rails_react_recipe_test'
Nu de toepassing is verbonden met een database, start u de toepassing door de volgende opdracht uit te voeren:
- bin/dev
Rails biedt een alternatief bin/dev
script dat een Rails-toepassing start door de opdrachten uit te voeren in het bestand Procfile.dev
in de hoofdmap van de app met behulp van de Foreman gem.
Zodra u deze opdracht uitvoert, zal uw opdrachtprompt verdwijnen en zal het volgende resultaat op zijn plaats worden afgedrukt:
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.
Om toegang te krijgen tot jouw applicatie, open je een browser venster en navigeer naar http://localhost:3000
. De standaard welkomstpagina van Rails wordt geladen, wat betekent dat je jouw Rails-applicatie correct hebt geconfigureerd:
Om de webserver te stoppen, druk op CTRL+C
in de terminal waar de server draait. Je ontvangt een afscheidsgroet van 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
Je terminal prompt zal dan opnieuw verschijnen.
Je hebt succesvol een database ingesteld voor jouw recepten-applicatie. In de volgende stap installeer je de JavaScript-afhankelijkheden die je nodig hebt om jouw React-frontend samen te stellen.
Stap 3 — Installeren van Frontend Afhankelijkheden
In deze stap installeer je de JavaScript-afhankelijkheden die nodig zijn aan de frontend van jouw recepten-applicatie. Deze omvatten:
- React voor het bouwen van gebruikersinterfaces.
- React DOM om React in staat te stellen te interageren met de browser DOM.
- React Router voor het behandelen van navigatie in een React-applicatie.
Voer de volgende opdracht uit om deze pakketten te installeren met de Yarn package manager:
- yarn add react react-dom react-router-dom
Deze opdracht gebruikt Yarn om de gespecificeerde pakketten te installeren en voegt ze toe aan het package.json
-bestand. Om dit te verifiëren, open het package.json
-bestand dat zich bevindt in de hoofdmap van het project:
- nano package.json
De geïnstalleerde pakketten worden vermeld onder de dependencies
-sleutel:
{
"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"
}
}
Sluit het bestand door op CTRL+X
te drukken.
Je hebt een paar front-end afhankelijkheden geïnstalleerd voor je applicatie. Vervolgens ga je een startpagina instellen voor je receptenapplicatie.
Stap 4 — Het instellen van de startpagina
Met de vereiste afhankelijkheden geïnstalleerd, ga je nu een startpagina voor de applicatie maken om te dienen als de landingspagina wanneer gebruikers de applicatie voor het eerst bezoeken.
Rails volgt het architecturale patroon van het Model-View-Controller voor applicaties. In het MVC-patroon heeft een controller als doel specifieke verzoeken te ontvangen en deze door te sturen naar het juiste model of de juiste weergave. Op dit moment toont de applicatie de welkomstpagina van Rails wanneer de hoofd-URL in de browser wordt geladen. Om dit te wijzigen, maak je een controller en weergave voor de startpagina en koppel je deze aan een route.
Rails biedt een controller
-generator om een controller aan te maken. De controller
-generator ontvangt een controller-naam en een bijpassende actie. Voor meer informatie hierover kun je de Rails-documentatie raadplegen.
Deze tutorial zal de controller Homepage
noemen. Voer de volgende opdracht uit om een Homepage
-controller met een index
-actie aan te maken:
- rails g controller Homepage index
Let op:
Op Linux kan de fout FATAAL: Luisterfout: kan directories niet bewaken voor wijzigingen.
veroorzaakt worden door een systeemlimiet op het aantal bestanden dat je machine kan bewaken voor wijzigingen. Voer de volgende opdracht uit om dit op te lossen:
- echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
Deze opdracht verhoogt permanent het aantal directories dat je kunt bewaken met Listen
naar 524288
. Je kunt dit opnieuw wijzigen door dezelfde opdracht uit te voeren en 524288
te vervangen door het gewenste aantal.
Het uitvoeren van het controller
-commando genereert de volgende bestanden:
- 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. - Een
index.html.erb
-bestand als de view-pagina voor alles wat met de homepage te maken heeft.
Naast deze nieuwe pagina’s die zijn aangemaakt door het uitvoeren van het Rails-commando, werkt Rails ook je routes-bestand bij dat zich bevindt op config/routes.rb
, met toevoeging van een get
-route voor je homepage, die je zult aanpassen als je root-route.
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
In dit bestand vervang je get 'homepage/index'
door root 'homepage#index'
zodat het bestand overeenkomt met het volgende:
Rails.application.routes.draw do
root 'homepage#index'
# Voor details over de DSL die beschikbaar is in dit bestand, zie http://guides.rubyonrails.org/routing.html
end
Deze aanpassing instrueert Rails om verzoeken naar de hoofdroute van de applicatie te koppelen aan de actie index
van de controller Homepage
, die op zijn beurt weergeeft in de browser wat er in het bestand index.html.erb
staat, te vinden op app/views/homepage/index.html.erb
.
Sla het bestand op en sluit het.
Om te controleren of dit werkt, start je applicatie:
- bin/dev
Wanneer je de applicatie in de browser opent of vernieuwt, laadt er een nieuwe startpagina voor je applicatie:
Zodra je hebt geverifieerd dat je applicatie werkt, druk je op CTRL+C
om de server te stoppen.
Vervolgens open je het bestand ~/rails_react_recipe/app/views/homepage/index.html.erb
:
- nano ~/rails_react_recipe/app/views/homepage/index.html.erb
Verwijder de code in het bestand, sla het bestand vervolgens op als leeg. Hiermee zorg je ervoor dat de inhoud van index.html.erb
niet interfereert met de React-rendering van je frontend.
Nu je de startpagina van je applicatie hebt ingesteld, kun je doorgaan naar het volgende gedeelte, waar je de frontend van je applicatie configureert om React te gebruiken.
Stap 5 — Configureren van React als je Rails Frontend
In deze stap configureer je Rails om React te gebruiken op de frontend van de applicatie, in plaats van de template engine. Deze nieuwe configuratie stelt je in staat om een visueel aantrekkelijkere homepage te maken met React.
Met behulp van de esbuild
-optie die is gespecificeerd bij het genereren van de Rails-applicatie, is het grootste deel van de benodigde setup al klaar om JavaScript naadloos met Rails te laten werken. Het enige wat rest, is het laden van het startpunt van de React-app in het esbuild
-startpunt voor JavaScript-bestanden. Om dit te doen, begin met het creëren van een map met de naam “components” in de map app/javascript
:
- mkdir ~/rails_react_recipe/app/javascript/components
De map components
zal de component voor de homepage bevatten, samen met andere React-componenten in de applicatie, inclusief het startbestand voor de React-toepassing.
Vervolgens, open het bestand application.js
dat zich bevindt op app/javascript/application.js
:
- nano ~/rails_react_recipe/app/javascript/application.js
Voeg de gemarkeerde regel code toe aan het bestand:
// Startpunt voor het bouwscript in je package.json
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
import "./components"
De toegevoegde regel code aan het bestand application.js
importeert de code in het startbestand index.jsx
, waardoor deze beschikbaar wordt gesteld aan esbuild
voor bundeling. Met de map /components
geïmporteerd in het JavaScript-startpunt van de Rails-app, kun je een React-component maken voor je homepage. De homepage bevat enkele teksten en een oproep tot actieknop om alle recepten te bekijken.
Sla het bestand op en sluit het.
Vervolgens, maak een bestand met de naam Home.jsx
in de map components
:
- nano ~/rails_react_recipe/app/javascript/components/Home.jsx
Voeg de volgende code toe aan het bestand:
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>
);
In deze code importeer je React en het Link
-component van React Router. Het Link
-component creëert een hyperlink om van de ene pagina naar de andere te navigeren. Vervolgens maak je een functioneel component aan en exporteer je het, waarin wat opmaaktaal staat voor je startpagina, gestileerd met Bootstrap-klassen.
Sla het bestand op en sluit het.
Met je Home
-component ingesteld, ga je nu de routing instellen met behulp van React Router. Maak een routes
-directory aan in de app/javascript
-directory:
- mkdir ~/rails_react_recipe/app/javascript/routes
De routes
-directory bevat een aantal routes met hun overeenkomstige componenten. Telkens wanneer een gespecificeerde route wordt geladen, zal het zijn overeenkomstige component naar de browser renderen.
In de routes
-directory, maak een index.jsx
-bestand aan:
- nano ~/rails_react_recipe/app/javascript/routes/index.jsx
Voeg de volgende code toe:
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>
);
In dit index.jsx
-routebestand importeer je de volgende modules: het React
-module waarmee je React kunt gebruiken, evenals de BrowserRouter
, Routes
en Route
-modules van React Router, die samen helpen om van de ene route naar de andere te navigeren. Ten slotte importeer je je Home
-component, die zal worden weergegeven wanneer een verzoek overeenkomt met de root (/
) route. Wanneer je meer pagina’s aan je applicatie wilt toevoegen, kun je in dit bestand een route declareren en deze koppelen aan het component dat je wilt renderen voor die pagina.
Sla het bestand op en verlaat het.
Je hebt nu routing ingesteld met behulp van React Router. Om React op de hoogte te stellen van de beschikbare routes en deze te gebruiken, moeten de routes beschikbaar zijn bij het ingangspunt van de toepassing. Om dit te bereiken, render je je routes in een component die React zal renderen in je invoerbestand.
Maak een App.jsx
bestand aan in de app/javascript/components
map:
- nano ~/rails_react_recipe/app/javascript/components/App.jsx
Voeg de volgende code toe aan het App.jsx
bestand:
import React from "react";
import Routes from "../routes";
export default props => <>{Routes}</>;
In het App.jsx
bestand importeer je React en de routebestanden die je zojuist hebt gemaakt. Vervolgens exporteer je een component om de routes binnen fragmenten te renderen. Deze component wordt gerenderd op het ingangspunt van de toepassing, waardoor de routes beschikbaar zijn wanneer de toepassing wordt geladen.
Sla het bestand op en sluit het.
Nu je je App.jsx
hebt ingesteld, kun je het renderen in je invoerbestand. Maak een index.jsx
bestand aan in de components
map:
- nano ~/rails_react_recipe/app/javascript/components/index.jsx
Voeg de volgende code toe aan het index.js
bestand:
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 />);
});
In de import
regels importeer je de React-bibliotheek, de createRoot
functie van ReactDOM, en je App
component. Met de createRoot
functie van ReactDOM maak je een hoofdelement aan als een div
element toegevoegd aan de pagina, en je rendert je App
component erin. Wanneer de toepassing wordt geladen, zal React de inhoud van de App
component renderen binnen het div
element op de pagina.
Sla het bestand op en sluit het.
Ten slotte voeg je wat CSS-stijlen toe aan je startpagina.
Open het bestand application.bootstrap.scss
in je ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
map:
- nano ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
Vervolgens vervang je de inhoud van het bestand application.bootstrap.scss
met de volgende code:
@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;
}
Je stelt enkele aangepaste kleuren in voor de pagina. De sectie .hero
zal het raamwerk creëren voor een hero-afbeelding, of een grote webbanner op de voorpagina van je website, die je later zult toevoegen. Daarnaast stileert de custom-button.btn
de knop die de gebruiker zal gebruiken om de applicatie binnen te gaan.
Met je CSS-stijlen op hun plaats, sla je het bestand op en sluit je het af.
Vervolgens herstart je de webserver voor je applicatie:
- bin/dev
Vernieuw vervolgens de applicatie in je browser. Een gloednieuwe startpagina wordt geladen:
Stop de webserver met CTRL+C
.
Je hebt je applicatie geconfigureerd om React te gebruiken als frontend in deze stap. In de volgende stap zul je modellen en controllers maken die het mogelijk maken om recepten te maken, te lezen, bij te werken en te verwijderen.
Stap 6 — Het maken van de Recept Controller en Model
Nu je een React-frontend hebt opgezet voor je applicatie, zul je een receptmodel en -controller maken. Het receptmodel zal de database tabel vertegenwoordigen met informatie over de recepten van de gebruiker, terwijl de controller verzoeken ontvangt en afhandelt om recepten aan te maken, te lezen, bij te werken of te verwijderen. Wanneer een gebruiker een recept opvraagt, ontvangt de receptcontroller dit verzoek en stuurt het door naar het receptmodel, dat de gevraagde gegevens uit de database haalt. Het model geeft vervolgens de receptgegevens terug als reactie naar de controller. Uiteindelijk worden deze gegevens weergegeven in de browser.
Begin met het maken van een Recept
model met behulp van de generate model
subopdracht die wordt geleverd door Rails en geef de naam van het model op samen met de kolommen en gegevenstypen. Voer de volgende opdracht uit:
- rails generate model Recipe name:string ingredients:text instruction:text image:string
De voorgaande opdracht instrueert Rails om een Recept
model te maken samen met een naam
kolom van het type string
, een ingrediënten
en instructie
kolom van het type text
, en een afbeelding
kolom van het type string
. Deze tutorial heeft het model Recept
genoemd, omdat modellen in Rails een enkelvoudige naam gebruiken, terwijl hun overeenkomstige databasetabellen een meervoudige naam gebruiken.
Door het uitvoeren van de generate model
opdracht worden er twee bestanden gemaakt en wordt de volgende uitvoer weergegeven:
Output invoke active_record
create db/migrate/20221017220817_create_recipes.rb
create app/models/recipe.rb
De twee gemaakte bestanden zijn:
- 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.
Vervolgens zul je het receptmodelbestand bewerken om ervoor te zorgen dat alleen geldige gegevens in de database worden opgeslagen. Dit kun je doen door wat databasevalidatie aan je model toe te voegen.
Open je receptmodel dat zich bevindt op app/models/recipe.rb
:
- nano ~/rails_react_recipe/app/models/recipe.rb
Voeg de volgende gemarkeerde regels code toe aan het bestand:
class Recipe < ApplicationRecord
validates :name, presence: true
validates :ingredients, presence: true
validates :instruction, presence: true
end
In deze code voeg je modelvalidatie toe, die controleert op de aanwezigheid van de velden name
, ingredients
en instruction
. Zonder deze drie velden is een recept ongeldig en wordt het niet opgeslagen in de database.
Sla het bestand op en sluit het.
Om Rails de tabel recipes
in je database te laten maken, moet je een migratie uitvoeren, wat een manier is om programmatisch wijzigingen aan te brengen in je database. Om ervoor te zorgen dat de migratie werkt met de database die je hebt opgezet, moet je wijzigingen aanbrengen in het bestand 20221017220817_create_recipes.rb
.
Open dit bestand in je editor:
- nano ~/rails_react_recipe/db/migrate/20221017220817_create_recipes.rb
Voeg het gemarkeerde materiaal toe zodat je bestand overeenkomt met het volgende:
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
Dit migratiebestand bevat een Ruby-klasse met een change
-methode en een opdracht om een tabel genaamd recipes
te maken samen met de kolommen en hun gegevenstypen. Je update ook 20221017220817_create_recipes.rb
met een NOT NULL
-beperking op de kolommen name
, ingredients
en instruction
door null: false
toe te voegen, zodat deze kolommen een waarde moeten hebben voordat de database wordt gewijzigd. Voeg ten slotte een standaardbeeld-URL toe voor je kolom met afbeeldingen; dit kan een andere URL zijn als je een andere afbeelding wilt gebruiken.
Met deze wijzigingen sla je het bestand op en verlaat je het. Je bent nu klaar om je migratie uit te voeren en je tabel te maken. Voer in je terminal het volgende commando uit:
- rails db:migrate
Je gebruikt het `database migrate`-commando om de instructies in je migratiebestand uit te voeren. Zodra het commando succesvol is uitgevoerd, ontvang je een uitvoer vergelijkbaar met het volgende:
Output== 20190407161357 CreateRecipes: migrating ====================================
-- create_table(:recipes)
-> 0.0140s
== 20190407161357 CreateRecipes: migrated (0.0141s) ===========================
Met je receptmodel op zijn plaats, maak je vervolgens je receptencontroller om de logica voor het maken, lezen en verwijderen van recepten toe te voegen. Voer het volgende commando uit:
- rails generate controller api/v1/Recipes index create show destroy --skip-template-engine --no-helper
In dit commando maak je een Recepten
-controller in een api/v1
-directory met een index
, create
, show
en destroy
-actie. De index
-actie zal het ophalen van al je recepten afhandelen; de create
-actie zal verantwoordelijk zijn voor het maken van nieuwe recepten; de show
-actie zal een enkel recept ophalen, en de destroy
-actie zal de logica bevatten voor het verwijderen van een recept.
Je geeft ook enkele vlaggen door om de controller lichter te maken, waaronder:
--skip-template-engine
, wat Rails instrueert om Rails-weergavebestanden over te slaan omdat React je front-end behoeften afhandelt.--no-helper
, wat Rails instrueert om het genereren van een hulpbestand voor je controller over te slaan.
Het uitvoeren van het commando werkt ook je routebestand bij met een route voor elke actie in de Recepten
-controller.
Wanneer het commando wordt uitgevoerd, wordt er een uitvoer afgedrukt zoals deze:
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
Om deze routes te gebruiken, moet je wijzigingen aanbrengen in je config/routes.rb
-bestand. Open het routes.rb
-bestand in je teksteditor:
- nano ~/rails_react_recipe/config/routes.rb
Update dit bestand om eruit te zien als de volgende code, waarbij je de gemarkeerde regels aanpast of toevoegt:
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'
# Definieer je applicatieroutes volgens de DSL in https://guides.rubyonrails.org/routing.html
# Definieert de root path route ("/")
# root "articles#index"
end
In dit routebestand wijzig je het HTTP-verb van de create
en destroy
routes zodat het post
en delete
data kan verwerken. Je wijzigt ook de routes voor de show
en destroy
acties door een :id
parameter aan de route toe te voegen. :id
zal het identificatienummer van het recept dat je wilt lezen of verwijderen bevatten.
Je voegt een catch-all route toe met get '/*path'
dat elke andere aanvraag die niet overeenkomt met de bestaande routes zal doorsturen naar de index
actie van de homepage
controller. De front-end routing zal aanvragen afhandelen die niet gerelateerd zijn aan het creëren, lezen, of verwijderen van recepten.
Sla het bestand op en sluit het af.
Om een lijst van beschikbare routes in je applicatie te beoordelen, voer je het volgende commando uit:
- rails routes
Het uitvoeren van dit commando toont een lange lijst van URI-patronen, werkwoorden en overeenkomende controllers of acties voor je project.
Vervolgens voeg je de logica toe om alle recepten tegelijk te krijgen. Rails gebruikt de ActiveRecord bibliotheek om databankgerelateerde taken zoals deze te behandelen. ActiveRecord verbindt klassen met relationele databanktabellen en biedt een rijke API om ermee te werken.
Om alle recepten te krijgen, gebruik je ActiveRecord om de receptentabel te bevragen en alle recepten in de databank op te halen.
Open het bestand recipes_controller.rb
met de volgende opdracht:
- nano ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
Voeg de gemarkeerde regels toe aan de recipes controller:
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
In de index
-actie gebruik je de all
-methode van ActiveRecord om alle recepten in je database op te halen. Met de order
-methode rangschik je ze in dalende volgorde op basis van hun creatiedatum, waardoor de nieuwste recepten eerst worden geplaatst. Ten slotte stuur je je lijst met recepten als JSON-respons met render
.
Vervolgens voeg je de logica toe voor het maken van nieuwe recepten. Net als bij het ophalen van alle recepten, vertrouw je op ActiveRecord om de verstrekte details van het recept te valideren en op te slaan. Werk je receptcontroller bij met de volgende gemarkeerde regels code:
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
In de create
actie gebruik je de create
methode van ActiveRecord om een nieuw recept aan te maken. De create
methode kan alle controllerparameters die zijn opgegeven in één keer aan het model toewijzen. Deze methode maakt het gemakkelijk om records aan te maken, maar opent de mogelijkheid van kwaadwillig gebruik. Kwaadwillig gebruik kan worden voorkomen door gebruik te maken van de sterke parameters functie die door Rails wordt geleverd. Op deze manier kunnen parameters niet worden toegewezen tenzij ze zijn toegestaan. Je geeft een recipe_params
parameter door aan de create
methode in je code. De recipe_params
is een private
methode waarin je je controllerparameters toestaat om te voorkomen dat verkeerde of kwaadwillige inhoud in je database terechtkomt. In dit geval sta je een name
, image
, ingredients
en instruction
parameter toe voor een geldig gebruik van de create
methode.
Je receptcontroller kan nu recepten lezen en aanmaken. Het enige wat nog rest is de logica voor het lezen en verwijderen van een enkel recept. Werk je receptencontroller bij met de gemarkeerde code:
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
In de nieuwe regels code maak je een private set_recipe
-methode die wordt aangeroepen door een before_action
, alleen wanneer de acties show
en delete
overeenkomen met een verzoek. De set_recipe
-methode gebruikt de find
-methode van ActiveRecord om een recept te vinden waarvan de id
overeenkomt met de id
die wordt meegegeven in de params
en kent dit toe aan een instantievariabele @recipe
. In de show
-actie retourneer je het @recipe
-object dat is ingesteld door de set_recipe
-methode als een JSON-response.
In de destroy
-actie heb je iets soortgelijks gedaan met behulp van de veiligheidsnavigatie-operator van Ruby &.
, die nil
-fouten voorkomt bij het aanroepen van een methode. Deze toevoeging stelt je in staat om een recept alleen te verwijderen als het bestaat, en vervolgens een bericht als respons te verzenden.
Na deze wijzigingen in recipes_controller.rb
op te slaan en het bestand te sluiten.
In deze stap heb je een model en controller gemaakt voor je recepten. Je hebt alle logica geschreven die nodig is om met recepten aan de backend te werken. In het volgende gedeelte zul je componenten maken om je recepten te bekijken.
Stap 7 — Recepten Bekijken
In dit gedeelte zul je componenten maken om recepten te bekijken. Je zult twee pagina’s maken: één om alle bestaande recepten te bekijken en een andere om individuele recepten te bekijken.
Je begint met het maken van een pagina om alle recepten te bekijken. Voordat je de pagina maakt, heb je recepten nodig om mee te werken, aangezien je database momenteel leeg is. Rails biedt een manier om zaaigegevens te maken voor je toepassing.
Open het zaadbestand genaamd seeds.rb
voor bewerking:
- nano ~/rails_react_recipe/db/seeds.rb
Vervang de oorspronkelijke inhoud van het zaadbestand door de volgende code:
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
In deze code gebruik je een lus die Rails opdraagt om negen recepten te maken met secties voor naam
, ingrediënten
en instructie
. Sla het bestand op en sluit af.
Om de database te bevoorraden met deze gegevens, voer je het volgende commando uit in je terminal:
- rails db:seed
Door dit commando uit te voeren, worden negen recepten aan je database toegevoegd. Nu kun je ze ophalen en op de frontend renderen.
Het onderdeel om alle recepten te bekijken zal een HTTP-verzoek maken naar de index
-actie in de RecipesController
om een lijst van alle recepten te krijgen. Deze recepten zullen dan op de pagina in kaarten worden weergegeven.
Maak een bestand Recipes.jsx
in de map app/javascript/components
:
- nano ~/rails_react_recipe/app/javascript/components/Recipes.jsx
Zodra het bestand geopend is, importeer de modules React
, useState
, useEffect
, Link
en useNavigate
door de volgende regels toe te voegen:
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
Voeg vervolgens de gemarkeerde regels toe om een functioneel React-component genaamd Recipes
te maken en exporteren:
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;
Binnen de Recipe
component zal de navigatie-API van React Router de useNavigate hook aanroepen. De useState hook van React zal de recipes
state initialiseren, die een lege array ([]
) is, en een setRecipes
functie voor het bijwerken van de recipes
state.
Vervolgens, in een useEffect
hook, zul je een HTTP-verzoek doen om al je recepten op te halen. Voeg hiervoor de gemarkeerde regels toe:
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;
In je useEffect
hook doe je een HTTP-verzoek om alle recepten op te halen met behulp van de Fetch API. Als de respons succesvol is, slaat de applicatie de array met recepten op in de recipes
state. Als er een fout optreedt, wordt de gebruiker doorgestuurd naar de startpagina.
Tenslotte, retourneer de markup voor de elementen die geëvalueerd en weergegeven zullen worden op de browserpagina wanneer de component wordt gerenderd. In dit geval zal de component een kaart met recepten renderen uit de recipes
state. Voeg de gemarkeerde regels toe aan 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;
Sla op en verlaat Recipes.jsx
.
Nu je een component hebt gemaakt om alle recepten weer te geven, zul je een route ervoor maken. Open het routebestand aan de voorkant app/javascript/routes/index.jsx
:
- nano app/javascript/routes/index.jsx
Voeg de gemarkeerde regels toe aan het bestand:
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>
);
Sla op en verlaat het bestand.
Op dit punt is het een goed idee om te controleren of je code werkt zoals verwacht. Gebruik, net als eerder, het volgende commando om je server te starten:
- bin/dev
Vervolgens open je de app in je browser. Druk op de knop Recept bekijken op de startpagina om toegang te krijgen tot een weergavepagina met je basisrecepten:
Gebruik CTRL+C
in je terminal om de server te stoppen en terug te keren naar je prompt.
Nu je alle recepten in je applicatie kunt bekijken, is het tijd om een tweede component te maken om individuele recepten te bekijken. Maak een Recipe.jsx
bestand aan in de app/javascript/components
directory:
- nano app/javascript/components/Recipe.jsx
Net als bij de Recipes
-component importeer je de React
, useState
, useEffect
, Link
, useNavigate
en useParam
modules door de volgende regels toe te voegen:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
Voeg vervolgens de gemarkeerde regels toe om een functionele React-component genaamd Recipe
te maken en te exporteren:
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;
Net als bij de Recipes
-component initialiseer je de React Router-navigatie met de useNavigate
-hook. Een recipe
-staat en een setRecipe
-functie zullen de staat bijwerken met de useState
-hook. Bovendien roep je de useParams
-hook aan, die een object retourneert waarvan de key/value-paren URL-parameters zijn.
Om een specifiek recept te vinden, moet je applicatie de id
van het recept kennen, wat betekent dat je Recipe
-component een id
-parameter verwacht in de URL. Je kunt hier toegang toe krijgen via het params
-object dat de retourwaarde van de useParams
-hook bevat.
Volgende, declareer een useEffect
hook waar je toegang zult krijgen tot de id
param
uit het params
object. Zodra je de recept id
param hebt, maak je een HTTP-verzoek om het recept op te halen. Voeg de gemarkeerde regels toe aan je bestand:
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;
In de useEffect
hook gebruik je de waarde van params.id
om een GET HTTP-verzoek te doen om het recept op te halen dat bij de id
hoort en vervolgens om het op te slaan in de component state met behulp van de setRecipe
functie. De app stuurt de gebruiker door naar de receptenpagina als het recept niet bestaat.
Voeg vervolgens een addHtmlEntities
functie toe, die zal worden gebruikt om tekenentiteiten te vervangen door HTML-entiteiten in de component. De addHtmlEntities
functie neemt een string en vervangt alle ontsnapte opening en sluitende haakjes door hun HTML-entiteiten. Deze functie zal je helpen om welk ontsnapt karakter dan ook om te zetten dat in je receptinstructie is opgeslagen. Voeg de gemarkeerde regels toe:
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;
Tenslotte, geef de opmaak terug om het recept op de pagina weer te geven door de gemarkeerde regels toe te voegen:
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;
Met een `ingredientenlijst`-functie splitst u uw met komma’s gescheiden receptingrediënten in een array en maakt u er een lijst van ingrediënten van. Als er geen ingrediënten zijn, geeft de app een bericht weer dat zegt Geen ingrediënten beschikbaar. U vervangt ook alle openingen en sluitingen haakjes in de receptinstructie door het door te geven aan de `addHtmlEntities`-functie. Ten slotte geeft de code de receptafbeelding weer als een heldenafbeelding, voegt een Recept verwijderen knop toe naast de receptinstructie, en voegt een knop toe die teruglinkt naar de receptenpagina.
Opmerking: Het gebruik van de `dangerouslySetInnerHTML`-attribuut van React is riskant omdat het uw app blootstelt aan cross-site scripting-aanvallen. Dit risico wordt verminderd door ervoor te zorgen dat speciale tekens die worden ingevoerd bij het maken van recepten worden vervangen door gebruik te maken van de `stripHtmlEntities`-functie die is gedeclareerd in de `NewRecipe`-component.
Sla het bestand op en sluit het af.
Om de `Recept`-component op een pagina te bekijken, voegt u deze toe aan uw routes-bestand. Open uw routebestand voor bewerking:
- nano app/javascript/routes/index.jsx
Voeg de volgende gemarkeerde regels toe aan het bestand:
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>
);
U importeert uw `Recept`-component in dit routebestand en voegt een route toe. De route heeft een `:id` `param` die zal worden vervangen door de `id` van het recept dat u wilt bekijken.
Sla het bestand op en sluit het af.
Gebruik het bin/dev
script om je server opnieuw te starten, bezoek vervolgens http://localhost:3000
in je browser. Klik op de Bekijk Recepten knop om naar de receptenpagina te gaan. Op de receptenpagina, bekijk een willekeurig recept door op de Bekijk Recept knop te klikken. Je wordt begroet met een pagina gevuld met gegevens uit je database:
Je kunt de server stoppen met CTRL+C
.
In deze stap heb je negen recepten aan je database toegevoegd en componenten gemaakt om deze recepten te bekijken, zowel individueel als in een collectie. In de volgende stap voeg je een component toe om recepten te maken.
Stap 8 — Recepten Maken
De volgende stap naar een bruikbare voedselrecepttoepassing is de mogelijkheid om nieuwe recepten te maken. In deze stap maak je een component voor deze functie. De component bevat een formulier om de vereiste receptdetails van de gebruiker te verzamelen en vervolgens een verzoek te doen naar de create
actie in de Recipe
controller om de receptgegevens op te slaan.
Maak een NewRecipe.jsx
bestand aan in de app/javascript/components
directory:
- nano app/javascript/components/NewRecipe.jsx
In het nieuwe bestand, importeer de React
, useState
, Link
, en useNavigate
modules die je in andere componenten hebt gebruikt:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
Maak vervolgens een functionele NewRecipe
component aan en exporteer deze door de gemarkeerde regels toe te voegen:
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;
Zoals bij eerdere onderdelen, initialiseer je de React-router navigatie met de useNavigate
-hook en gebruik je vervolgens de useState
-hook om een name
, ingredients
, en instruction
state te initialiseren, elk met hun respectievelijke update functies. Dit zijn de velden die je nodig hebt om een geldig recept te maken.
Vervolgens maak je een stripHtmlEntities
-functie die speciale tekens (zoals <
) zal omzetten naar hun geëscapete/gecodeerde waarden (zoals <
), respectievelijk. Voeg hiervoor de gemarkeerde regels toe aan het NewRecipe
-component:
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;
In de stripHtmlEntities
-functie vervang je de tekens <
en >
door hun geëscapete waarden. Op deze manier sla je geen ruwe HTML op in je database.
Voeg vervolgens de gemarkeerde regels toe om de onChange
– en onSubmit
-functies toe te voegen aan het NewRecipe
-component om het bewerken en indienen van het formulier te behandelen:
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;
De functie onChange
accepteert de invoer van de gebruiker event
en de functie om de staat in te stellen, waarna het de staat vervolgens bijwerkt met de waarde van de gebruikersinvoer. In de functie onSubmit
controleer je of geen van de vereiste invoervelden leeg is. Vervolgens bouw je een object met de parameters die nodig zijn om een nieuw recept te maken. Met behulp van de functie stripHtmlEntities
vervang je de tekens <
en >
in de receptinstructie door hun geëscapete waarde en vervang je elk nieuw regelteken door een afbreektag, waardoor de opmaak van de tekst die door de gebruiker is ingevoerd behouden blijft. Ten slotte doe je een POST HTTP-verzoek om het nieuwe recept te maken en door te sturen naar de pagina ervan bij een succesvolle respons.
Om te beschermen tegen aanvallen van Cross-Site Request Forgery (CSRF), voegt Rails een CSRF-beveiligingstoken toe aan het HTML-document. Dit token is vereist telkens wanneer er een niet-GET
-verzoek wordt gedaan. Met de constante token
in de voorgaande code controleert je applicatie het token op de server en genereert een uitzondering als het beveiligingstoken niet overeenkomt met wat wordt verwacht. In de functie onSubmit
haalt de applicatie het ingebedde CSRF-token op in je HTML-document door Rails en maakt vervolgens een HTTP-verzoek met een JSON-string. Als het recept succesvol is aangemaakt, stuurt de applicatie de gebruiker door naar de receptpagina waar ze hun nieuw aangemaakte recept kunnen bekijken.
Als laatste, retourneer de markup die een formulier rendert voor de gebruiker om de details voor het gewenste recept in te voeren. Voeg de gemarkeerde regels toe:
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;
Het geretourneerde markering bevat een formulier met drie invoervelden; één voor elk van de recipeName
, recipeIngredients
en instruction
. Elk invoerveld heeft een onChange
-gebeurtenishandler die de functie onChange
oproept. Een onSubmit
-gebeurtenishandler is ook gekoppeld aan de verzendknop en roept de functie onSubmit
aan die de formuliergegevens verzendt.
Sla het bestand op en sluit af.
Om toegang te krijgen tot dit component in de browser, bijwerk je je routebestand met de route:
- nano app/javascript/routes/index.jsx
Werk je routebestand bij om deze gemarkeerde regels op te nemen:
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>
);
Met de route ingesteld, sla je het bestand op en sluit je af.
Herstart je ontwikkelingsserver en ga naar http://localhost:3000
in je browser. Ga naar de pagina met recepten en klik op de Maak nieuw recept-knop. Je zult een pagina vinden met een formulier om recepten aan je database toe te voegen:
Vul de vereiste gegevens in en klik op de Maak recept-knop. Het nieuw gemaakte recept zal dan op de pagina verschijnen. Sluit de server af wanneer je klaar bent.
In deze stap heb je de mogelijkheid toegevoegd om recepten te maken voor je voedselrecepten-applicatie. In de volgende stap voeg je de functionaliteit toe om recepten te verwijderen.
Stap 9 — Recepten Verwijderen
In deze sectie ga je je Receptcomponent aanpassen om een optie toe te voegen voor het verwijderen van recepten. Wanneer je op de verwijderknop op de receptpagina klikt, stuurt de applicatie een verzoek om een recept uit de database te verwijderen.
Ten eerste, open je Recipe.jsx
bestand om te bewerken:
- nano app/javascript/components/Recipe.jsx
Voeg in de Recipe
component een deleteRecipe
functie toe met de gemarkeerde regels:
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="">
...
In de deleteRecipe
functie haal je de id
op van het recept dat verwijderd moet worden, bouw je vervolgens je URL en pak je de CSRF-token. Vervolgens doe je een DELETE
verzoek naar de Recipes
controller om het recept te verwijderen. De applicatie leidt de gebruiker om naar de receptenpagina als het recept succesvol is verwijderd.
Om de code in de deleteRecipe
functie uit te voeren wanneer er op de verwijderknop wordt geklikt, geef je het door als de klikgebeurtenisbehandelaar aan de knop. Voeg een onClick
gebeurtenis toe aan het verwijderknop element in de component:
...
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>
);
...
Op dit punt in de tutorial moet je volledige Recipe.jsx
bestand overeenkomen met dit bestand:
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;
Sla op en verlaat het bestand.
Herstart de applicatieserver en navigeer naar de homepage. Klik op de Bekijk Recepten knop om toegang te krijgen tot alle bestaande recepten, open vervolgens een specifiek recept en klik op de Recept Verwijderen knop op de pagina om het artikel te verwijderen. Je wordt omgeleid naar de receptenpagina en het verwijderde recept zal niet langer bestaan.
Met de werkende verwijderknop heb je nu een volledig functionele receptenapplicatie!
Conclusie
In deze tutorial heb je een voedselrecepttoepassing gemaakt met Ruby on Rails en een React-frontend, waarbij PostgreSQL als je database werd gebruikt en Bootstrap voor de opmaak. Als je verder wilt gaan met het bouwen met Ruby on Rails, overweeg dan om onze Beveiligen van communicatie in een Rails-toepassing met drie lagen met behulp van SSH-tunnels tutorial te volgen of bezoek onze Hoe te coderen in Ruby serie om je Ruby-vaardigheden op te frissen. Om dieper in React te duiken, probeer Hoe gegevens van de DigitalOcean API weer te geven met React.