Автор выбрал Electronic Frontier Foundation в качестве получателя пожертвования в рамках программы Write for DOnations.
Введение
Ruby on Rails – популярный серверный фреймворк веб-приложений. Он используется во многих популярных веб-приложениях, таких как GitHub, Basecamp, SoundCloud, Airbnb и Twitch. С акцентом на опыт программиста и страстным сообществом вокруг, Ruby on Rails предоставляет вам инструменты для создания и поддержки вашего современного веб-приложения.
React – библиотека JavaScript, используемая для создания пользовательских интерфейсов на стороне клиента. Поддерживаемая Facebook, она является одной из самых популярных библиотек для разработки интерфейсов на веб-сайтах. React предлагает функции, такие как виртуальная модель объектов документа (DOM), архитектура компонентов и управление состоянием, что делает процесс разработки интерфейса более организованным и эффективным.
Со сдвигом веб-фронтенда к фреймворкам, отделенным от серверного кода, объединение изящества Rails с эффективностью React позволит вам создавать мощные и современные приложения, основанные на текущих тенденциях. Используя React для рендеринга компонентов изнутри представления Rails (вместо шаблонного движка Rails), ваше приложение будет воспользоваться последними достижениями в JavaScript и разработке фронтенда, при этом используя экспрессивность 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
, устанавливает необходимые зависимости Ruby и JavaScript, а также настраивает Webpack. Флаги, связанные с этой командой генератора new
, включают следующее:
- Флаг
-d
указывает предпочтительный движок базы данных, который в данном случае является PostgreSQL. - Флаг
-j
определяет подход к JavaScript в приложении. Rails предлагает несколько способов обработки кода JavaScript в приложениях Rails. Опцияesbuild
, переданная флагу-j
, указывает Rails предварительно настроить esbuild как предпочтительный инструмент сборки JavaScript. - Флаг
-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
, чтобы установить, какую роль PostgreSQL вы хотите, чтобы 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
Затем ваш терминальный приглашение снова появится.
Вы успешно настроили базу данных для вашего приложения с рецептами. На следующем этапе вы установите JavaScript-зависимости, необходимые для создания вашего фронтенда React.
Шаг 3 — Установка зависимостей фронтенда
На этом этапе вы установите необходимые зависимости JavaScript для фронтенда вашего приложения с рецептами. Они включают в себя:
- 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'
# Для получения подробной информации о DSL, доступном в этом файле, см. 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, большая часть необходимой настройки для безпроблемной работы JavaScript с Rails уже выполнена. Все, что осталось, это загрузить точку входа React-приложения в точку входа esbuild
для файлов JavaScript. Для этого начните с создания директории компонентов в директории 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
в точку входа JavaScript приложения 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
создает гиперссылку для перехода с одной страницы на другую. Затем вы создаете и экспортируете функциональный компонент, содержащий некоторый язык разметки для вашей домашней страницы, стилизованный с помощью классов 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 />);
});
В строках import
вы импортируете библиотеку React, функцию createRoot
из ReactDOM и ваш компонент App
. Используя функцию createRoot
из ReactDOM, вы создаете корневой элемент в виде элемента div
, присоединенного к странице, и рендерите ваш компонент App
в нем. Когда приложение загружено, React будет рендерить содержимое компонента App
внутри элемента div
на странице.
Сохраните и закройте файл.
Наконец, вы добавите некоторые CSS-стили на вашу домашнюю страницу.
Откройте файл application.bootstrap.scss
в вашем каталоге ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
:
- 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
создаст основу для героического изображения или большого веб-баннера на главной странице вашего веб-сайта, который вы добавите позже. Кроме того, стиль custom-button.btn
стилизует кнопку, которую пользователь будет использовать для входа в приложение.
После того как ваши стили CSS будут на месте, сохраните и закройте файл.
Затем перезапустите веб-сервер для вашего приложения:
- bin/dev
Затем перезагрузите приложение в вашем браузере. Загрузится совершенно новая домашняя страница:
Остановите веб-сервер с помощью CTRL+C
.
Вы настроили ваше приложение на использование React в качестве его фронтенда на этом этапе. На следующем этапе вы создадите модели и контроллеры, которые позволят вам создавать, читать, обновлять и удалять рецепты.
Шаг 6 — Создание контроллера и модели рецепта
Теперь, когда вы настроили фронтенд на React для вашего приложения, вы создадите модель и контроллер для рецептов. Модель рецепта будет представлять таблицу базы данных, содержащую информацию о рецептах пользователя, в то время как контроллер будет принимать и обрабатывать запросы на создание, чтение, обновление или удаление рецептов. Когда пользователь запрашивает рецепт, контроллер рецептов получает этот запрос и передает его модели рецепта, которая извлекает запрошенные данные из базы данных. Затем модель возвращает данные рецепта в качестве ответа контроллеру. Наконец, эта информация отображается в браузере.
Начните с создания модели Recipe
с использованием подкоманды generate model
, предоставляемой Rails, и указания названия модели вместе с ее столбцами и типами данных. Выполните следующую команду:
- rails generate model Recipe name:string ingredients:text instruction:text image:string
Приведенная команда указывает Rails создать модель Recipe
вместе со столбцом name
типа string
, столбцами ingredients
и instruction
типа text
, и столбцом image
типа string
. В этом учебнике модель названа Recipe
, потому что модели в Rails используют единственное число в названии, в то время как соответствующие таблицы базы данных используют множественное число.
Выполнение команды generate model
создает два файла и выводит следующий результат:
Output invoke active_record
create db/migrate/20221017220817_create_recipes.rb
create app/models/recipe.rb
Созданные файлы:
- A
recipe.rb
file that holds all the model-related logic. - A
20221017220817_create_recipes.rb
file (the number at the beginning of the file may differ depending on the date when you run the command). This migration file contains the instruction for creating the database structure.
Далее вы будете редактировать файл модели рецепта, чтобы убедиться, что в базу данных сохраняются только допустимые данные. Этого можно достичь, добавив некоторые проверки валидности данных в вашу модель.
Откройте вашу модель рецепта, расположенную в app/models/recipe.rb
:
- 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
. Без этих трех полей рецепт недействителен и не будет сохранен в базу данных.
Сохраните и закройте файл.
Чтобы Rails создал таблицу recipes
в вашей базе данных, вы должны выполнить миграцию, которая представляет собой способ вносить изменения в вашу базу данных программно. Чтобы убедиться, что миграция работает с настройками вашей базы данных, вы должны внести изменения в файл 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
Вы используете команду `database 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
В этой команде вы создаете контроллер `Recipes` в каталоге `api/v1` с действиями `index`, `create`, `show` и `destroy`. Действие `index` будет обрабатывать получение всех ваших рецептов; действие `create` будет отвечать за создание новых рецептов; действие `show` будет получать один рецепт, а действие `destroy` будет содержать логику удаления рецепта.
Вы также передаете некоторые флаги, чтобы сделать контроллер более легким, включая:
- `–skip-template-engine`, который указывает Rails пропустить генерацию файлов представлений Rails, поскольку React обрабатывает ваши потребности в интерфейсе.
- `–no-helper`, который указывает Rails пропустить генерацию файла помощника для вашего контроллера.
Запуск команды также обновляет файл маршрутов с маршрутом для каждого действия в контроллере `Recipes`.
При выполнении команды будет выведен вывод подобный следующему:
Output create app/controllers/api/v1/recipes_controller.rb
route namespace :api do
namespace :v1 do
get 'recipes/index'
get 'recipes/create'
get 'recipes/show'
get 'recipes/destroy'
end
end
Чтобы использовать эти маршруты, вам нужно внести изменения в ваш файл `config/routes.rb`. Откройте файл `routes.rb` в вашем текстовом редакторе:
- 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
будет содержать идентификационный номер рецепта, который вы хотите прочитать или удалить.
Вы добавляете маршрут catch-all с get '/*path'
, который направит любой другой запрос, который не соответствует существующим маршрутам, к действию index
контроллера homepage
. Маршрутизация с фронт-энда будет обрабатывать запросы, не относящиеся к созданию, чтению или удалению рецептов.
Сохраните и закройте файл.
Для оценки списка доступных маршрутов в вашем приложении выполните следующую команду:
- rails routes
Запуск этой команды отобразит длинный список шаблонов URI, глаголов и соответствующих контроллеров или действий для вашего проекта.
Затем вы добавите логику для получения всех рецептов сразу. Rails использует библиотеку ActiveRecord для обработки задач, связанных с базой данных, подобных этой. ActiveRecord соединяет классы с таблицами реляционной базы данных и предоставляет обширный API для работы с ними.
Чтобы получить все рецепты, вы будете использовать 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
может назначать все параметры контроллера сразу в модель. Этот метод упрощает создание записей, но открывает возможность злоупотребления. Злоупотребление можно предотвратить, используя функцию сильных параметров, предоставляемую 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
совпадает с id
, указанным в params
, и присваивает его переменной экземпляра @recipe
. В действии show
вы возвращаете объект @recipe
, установленный методом set_recipe
, как JSON-ответ.
В действии destroy
вы сделали нечто похожее, используя оператор безопасной навигации Ruby &.
, который избегает ошибок nil
при вызове метода. Это дополнение позволяет удалить рецепт только в том случае, если он существует, а затем отправить сообщение в качестве ответа.
После внесения этих изменений в recipes_controller.rb
сохраните и закройте файл.
На этом шаге вы создали модель и контроллер для ваших рецептов. Вы написали всю логику, необходимую для работы с рецептами на бэкенде. В следующем разделе вы создадите компоненты для просмотра ваших рецептов.
Шаг 7 — Просмотр рецептов
В этом разделе вы создадите компоненты для просмотра рецептов. Вы создадите две страницы: одну для просмотра всех существующих рецептов и другую для просмотра отдельных рецептов.
Вы начнете с создания страницы для просмотра всех рецептов. Прежде чем создать страницу, вам нужны рецепты для работы, так как ваша база данных в настоящее время пуста. Rails предоставляет способ создания начальных данных (seed data) для вашего приложения.
Откройте файл начальных данных с именем 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 создать девять рецептов с разделами для name
, ingredients
и instruction
. Сохраните и закройте файл.
Чтобы заполнить базу данных этими данными, выполните следующую команду в вашем терминале:
- 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
, навигационный API 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-запрос для получения всех рецептов с помощью Fetch 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-параметрами.
Чтобы найти определенный рецепт, ваше приложение должно знать id
рецепта, что означает, что ваш компонент 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
для выполнения GET-запроса HTTP для получения рецепта с указанным 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 для создания нового рецепта и перенаправляетесь на его страницу при успешном ответе.
Для защиты от атак межсайтового подделывания запроса (CSRF) Rails прикрепляет защитный токен CSRF к HTML-документу. Этот токен требуется при выполнении любого не-GET
запроса. С постоянной token
в предыдущем коде ваше приложение проверяет токен на сервере и генерирует исключение, если защитный токен не соответствует ожидаемому. В функции onSubmit
приложение извлекает встроенный в ваш HTML-документ токен CSRF от 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.