Поиск по сайту:

Как настроить проект Ruby on Rails v7 с интерфейсом React в Ubuntu 20.04


Автор выбрал программу Write for DOnations.

Введение

Дергаться. Делая акцент на опыте программиста и увлеченном сообществе, созданном вокруг него, Ruby on Rails предоставит вам инструменты, необходимые для создания и поддержки вашего современного веб-приложения.

управление состоянием, которые делают процесс front-end разработки более организованным и эффективным.

По мере того, как веб-интерфейс движется к фреймворкам, отделенным от кода на стороне сервера, сочетание элегантности Rails с эффективностью React позволит вам создавать мощные и современные приложения, соответствующие текущим тенденциям. Используя React для рендеринга компонентов из представления Rails (вместо механизма шаблонов Rails), ваше приложение получит выгоду от последних достижений в JavaScript и разработке интерфейса, используя при этом выразительность Ruby on Rails.

В этом руководстве вы создадите приложение Ruby on Rails, которое хранит ваши любимые рецепты, а затем отображает их с помощью внешнего интерфейса React. Когда вы закончите, вы сможете создавать, просматривать и удалять рецепты, используя интерфейс React, стилизованный под Bootstrap:

Предпосылки

Чтобы следовать этому руководству, вам нужно:

  • Как установить Node.js и создать локальную среду разработки на macOS.
  • Менеджер пакетов Yarn, установленный на вашем компьютере для разработки, позволит вам загрузить среду React. Это руководство было протестировано на версии 1.22.10; чтобы установить эту зависимость, следуйте официальному руководству по установке Yarn.
  • Установлен Ruby on Rails. Чтобы получить это, следуйте нашему руководству «Как установить 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».

Шаг 1 — Создание нового приложения Rails

На этом шаге вы создадите свое приложение рецепта на платформе приложения Rails. Сначала вы создадите новое приложение Rails, которое будет настроено для работы с React.

Rails предоставляет несколько скриптов, называемых генераторами, которые создают все необходимое для создания современного веб-приложения. Чтобы просмотреть полный список этих команд и то, что они делают, выполните следующую команду в своем терминале:

  1. rails -h

Эта команда выдаст исчерпывающий список опций, позволяющих вам установить параметры вашего приложения. Одной из перечисленных команд является команда new, которая создает новое приложение Rails.

Теперь вы создадите новое приложение Rails, используя генератор new. Запустите следующую команду в своем терминале:

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

Предыдущая команда создает новое приложение Rails в каталоге с именем rails_react_recipe, устанавливает необходимые зависимости Ruby и JavaScript и настраивает Webpack. Флаги, связанные с этой командой генератора new, включают следующее:

  • Флаг -d указывает предпочтительное ядро базы данных, в данном случае это PostgreSQL.
  • Флаг -j указывает подход приложения к JavaScript. Rails предлагает несколько различных способов обработки кода Javascript в приложениях Rails. Опция esbuild, переданная флагу -j, указывает Rails предварительно настроить esbuild в качестве предпочтительного сборщика JavaScript.
  • Флаг -c указывает процессор CSS приложения. В этом случае предпочтительным вариантом является Bootstrap.
  • Флаг -T указывает Rails пропустить создание тестовых файлов, поскольку вы не будете писать тесты для этого руководства. Эта команда также рекомендуется, если вы хотите использовать инструмент тестирования Ruby, отличный от того, который предоставляет Rails.

После завершения команды перейдите в каталог rails_react_recipe, который является корневым каталогом вашего приложения:

  1. cd rails_react_recipe

Затем перечислите содержимое каталога:

  1. ls

Содержимое будет напечатано примерно так:

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

В этом корневом каталоге есть несколько автоматически сгенерированных файлов и папок, составляющих структуру приложения Rails, включая файл package.json, содержащий зависимости для приложения React.

Теперь, когда вы успешно создали новое приложение Rails, на следующем шаге вы подключите его к базе данных.

Шаг 2 — Настройка базы данных

Прежде чем запускать новое приложение Rails, вы должны сначала подключить его к базе данных. На этом шаге вы подключите только что созданное приложение Rails к базе данных PostgreSQL, чтобы данные рецепта можно было сохранять и извлекать по мере необходимости.

Файл database.yml, расположенный в config/database.yml, содержит сведения о базе данных, такие как имена баз данных для различных сред разработки. Rails указывает имя базы данных для различных сред разработки, добавляя символ подчеркивания (_), за которым следует имя среды. В этом руководстве вы будете использовать значения конфигурации базы данных по умолчанию, но при необходимости вы можете изменить свои значения конфигурации.

Примечание. На этом этапе вы можете изменить config/database.yml, чтобы указать, какую роль PostgreSQL вы хотите, чтобы Rails использовал для создания вашей базы данных. Во время предварительных требований вы создали роль, защищенную паролем, на шаге 4 — Настройка и создание базы данных в том же учебнике по предварительным требованиям.

Rails предлагает множество команд, упрощающих разработку веб-приложений, включая команды для работы с базами данных, такие как create, drop и reset. Чтобы создать базу данных для вашего приложения, выполните в терминале следующую команду:

  1. rails db:create

Эта команда создает базу данных development и test, что дает следующий результат:

Output
Created database 'rails_react_recipe_development' Created database 'rails_react_recipe_test'

Теперь, когда приложение подключено к базе данных, запустите приложение, выполнив следующую команду:

  1. bin/dev

Rails предоставляет альтернативный скрипт bin/dev, который запускает приложение Rails, выполняя команды в файле Procfile.dev в корневом каталоге приложения с помощью гема Foreman.

Как только вы запустите эту команду, ваша командная строка исчезнет, и вместо нее будет напечатан следующий вывод:

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

Чтобы получить доступ к приложению, откройте окно браузера и перейдите по адресу http://localhost:3000. Будет загружена страница приветствия 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:

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

Эта команда использует Yarn для установки указанных пакетов и добавляет их в файл package.json. Чтобы убедиться в этом, откройте файл package.json, расположенный в корневом каталоге проекта:

  1. 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:

  1. rails g controller Homepage index

Примечание:

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

Эта команда навсегда увеличит количество каталогов, которые вы можете отслеживать с помощью Listen to 524288. Вы можете изменить это снова, выполнив ту же команду и заменив 524288 на нужный номер.

При выполнении команды controller создаются следующие файлы:

  • Файл homepage_controller.rb для получения всех запросов, связанных с домашней страницей. Этот файл содержит действие index, указанное вами в команде.
  • Файл homepage_helper.rb для добавления вспомогательных методов, связанных с контроллером Homepage.
  • Файл index.html.erb в качестве страницы просмотра для отображения всего, что связано с главной страницей.

Помимо этих новых страниц, созданных с помощью команды Rails, Rails также обновляет ваш файл маршрутов, расположенный по адресу config/routes.rb, добавляя маршрут get для вашей домашней страницы, который вы будет изменен как ваш корневой маршрут.

Корневой маршрут в Rails указывает, что будет отображаться, когда пользователи посетят корневой URL-адрес вашего приложения. В этом случае вы хотите, чтобы ваши пользователи видели вашу домашнюю страницу. Откройте файл маршрутов, расположенный по адресу config/routes.rb, в вашем любимом редакторе:

  1. nano config/routes.rb

В этом файле замените get homepage/index на root homepage#index, чтобы файл соответствовал следующему:

Rails.application.routes.draw do
  root 'homepage#index'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

Эта модификация указывает Rails сопоставлять запросы к корню приложения с действием index контроллера Homepage, которое, в свою очередь, отображается в браузере. все, что находится в файле index.html.erb, расположенном по адресу app/views/homepage/index.html.erb.

Сохраните и закройте файл.

Чтобы убедиться, что это работает, запустите приложение:

  1. bin/dev

Когда вы открываете или обновляете приложение в браузере, загружается новая целевая страница для вашего приложения:

Убедившись, что ваше приложение работает, нажмите CTRL+C, чтобы остановить сервер.

Затем откройте файл ~/rails_react_recipe/app/views/homepage/index.html.erb:

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

Удалите код внутри файла, затем сохраните файл как пустой. Делая это, вы гарантируете, что содержимое index.html.erb не будет мешать рендерингу React вашего внешнего интерфейса.

Теперь, когда вы настроили домашнюю страницу для своего приложения, вы можете перейти к следующему разделу, где вы настроите внешний интерфейс вашего приложения для использования React.

Шаг 5 — Настройка React в качестве внешнего интерфейса Rails

На этом шаге вы настроите Rails для использования React во внешнем интерфейсе приложения вместо механизма шаблонов. Эта новая конфигурация позволит вам создать более привлекательную домашнюю страницу с помощью React.

С помощью параметра esbuild, указанного при создании приложения Rails, большая часть настроек, необходимых для беспрепятственной работы JavaScript с Rails, уже выполнена. Осталось только загрузить точку входа приложения React в точку входа esbuild для файлов JavaScript. Для этого начните с создания каталога компонентов в каталоге app/javascript:

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

Каталог components будет содержать компонент для домашней страницы вместе с другими компонентами React в приложении, включая файл входа в приложение React.

Затем откройте файл application.js, расположенный по адресу app/javascript/application.js:

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

Добавьте выделенную строку кода в файл:

// Entry point for the build script in your 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:

  1. 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:

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

Каталог routes будет содержать несколько маршрутов с соответствующими компонентами. Всякий раз, когда какой-либо указанный маршрут загружается, он отображает соответствующий компонент в браузере.

В каталоге routes создайте файл index.jsx:

  1. 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:

  1. 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:

  1. 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. Используя функцию ReactDOM createRoot, вы создаете корневой элемент в виде элемента div, присоединяемого к странице, и визуализируете в нем свой компонент App. Когда приложение загружается, React отображает содержимое компонента App внутри элемента div на странице.

Сохраните и закройте файл.

Наконец, вы добавите несколько стилей CSS на свою домашнюю страницу.

Откройте файл application.bootstrap.scss в каталоге ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss:

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

Затем замените содержимое файла application.bootstrap.scss следующим кодом:

@import 'bootstrap/scss/bootstrap';
@import 'bootstrap-icons/font/bootstrap-icons';

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

Вы устанавливаете некоторые пользовательские цвета для страницы. Раздел .hero создаст основу для главного изображения или большого веб-баннера на главной странице вашего веб-сайта, который вы добавите позже. Кроме того, custom-button.btn определяет стиль кнопки, которую пользователь будет использовать для входа в приложение.

Установив стили CSS, сохраните и закройте файл.

Затем перезапустите веб-сервер для вашего приложения:

  1. bin/dev

Затем перезагрузите приложение в браузере. Будет загружена новая домашняя страница:

Остановите веб-сервер с помощью CTRL+C.

На этом шаге вы настроили свое приложение для использования React в качестве внешнего интерфейса. На следующем шаге вы создадите модели и контроллеры, которые позволят вам создавать, читать, обновлять и удалять рецепты.

Шаг 6 — Создание контроллера рецепта и модели

Теперь, когда вы настроили интерфейс React для своего приложения, вы создадите модель Recipe и контроллер. Модель рецепта будет представлять собой таблицу базы данных, содержащую информацию о рецептах пользователя, а контроллер будет получать и обрабатывать запросы на создание, чтение, обновление или удаление рецептов. Когда пользователь запрашивает рецепт, контроллер рецепта получает этот запрос и передает его модели рецепта, которая извлекает запрошенные данные из базы данных. Затем модель возвращает данные рецепта в качестве ответа контроллеру. Наконец, эта информация отображается в браузере.

Начните с создания модели Recipe с помощью подкоманды generate model, предоставленной Rails, и укажите имя модели вместе с ее столбцами и типами данных. Выполните следующую команду:

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

Предыдущая команда указывает Rails создать модель Recipe вместе со столбцом name типа string, ингредиентами и Столбец instruction типа text и столбец image типа string. В этом руководстве модель названа Recipe, потому что модели в Rails используют имена в единственном числе, а соответствующие им таблицы базы данных используют имена во множественном числе.

Выполнение команды generate model создает два файла и выводит следующий вывод:

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

Два созданных файла:

  • Файл recipe.rb, содержащий всю логику, связанную с моделью.
  • Файл 20221017220817_create_recipes.rb (номер в начале файла может отличаться в зависимости от даты запуска команды). Этот файл миграции содержит инструкции по созданию структуры базы данных.

Далее вы отредактируете файл модели рецепта, чтобы в базу данных сохранялись только действительные данные. Вы можете добиться этого, добавив в свою модель некоторую проверку базы данных.

Откройте свою модель рецепта, расположенную по адресу app/models/recipe.rb:

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

Добавьте в файл следующие выделенные строки кода:

class Recipe < ApplicationRecord
  validates :name, presence: true
  validates :ingredients, presence: true
  validates :instruction, presence: true
end

В этом коде вы добавляете проверку модели, которая проверяет наличие полей название, ингредиенты и инструкция. Без этих трех полей рецепт недействителен и не будет сохранен в базе данных.

Сохраните и закройте файл.

Чтобы Rails создал таблицу recipes в вашей базе данных, вы должны запустить миграцию, которая представляет собой способ программного внесения изменений в вашу базу данных. Чтобы миграция работала с настроенной вами базой данных, необходимо внести изменения в файл 20221017220817_create_recipes.rb.

Откройте этот файл в вашем редакторе:

  1. 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 для названия, ингредиентов и столбцы instruction, добавив null: false и убедившись, что эти столбцы имеют значение перед изменением базы данных. Наконец, вы добавляете URL-адрес изображения по умолчанию для своего столбца изображений; это может быть другой URL, если вы хотите использовать другое изображение.

С этими изменениями сохраните и закройте файл. Теперь вы готовы запустить миграцию и создать таблицу. В терминале выполните следующую команду:

  1. rails db:migrate

Вы используете команду миграции базы данных для выполнения инструкций в файле миграции. После успешного выполнения команды вы получите вывод, подобный следующему:

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

Создав модель рецепта, вы затем создадите свой контроллер рецептов, чтобы добавить логику для создания, чтения и удаления рецептов. Выполните следующую команду:

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

В этой команде вы создаете контроллер Recipes в каталоге api/v1 с index, create, show и действие destroy. Действие index будет обрабатывать выборку всех ваших рецептов; действие create будет отвечать за создание новых рецептов; действие show извлечет один рецепт, а действие destroy будет содержать логику удаления рецепта.

Вы также передаете некоторые флаги, чтобы сделать контроллер более легким, в том числе:

  • --skip-template-engine, который указывает Rails пропустить создание файлов представлений Rails, поскольку React обрабатывает ваши потребности во внешнем интерфейсе.
  • --no-helper, который указывает Rails пропустить создание вспомогательного файла для вашего контроллера.

Выполнение команды также обновляет ваш файл маршрутов маршрутом для каждого действия в контроллере Recipes.

Когда команда запустится, она напечатает такой вывод:

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

Чтобы использовать эти маршруты, вы внесете изменения в файл config/routes.rb. Откройте файл routes.rb в текстовом редакторе:

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

Обновите этот файл, чтобы он выглядел как следующий код, изменив или добавив выделенные строки:

Rails.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'
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Defines the root path route ("/")
  # root "articles#index"
end

В этом файле маршрута вы изменяете глагол HTTP маршрутов create и destroy, чтобы он мог отправлять и удалять данные. Вы также изменяете маршруты для действий show и destroy, добавляя к маршруту параметр :id. :id будет содержать идентификационный номер рецепта, который вы хотите прочитать или удалить.

Вы добавляете маршрут перехвата с помощью get /*path, который будет направлять любой другой запрос, не соответствующий существующим маршрутам, в действие index на главной странице. контроллер. Интерфейсная маршрутизация будет обрабатывать запросы, не связанные с созданием, чтением или удалением рецептов.

Сохраните и закройте файл.

Чтобы оценить список маршрутов, доступных в вашем приложении, выполните следующую команду:

  1. rails routes

Выполнение этой команды отображает длинный список шаблонов URI, глаголов и соответствующих контроллеров или действий для вашего проекта.

Далее вы добавите логику для получения всех рецептов одновременно. Rails использует библиотеку ActiveRecord для обработки подобных задач, связанных с базой данных. ActiveRecord соединяет классы с таблицами реляционной базы данных и предоставляет богатый API для работы с ними.

Чтобы получить все рецепты, вы будете использовать ActiveRecord для запроса таблицы рецептов и извлечения всех рецептов из базы данных.

Откройте файл recipes_controller.rb с помощью следующей команды:

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

Добавьте выделенные строки в контроллер рецептов:

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 вы используете метод ActiveRecord all, чтобы получить все рецепты в вашей базе данных. Используя метод 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 вы используете метод ActiveRecord create для создания нового рецепта. Метод create может одновременно назначить все параметры контроллера, представленные в модели. Этот метод упрощает создание записей, но открывает возможность злонамеренного использования. Злонамеренное использование может быть предотвращено с помощью функции надежных параметров, предоставляемой Rails. Таким образом, параметры не могут быть назначены, если они не разрешены. Вы передаете параметр recipe_params методу create в своем коде. recipe_params – это метод private, в котором вы разрешаете параметрам вашего контроллера предотвращать попадание неправильного или вредоносного содержимого в вашу базу данных. В этом случае вы разрешаете параметры name, image, ingredients и instruction для допустимого использования создать метод.

Теперь ваш контроллер рецептов может читать и создавать рецепты. Остается только логика чтения и удаления одного рецепта. Обновите контроллер рецептов выделенным кодом:

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 использует метод ActiveRecord find для поиска рецепта, чей id соответствует id, предоставленному в params и присваивает его переменной экземпляра @recipe. В действии show вы возвращаете объект @recipe, заданный методом set_recipe, в качестве ответа JSON.

В действии destroy вы сделали нечто подобное, используя оператор безопасной навигации Ruby &., который позволяет избежать ошибок nil при вызове метода. Это дополнение позволяет вам удалить рецепт, только если он существует, а затем отправить сообщение в качестве ответа.

После внесения этих изменений в recipes_controller.rb сохраните и закройте файл.

На этом шаге вы создали модель и контроллер для своих рецептов. Вы написали всю логику, необходимую для работы с рецептами на бэкенде. В следующем разделе вы создадите компоненты для просмотра ваших рецептов.

Шаг 7 — Просмотр рецептов

В этом разделе вы будете создавать компоненты для просмотра рецептов. Вы создадите две страницы: одну для просмотра всех существующих рецептов и другую для просмотра отдельных рецептов.

Вы начнете с создания страницы для просмотра всех рецептов. Перед созданием страницы вам нужны рецепты для работы, так как ваша база данных в настоящее время пуста. Rails предоставляет способ создания начальных данных для вашего приложения.

Откройте исходный файл с именем seeds.rb для редактирования:

  1. 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, ингредиентов и инструкций. Сохраните и закройте файл.

Чтобы заполнить базу данных этими данными, выполните следующую команду в своем терминале:

  1. rails db:seed

Выполнение этой команды добавит девять рецептов в вашу базу данных. Теперь вы можете получить их и отобразить во внешнем интерфейсе.

Компонент для просмотра всех рецептов сделает HTTP-запрос к действию index в RecipesController, чтобы получить список всех рецептов. Эти рецепты затем будут отображаться в карточках на странице.

Создайте файл Recipes.jsx в каталоге app/javascript/components:

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

После открытия файла импортируйте React, useState, useEffect, Link и useNavigate модули, добавив следующие строки:

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 вызовет хук useState, который инициализирует состояние 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:

  1. 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>
);

Сохраните и закройте файл.

На этом этапе рекомендуется убедиться, что ваш код работает должным образом. Как и раньше, используйте следующую команду для запуска сервера:

  1. bin/dev

Затем откройте приложение в браузере. Нажмите кнопку «Просмотреть рецепт» на главной странице, чтобы открыть страницу с вашими рецептами семян:

Используйте CTRL+C в своем терминале, чтобы остановить сервер и вернуться к подсказке.

Теперь, когда вы можете просматривать все рецепты в своем приложении, пришло время создать второй компонент для просмотра отдельных рецептов. Создайте файл Recipe.jsx в каталоге app/javascript/components:

  1. 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 param из объекта params. Как только вы получите параметр id рецепта, вы сделаете HTTP-запрос для получения рецепта. Добавьте выделенные строки в ваш файл:

import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

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

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

export default Recipe;

В хуке useEffect вы используете значение params.id, чтобы сделать HTTP-запрос GET для получения рецепта, которому принадлежит id, а затем сохраните его в состоянии компонента с помощью функции setRecipe. Приложение перенаправляет пользователя на страницу рецептов, если рецепт не существует.

Затем добавьте функцию addHtmlEntities, которая будет использоваться для замены символов в компоненте объектами HTML. Функция addHtmlEntities примет строку и заменит все экранированные открывающие и закрывающие скобки их объектами HTML. Эта функция поможет вам преобразовать любой экранированный символ, который был сохранен в вашей инструкции рецепта. Добавьте выделенные строки:

import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

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

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

  const addHtmlEntities = (str) => {
    return String(str).replace(/&lt;/g, "<").replace(/&gt;/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(/&lt;/g, "<").replace(/&gt;/g, ">");
  };

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

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

    return ingredientList;
  };

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

export default Recipe;

С помощью функции ingredientList вы разделяете ингредиенты рецепта, разделенные запятыми, на массив и сопоставляете его, чтобы создать список ингредиентов. Если ингредиентов нет, приложение отображает сообщение «Нет доступных ингредиентов». Вы также заменяете все открывающие и закрывающие скобки в инструкции рецепта, передавая ее через функцию addHtmlEntities. Наконец, код отображает изображение рецепта в качестве главного изображения, добавляет кнопку «Удалить рецепт» рядом с инструкцией по рецепту и добавляет кнопку, которая ссылается на страницу рецептов.

Примечание. Использование атрибута React dangerouslySetInnerHTML рискованно, так как оно подвергает ваше приложение атакам межсайтового скриптинга. Этот риск снижается за счет замены специальных символов, введенных при создании рецептов, с помощью функции stripHtmlEntities, объявленной в компоненте NewRecipe.

Сохраните и закройте файл.

Чтобы просмотреть компонент Recipe на странице, вы добавите его в свой файл маршрутов. Откройте файл маршрута для редактирования:

  1. 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 param, который будет заменен id рецепта, который вы хотите просмотреть.

Сохраните и закройте файл.

Используйте сценарий bin/dev, чтобы снова запустить сервер, а затем перейдите на страницу http://localhost:3000 в браузере. Нажмите кнопку «Просмотреть рецепты», чтобы перейти на страницу рецептов. На странице рецептов откройте любой рецепт, нажав кнопку «Просмотреть рецепт». Вас встретит страница, заполненная данными из вашей базы данных:

Вы можете остановить сервер с помощью CTRL+C.

На этом шаге вы добавили девять рецептов в свою базу данных и создали компоненты для просмотра этих рецептов как по отдельности, так и в виде коллекции. На следующем шаге вы добавите компонент для создания рецептов.

Шаг 8 — Создание рецептов

Следующим шагом к созданию удобного приложения для рецептов еды является возможность создавать новые рецепты. На этом шаге вы создадите компонент для этой функции. Компонент будет содержать форму для сбора необходимых сведений о рецепте от пользователя, а затем сделать запрос к действию create в контроллере Recipe для сохранения данных рецепта.

Создайте файл NewRecipe.jsx в каталоге app/javascript/components:

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

В новый файл импортируйте модули React, useState, Link и useNavigate, которые вы использовали в других компонентах:

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, ингредиенты и состояние инструкции, каждое из которых имеет свои функции обновления. Это поля, которые вам понадобятся для создания действительного рецепта.

Затем создайте функцию 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, "&lt;")
      .replace(/>/g, "&gt;");
  };
};

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, "&lt;")
      .replace(/>/g, "&gt;");
  };

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

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

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

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

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

export default NewRecipe;

Функция onChange принимает пользовательский ввод event и функцию установки состояния, а затем обновляет состояние с помощью введенного пользователем значения. В функции onSubmit вы проверяете, что ни один из обязательных входных данных не пуст. Затем вы создаете объект, содержащий параметры, необходимые для создания нового рецепта. Используя функцию stripHtmlEntities, вы заменяете символы < и > в инструкции рецепта их экранированным значением и заменяете каждый новый символ строки разрывом. тег, тем самым сохраняя текстовый формат, введенный пользователем. Наконец, вы делаете HTTP-запрос POST, чтобы создать новый рецепт и перенаправить на его страницу в случае успешного ответа.

Для защиты от токена CSRF, встроенного в ваш HTML-документ, Rails делает HTTP-запрос со строкой JSON. Если рецепт успешно создан, приложение перенаправляет пользователя на страницу рецепта, где он может просмотреть только что созданный рецепт.

Наконец, верните разметку, которая отображает форму для ввода пользователем сведений о рецепте, который пользователь хочет создать. Добавьте выделенные строки:

import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

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

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

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

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

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

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

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

  return (
    <div className="container mt-5">
      <div className="row">
        <div className="col-sm-12 col-lg-6 offset-lg-3">
          <h1 className="font-weight-normal mb-5">
            Add a new recipe to our awesome recipe collection.
          </h1>
          <form onSubmit={onSubmit}>
            <div className="form-group">
              <label htmlFor="recipeName">Recipe name</label>
              <input
                type="text"
                name="name"
                id="recipeName"
                className="form-control"
                required
                onChange={(event) => onChange(event, setName)}
              />
            </div>
            <div className="form-group">
              <label htmlFor="recipeIngredients">Ingredients</label>
              <input
                type="text"
                name="ingredients"
                id="recipeIngredients"
                className="form-control"
                required
                onChange={(event) => onChange(event, setIngredients)}
              />
              <small id="ingredientsHelp" className="form-text text-muted">
                Separate each ingredient with a comma.
              
            </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, которая отправляет данные формы.

Сохраните и закройте файл.

Чтобы получить доступ к этому компоненту в браузере, обновите файл маршрута, указав его маршрут:

  1. 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, включив в него возможность удаления рецептов. При нажатии кнопки удаления на странице рецепта приложение отправит запрос на удаление рецепта из базы данных.

Сначала откройте файл Recipe.jsx для редактирования:

  1. 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(/&lt;/g, "<").replace(/&gt;/g, ">");
  };

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

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

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

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

    return ingredientList;
  };

  const recipeInstruction = addHtmlEntities(recipe.instruction);

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

В функции deleteRecipe вы получаете id удаляемого рецепта, затем создаете свой URL-адрес и получаете токен CSRF. Затем вы отправляете запрос DELETE контроллеру 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(/&lt;/g, "<").replace(/&gt;/g, ">");
  };

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

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

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

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

    return ingredientList;
  };

  const recipeInstruction = addHtmlEntities(recipe.instruction);

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

export default Recipe;

Сохраните и закройте файл.

Перезапустите сервер приложений и перейдите на домашнюю страницу. Нажмите кнопку «Просмотреть рецепты», чтобы получить доступ ко всем существующим рецептам, затем откройте любой конкретный рецепт и нажмите кнопку «Удалить рецепт» на странице, чтобы удалить статью. Вы будете перенаправлены на страницу рецептов, и удаленный рецепт больше не будет существовать.

Теперь, когда кнопка удаления работает, у вас есть полнофункциональное приложение рецептов!

Заключение

В этом руководстве вы создали приложение рецепта еды с Ruby on Rails и интерфейсом React, используя PostgreSQL в качестве базы данных и Bootstrap для стилей. Если вы хотите продолжить разработку с Ruby on Rails, рассмотрите возможность следовать нашему разделу «Как отображать данные из API DigitalOcean с помощью React».