如何在 Ubuntu 20.04 上使用 React 前端设置 Ruby on Rails v7 项目

2023-11-18

作者选择了电子前沿基金会接受捐赠作为为捐款而写程序。

介绍

红宝石 on Rails是一个流行的服务器端 Web 应用程序框架。它为当今网络上存在的许多流行应用程序提供支持,例如GitHub, Basecamp, 声云, Airbnb, and Twitch。 Ruby on Rails 强调程序员经验以及围绕它构建的热情社区,将为您提供构建和维护现代 Web 应用程序所需的工具。

React是一个用于创建前端用户界面的 JavaScript 库。在 Facebook 的支持下,它是当今网络上最流行的前端库之一。 React 提供了类似的功能虚拟文档对象模型 (DOM), 组件架构, and 状态管理,使得前端开发的过程更加有组织、高效。

随着 Web 前端向与服务器端代码分离的框架发展,将 Rails 的优雅与 React 的高效相结合将使您能够根据当前趋势构建强大且现代的应用程序。通过使用 React 从 Rails 视图(而不是 Rails 模板引擎)中渲染组件,您的应用程序将受益于 JavaScript 和前端开发的最新进展,同时利用 Ruby on Rails 的表现力。

在本教程中,您将创建一个 Ruby on Rails 应用程序,用于存储您最喜欢的食谱,然后使用 React 前端显示它们。完成后,您将能够使用 React 界面创建、查看和删除食谱,样式为引导程序:

Screencapture of the completed recipe app home page

先决条件

要学习本教程,您需要:

  • Node.js and npm安装在您的开发机器上。本教程使用 Node.js 版本 16.14.0 和 npm 版本 8.3.1。 Node.js 是一个 JavaScript 运行时环境,允许您在浏览器之外运行代码。它带有一个预安装的包管理器,称为npm,它允许您安装和更新软件包。要在 Ubuntu 20.04 或 macOS 上安装这些软件,请按照“使用 PPA 安装”部分进行操作如何在 Ubuntu 20.04 上安装 Node.js或中的步骤如何在 macOS 上安装 Node.js 并创建本地开发环境.

  • Yarn 包管理器安装在您的开发计算机上,它允许您下载 React 框架。本教程在1.22.10版本上测试;要安装此依赖项,请按照官方 Yarn 安装指南.

  • 安装了 Ruby on Rails。要获得此信息,请按照我们的指南进行操作如何在 Ubuntu 20.04 上使用 rbenv 安装 Ruby on Rails。如果您想在 macOS 上开发此应用程序,您可以使用如何在 macOS 上使用 rbenv 安装 Ruby on Rails。本教程在 Ruby 版本 3.1.2 和 Rails 版本 7.0.4 上进行了测试,因此请确保在安装过程中指定这些版本。

Note:Rails 版本 7 不向后兼容。如果您使用的是 Rails 版本 5,请访问教程如何在 Ubuntu 18.04 上使用 React 前端设置 Ruby on Rails v5 项目.

  • 已安装 PostgreSQL,如步骤 1 和 2 中所述如何在 Ubuntu 20.04 上将 PostgreSQL 与 Ruby on Rails 应用程序结合使用 or 如何在 macOS 上将 PostgreSQL 与 Ruby on Rails 应用程序结合使用。要学习本教程,您可以使用 PostgreSQL 版本 12 或更高版本。如果您想在不同的 Linux 发行版或其他操作系统上开发此应用程序,请参阅官方 PostgreSQL 下载页面。有关如何使用 PostgreSQL 的更多信息,请参阅如何安装和使用 PostgreSQL.

第 1 步 — 创建新的 Rails 应用程序

在此步骤中,您将在 Rails 应用程序框架上构建菜谱应用程序。首先,您将创建一个新的 Rails 应用程序,将其设置为与 React 一起使用。

Rails 提供了几个脚本,称为发电机它创建了构建现代 Web 应用程序所需的一切。要查看这些命令及其用途的完整列表,请在终端中运行以下命令:

  1. rails -h

该命令将产生一个完整的选项列表,允许您设置应用程序的参数。列出的命令之一是new命令,该命令创建一个新的 Rails 应用程序。

现在,您将使用以下命令创建一个新的 Rails 应用程序new发电机。在终端中运行以下命令:

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

The preceding command creates a new Rails application in a directory named rails_react_recipe, installs the required Ruby and JavaScript dependencies, and configures Webpack. The flags associated with this new generator command include the following:

  • The -dflag 指定首选数据库引擎,在本例中为 PostgreSQL。
  • The -jflag 指定应用程序的 JavaScript 方法。 Rails 提供了几种不同的方法来处理 Rails 应用程序中的 Javascript 代码。这esbuild选项传递给-jflag 指示 Rails 进行预配置esbuild作为首选的 JavaScript 捆绑器。
  • The -cflag 指定应用程序的 CSS 处理器。在这种情况下,Bootstrap 是首选。
  • The -Tflag 指示 Rails 跳过测试文件的生成,因为您不会为本教程编写测试。如果您想使用与 Rails 提供的工具不同的 Ruby 测试工具,也建议使用此命令。

Once the command has finished, move to the rails_react_recipe directory, which is the root directory of your app:

  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 数据库,以便可以根据需要存储和获取配方数据。

The database.yml文件发现于config/database.yml包含数据库详细信息,例如不同开发环境的数据库名称。 Rails 通过附加下划线指定各种开发环境的数据库名称 (_) 后跟环境名称。在本教程中,您将使用默认的数据库配置值,但如果需要,您可以更改配置值。

Note:此时,您可以更改config/database.yml设置您希望 Rails 使用哪个 PostgreSQL 角色来创建数据库。在先决条件期间,您创建了一个由密码保护的角色如何将 PostgreSQL 与 Ruby on Rails 应用程序结合使用教程。如果您尚未设置用户,现在可以按照以下说明进行操作第 4 步 — 配置和创建数据库在相同的先决条件教程中。

Rails 提供了许多命令,使开发 Web 应用程序变得容易,包括使用数据库的命令,例如create, drop, and reset。要为您的应用程序创建数据库,请在终端中运行以下命令:

  1. Rails 数据库:创建

该命令创建一个development and test数据库,产生以下输出:

Output
Created database 'rails_react_recipe_development' Created database 'rails_react_recipe_test'

现在应用程序已连接到数据库,请通过运行以下命令启动应用程序:

  1. bin/dev

Rails 提供了一种替代方案bin/dev通过执行以下命令来启动 Rails 应用程序的脚本Procfile.dev使用以下命令在应用程序的根目录中创建文件Foreman gem.

运行此命令后,命令提示符将消失,并且将在其位置打印以下输出:

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 应用程序:

Screencapture of the Rails welcome page

要停止 Web 服务器,请按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

然后您的终端提示将重新出现。

您已成功为您的食品配方应用程序设置了数据库。在下一步中,您将安装组装 React 前端所需的 JavaScript 依赖项。

第 3 步 — 安装前端依赖项

在此步骤中,您将安装食品食谱应用程序前端所需的 JavaScript 依赖项。他们包括:

  • React用于构建用户界面。
  • 反应 DOM使 React 能够与浏览器 DOM 交互。
  • 反应路由器用于处理 React 应用程序中的导航。

运行以下命令以使用 Yarn 包管理器安装这些包:

  1. yarn add反应反应-dom反应-路由器-dom

该命令使用 Yarn 安装指定的包并将它们添加到package.json文件。要验证这一点,请打开package.json文件位于项目根目录:

  1. nano包.json

已安装的软件包将列在dependencies key:

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

按关闭文件CTRL+X.

您已经为您的应用程序安装了一些前端依赖项。接下来,您将为食品食谱应用程序设置主页。

第 4 步 — 设置主页

安装所需的依赖项后,您现在将为应用程序创建一个主页,以作为用户首次访问应用程序时的登录页面。

Rails 遵循模型-视图-控制器应用程序的架构模式。在 MVC 模式中,控制器的目的是接收特定请求并将它们传递到适当的模型或视图。当前,当浏览器中加载根 URL 时,应用程序会显示 Rails 欢迎页面。要更改此设置,您将为主页创建一个控制器和视图,然后将其与路线匹配。

Rails 提供了一个controller用于创建控制器的生成器。这controller生成器接收控制器名称和匹配的操作。有关这方面的更多信息,您可以查看Rails 文档.

This tutorial will call the controller Homepage. Run the following command to create a Homepage controller with an index action:

  1. rails g controller Homepage index

Note:在 Linux 上,错误FATAL: Listen error: unable to monitor directories for changes.可能是由于系统对您的计算机可以监视更改的文件数量进行了限制。运行以下命令来修复它:

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

此命令将永久增加您可以监视的目录数量Listen to 524288。您可以通过运行相同的命令并替换来再次更改此设置524288与您想要的号码。

运行controller命令生成以下文件:

  • A homepage_controller.rb用于接收所有与主页相关的请求的文件。该文件包含index您在命令中指定的操作。
  • A homepage_helper.rb file for adding helper methods related to the Homepage controller.
  • An index.html.erb文件作为视图页面,用于呈现与主页相关的任何内容。

除了通过运行 Rails 命令创建的这些新页面之外,Rails 还会更新位于以下位置的路由文件:config/routes.rb,添加一个get您的主页的路由,您将其修改为根路由。

Rails 中的根路由指定当用户访问应用程序的根 URL 时将显示的内容。在这种情况下,您希望用户看到您的主页。打开位于以下位置的路由文件config/routes.rb在你最喜欢的编辑器中:

  1. nano配置/routes.rb

In this file, replace get 'homepage/index' with root 'homepage#index' so that the file matches the following:

〜/rails_react_recipe/config/routes.rb
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

This modification instructs Rails to map requests to the root of the application to the index action of the Homepage controller, which in turn renders in the browser whatever is in the index.html.erb file located at app/views/homepage/index.html.erb.

保存并关闭文件。

要验证这是否有效,请启动您的应用程序:

  1. bin/dev

当您在浏览器中打开或刷新应用程序时,将加载应用程序的新登录页面:

The "Homepage#index" Application page will load

一旦您确认您的应用程序正在运行,请按CTRL+C停止服务器。

接下来,打开~/rails_react_recipe/app/views/homepage/index.html.erb file:

  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 应用程序的入口点加载到esbuildJavaScript 文件的入口点。为此,首先在app/javascript目录:

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

The components目录将容纳主页的组件以及应用程序中的其他 React 组件,包括 React 应用程序的入口文件。

接下来,打开application.js文件位于app/javascript/application.js:

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

将突出显示的代码行添加到文件中:

〜/rails_react_recipe/app/javascript/application.js
// 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导入到 Rails 应用程序的 JavaScript 入口点的目录中,您可以为主页创建一个 React 组件。主页将包含一些文本和一个号召性用语按钮来查看所有食谱。

保存并关闭文件。

然后,创建一个Home.jsx文件在components目录:

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

将以下代码添加到文件中:

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

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

在此代码中,您导入 React 和Link来自 React Router 的组件。这Link组件创建一个超链接以从一个页面导航到另一页面。然后,您可以为主页创建并导出包含某种标记语言的功能组件,并使用 Bootstrap 类进行样式设置。

保存并关闭文件。

和你的Home组件集,您现在将使用 React Router 设置路由。创建一个routes目录中的app/javascript目录:

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

The routes目录将包含一些路由及其相应的组件。每当加载任何指定的路由时,它都会将其相应的组件呈现给浏览器。

In the routes目录,创建一个index.jsx file:

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

添加以下代码:

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

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

In this index.jsx路由文件中,您导入以下模块:React允许您使用 React 的模块,以及BrowserRouter, Routes, and RouteReact Router 的模块,它们一起帮助您从一条路线导航到另一条路线。最后,您导入您的Home组件,每当请求与根(/) 路线。当您想要向应用程序添加更多页面时,您可以在此文件中声明一个路由,并将其与您想要为该页面呈现的组件相匹配。

保存并退出文件。

您现在已经使用 React Router 设置了路由。为了让 React 了解可用的路由并使用它们,这些路由必须在应用程序的入口点可用。为了实现这一点,您将在一个组件中渲染您的路由,React 将在您的入口文件中渲染该组件。

创建一个App.jsx文件在app/javascript/components目录:

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

将以下代码添加到App.jsx file:

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

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

In the App.jsx文件中,您导入 React 和刚刚创建的路由文件。然后导出一个组件来渲染其中的路线碎片。该组件将在应用程序的入口点呈现,使路由在应用程序加载时可用。

保存并关闭文件。

现在你有了你的App.jsx设置后,您可以将其呈现在您的条目文件中。创建一个index.jsx文件在components目录:

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

将以下代码添加到index.js file:

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

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

In the import行,导入 React 库,createRoot来自 ReactDOM 的函数,以及你的App成分。使用 ReactDOMcreateRoot函数,您创建一个根元素作为div元素附加到页面,然后渲染您的App其中的组件。当应用程序加载时,React 将渲染应用程序的内容App里面的组件div页面上的元素。

保存并退出文件。

最后,您将向主页添加一些 CSS 样式。

打开application.bootstrap.scss文件在你的~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss目录:

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

接下来,将内容替换为application.bootstrap.scss文件包含以下代码:

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

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

您为页面设置一些自定义颜色。这.hero部分将创建一个框架英雄形象,或者您网站首页上的大型网页横幅,您稍后将添加。此外,custom-button.btn设置用户用于进入应用程序的按钮的样式。

设置好 CSS 样式后,保存并退出文件。

接下来,重新启动应用程序的 Web 服务器:

  1. bin/dev

然后在浏览器中重新加载该应用程序。将加载一个全新的主页:

The homepage with its new styling

停止网络服务器CTRL+C.

在此步骤中,您将应用程序配置为使用 React 作为其前端。在下一步中,您将创建模型和控制器,使您能够创建、读取、更新和删除配方。

第 6 步 — 创建配方控制器和模型

现在您已经为应用程序设置了 React 前端,您将创建一个 Recipe 模型和控制器。菜谱模型将表示包含有关用户菜谱信息的数据库表,而控制器将接收并处理创建、读取、更新或删除菜谱的请求。当用户请求配方时,配方控制器接收该请求并将其传递给配方模型,配方模型从数据库检索请求的数据。然后,模型返回配方数据作为对控制器的响应。最后,该信息显示在浏览器中。

首先创建一个Recipe模型使用generate modelRails 提供的子命令,指定模型的名称及其列和数据类型。运行以下命令:

  1. Rails 生成模型 配方名称:字符串 成分:文本 说明:文本 图片:字符串

前面的命令指示 Rails 创建一个Recipe模型连同name类型列string, an ingredients and 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保存所有与模型相关的逻辑的文件。
  • A 20221017220817_create_recipes.rb file (the number at the beginning of the file may differ depending on the date when you run the command). This migration file contains the instruction for creating the database structure.

接下来,您将编辑配方模型文件以确保仅将有效数据保存到数据库中。您可以通过向模型添加一些数据库验证来实现此目的。

打开位于以下位置的配方模型app/models/recipe.rb:

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

将以下突出显示的代码行添加到文件中:

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

在此代码中,您添加模型验证,以检查是否存在name, ingredients, and instruction字段。没有这三个字段,菜谱无效,不会保存到数据库中。

保存并关闭文件。

For Rails to create the recipes table in your database, you have to run a migration, which is a way to make changes to your database programmatically. To ensure that the migration works with the database you set up, you must make changes to the 20221017220817_create_recipes.rb file.

在编辑器中打开此文件:

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

添加突出显示的材料,使您的文件与以下内容匹配:

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

      t.timestamps
    end
  end
end

This migration file contains a Ruby class with a change method and a command to create a table called recipes along with the columns and their data types. You also update 20221017220817_create_recipes.rb with a NOT NULL constraint on the name, ingredients, and instruction columns by adding null: false, ensuring that these columns have a value before changing the database. Finally, you add a default image URL for your image column; this could be another URL if you want to use a different image.

完成这些更改后,保存并退出文件。您现在已准备好运行迁移并创建表。在您的终端中,运行以下命令:

  1. Rails 数据库:迁移

您可以使用数据库迁移命令来运行迁移文件中的说明。命令成功运行后,您将收到类似于以下内容的输出:

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

菜谱模型就位后,您接下来将创建菜谱控制器以添加用于创建、读取和删除菜谱的逻辑。运行以下命令:

  1. Rails 生成控制器 api/v1/Recipes 索引 create show destroy --skip-template-engine --no-helper

在此命令中,您创建一个Recipes控制器中的api/v1目录与index, create, show, and destroy行动。这index操作将处理获取所有食谱;这create行动将负责创造新的食谱;这show操作将获取单个食谱,并且destroy操作将保存删除菜谱的逻辑。

您还可以传递一些标志以使控制器更加轻量级,包括:

  • --skip-template-engine,它指示 Rails 跳过生成 Rails 视图文件,因为 React 会处理您的前端需求。
  • --no-helper,它指示 Rails 跳过为控制器生成帮助程序文件。

运行该命令还会更新您的路由文件,其中包含每个操作的路由Recipes控制器。

当命令运行时,它将打印如下输出:

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

要使用这些路线,您需要更改您的config/routes.rb文件。打开routes.rb在文本编辑器中创建文件:

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

更新此文件,使其类似于以下代码,更改或添加突出显示的行:

〜/rails_react_recipe/config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      get 'recipes/index'
      post 'recipes/create'
      get '/show/:id', to: 'recipes#show'
      delete '/destroy/:id', to: 'recipes#destroy'
    end
  end
  root 'homepage#index'
  get '/*path' => 'homepage#index'
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

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

在此路由文件中,您可以修改create and destroy路线,以便它可以post and delete数据。您还可以修改show and destroy通过添加一个动作:id路由的参数。:id将保存您要读取或删除的菜谱的标识号。

您添加一条包罗万象的路线get '/*path'这会将与现有路由不匹配的任何其他请求定向到index的行动homepage控制器。前端路由将处理与创建、读取或删除菜谱无关的请求。

保存并退出文件。

要评估应用程序中可用的路由列表,请运行以下命令:

  1. 铁路路线

运行此命令会显示一长串 URI 模式、动词以及与您的项目匹配的控制器或操作。

接下来,您将添加逻辑以立即获取所有食谱。 Rails 使用活动记录库来处理这样的数据库相关任务。 ActiveRecord 将类连接到关系数据库表,并提供丰富的 API 来使用它们。

要获取所有食谱,您将使用 ActiveRecord 查询食谱表并获取数据库中的所有食谱。

打开recipes_controller.rb使用以下命令创建文件:

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

将突出显示的行添加到菜谱控制器:

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

  def create
  end

  def show
  end

  def destroy
  end
end

In your index动作,你使用ActiveRecord的all获取数据库中所有食谱的方法。使用order方法中,您可以按创建日期降序排列它们,这会将最新的食谱放在第一位。最后,您将食谱列表作为 JSON 响应发送render.

接下来,您将添加创建新食谱的逻辑。与获取所有菜谱一样,您将依靠 ActiveRecord 来验证并保存提供的菜谱详细信息。使用以下突出显示的代码行更新您的配方控制器:

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

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

  def show
  end

  def destroy
  end

  private

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

In the create动作,你使用ActiveRecord的create创建新配方的方法。这create方法可以一次性将提供的所有控制器参数分配给模型。这种方法可以很容易地创建记录,但也带来了恶意使用的可能性。可以通过使用来防止恶意使用参数强Rails 提供的功能。这样,除非允许,否则无法分配参数。你通过一个recipe_params参数到create代码中的方法。这recipe_params is a private允许控制器参数防止错误或恶意内容进入数据库的方法。在这种情况下,您允许name, image, ingredients, and instruction参数的有效使用create method.

您的食谱控制器现在可以读取和创建食谱。剩下的就是读取和删除单个菜谱的逻辑。使用突出显示的代码更新您的食谱控制器:

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

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

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

  def show
    render json: @recipe
  end

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

  private

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

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

在新的代码行中,您创建一个私有的set_recipe调用的方法before_action仅当show and delete操作与请求匹配。这set_recipe方法使用 ActiveRecord 的find找到食谱的方法id匹配id中提供的params并将其分配给实例变量@recipe。在里面show行动,你返回@recipe对象设置由set_recipe方法作为 JSON 响应。

In the destroy动作,您使用 Ruby 的安全导航运算符做了类似的事情&.,这避免了nil调用方法时出错。通过此添加,您可以仅删除存在的配方,然后发送消息作为响应。

进行这些更改后recipes_controller.rb, 保存并关闭文件。

在此步骤中,您为配方创建了模型和控制器。您已经编写了在后端使用菜谱所需的所有逻辑。在下一部分中,您将创建组件来查看您的食谱。

第 7 步 — 查看菜谱

在本部分中,您将创建用于查看菜谱的组件。您将创建两个页面:一个用于查看所有现有食谱,另一个用于查看各个食谱。

您将首先创建一个页面来查看所有食谱。在创建页面之前,您需要使用食谱,因为您的数据库当前为空。 Rails 提供了一种为应用程序创建种子数据的方法。

打开名为的种子文件seeds.rb用于编辑:

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

将种子文件的初始内容替换为以下代码:

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

在此代码中,您使用一个循环来指示 Rails 创建九个配方,其中包含以下部分:name, ingredients, and instruction。保存并退出文件。

要使用此数据为数据库播种,请在终端中运行以下命令:

  1. Rails 数据库:种子

运行此命令会将九个食谱添加到您的数据库中。现在您可以获取它们并在前端渲染它们。

查看所有菜谱的组件将向index行动于RecipesController获取所有食谱的列表。这些食谱随后将显示在页面上的卡片中。

创建一个Recipes.jsx文件在app/javascript/components目录:

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

文件打开后,导入React, useState, useEffect, Link, and useNavigate通过添加以下行来模块:

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

接下来,添加突出显示的行以创建并导出名为的功能性 React 组件Recipes:

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

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

export default Recipes;

在 - 的里面Recipe组件,React Router 的导航 API 将调用使用导航钩。反应的useState钩子将初始化recipesstate,这是一个空数组([]),和一个setRecipes更新函数recipes state.

接下来,在一个useEffect钩子后,您将发出 HTTP 请求来获取所有食谱。为此,请添加突出显示的行:

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

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

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

export default Recipes;

In your useEffect钩子,您使用 HTTP 调用来获取所有食谱获取API。如果响应成功,应用程序会将食谱数组保存到recipes状态。如果发生错误,它将把用户重定向到主页。

最后,返回渲染组件时将在浏览器页面上评估并显示的元素的标记。在这种情况下,该组件将呈现一张食谱卡recipes状态。将突出显示的行添加到Recipes.jsx:

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

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

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

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

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

export default Recipes;

保存并退出Recipes.jsx.

现在您已经创建了一个组件来显示所有食谱,您将为它创建一个路由。打开前端路由文件app/javascript/routes/index.jsx:

  1. nano应用程序/javascript/routes/index.jsx

将突出显示的行添加到文件中:

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

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

保存并退出文件。

此时,最好验证您的代码是否按预期工作。像之前一样,使用以下命令启动服务器:

  1. bin/dev

然后在浏览器中打开该应用程序。按查看食谱主页上的按钮可访问包含种子食谱的显示页面:

Screencapture with the seed recipes page

Use CTRL+C在您的终端中停止服务器并返回到您的提示符。

现在您可以查看应用程序中的所有食谱,是时候创建第二个组件来查看各个食谱了。创建一个Recipe.jsx文件在app/javascript/components目录:

  1. nano应用程序/javascript/组件/Recipe.jsx

Recipes组件,导入React, useState, useEffect, Link, useNavigate, and useParam通过添加以下行来模块:

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

接下来,添加突出显示的行以创建并导出名为的功能性 React 组件Recipe:

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

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

export default Recipe;

Recipes组件,您可以使用以下命令初始化 React Router 导航useNavigate钩。 Arecipe状态和一个setRecipe函数将更新状态useState钩。此外,您还可以调用useParamshook,它返回一个对象,其键/值对是 URL 参数。

要查找特定的食谱,您的应用程序需要知道该食谱的id,这意味着你的Recipe组件期望一个id param在网址中。您可以通过以下方式访问此内容params保存返回值的对象useParams hook.

接下来,声明一个useEffect您将在其中访问的挂钩id param来自params目的。拿到食谱后idparam,您将发出 HTTP 请求来获取配方。将突出显示的行添加到您的文件中:

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

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

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

export default Recipe;

In the useEffect钩子,你使用params.id值来发出 GET HTTP 请求来获取拥有该配方的配方id然后使用将其保存到组件状态setRecipe功能。如果菜谱不存在,应用程序会将用户重定向到菜谱页面。

接下来,添加一个addHtmlEntities函数,它将用于替换字符实体HTML 实体在组件中。这addHtmlEntities函数将接受一个字符串并用 HTML 实体替换所有转义的左括号和右括号。此功能将帮助您转换配方指令中保存的任何转义字符。添加突出显示的行:

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

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

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

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

export default Recipe;

最后,返回标记以通过添加突出显示的行来在页面上以组件状态呈现配方:

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

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

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

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

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

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

    return ingredientList;
  };

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

export default Recipe;

With an ingredientList函数中,您可以将逗号分隔的食谱成分拆分为一个数组,并对其进行映射以创建成分列表。如果没有成分,应用程序会显示一条消息:没有可用的成分。您还可以通过将配方指令传递给addHtmlEntities功能。最后,代码将食谱图像显示为英雄图像,添加一个删除食谱按钮旁边的食谱说明,并添加一个链接回食谱页面的按钮。

Note:使用 React 的dangerouslySetInnerHTML属性是有风险的,因为它会将您的应用程序暴露给跨站脚本攻击。通过确保使用以下命令替换创建菜谱时输入的特殊字符,可以降低这种风险:stripHtmlEntities中声明的函数NewRecipe成分。

保存并退出文件。

要查看Recipe页面上的组件,您将其添加到您的路由文件中。打开您的路线文件进行编辑:

  1. nano应用程序/javascript/routes/index.jsx

将以下突出显示的行添加到文件中:

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

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

你导入你的Recipe在此路由文件中添加一个组件并添加一条路由。其路线有一条:id param将被替换为id您要查看的食谱的名称。

保存并关闭文件。

Use the bin/dev脚本再次启动服务器,然后访问http://localhost:3000在您的浏览器中。点击查看食谱按钮导航到食谱页面。在食谱页面上,通过单击其访问任何食谱查看食谱按钮。您将看到一个页面,其中填充了数据库中的数据:

Single Recipe Page

您可以使用以下命令停止服务器CTRL+C.

在此步骤中,您向数据库添加了九个菜谱,并创建了组件来查看这些菜谱(单独查看或作为集合查看)。在下一步中,您将添加一个组件来创建菜谱。

第 8 步 — 创建食谱

拥有可用的食品食谱应用程序的下一步是创建新食谱的能力。在此步骤中,您将为此功能创建一个组件。该组件将包含一个表单,用于从用户收集所需的食谱详细信息,然后向create行动于Recipe控制器保存配方数据。

创建一个NewRecipe.jsx文件在app/javascript/components目录:

  1. nano应用程序/javascript/components/NewRecipe.jsx

在新文件中,导入React, useState, Link, and useNavigate您在其他组件中使用的模块:

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

接下来,创建并导出函数NewRecipe通过添加突出显示的行来组成组件:

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

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

export default NewRecipe;

与之前的组件一样,您可以使用以下命令初始化 React 路由器导航useNavigate挂钩,然后使用useState钩子来初始化name, ingredients, and instruction状态,每个状态都有各自的更新功能。这些是您创建有效配方所需的字段。

接下来,创建一个stripHtmlEntities将转换特殊字符的函数(例如<)到他们的转义/编码值(如&lt;), 分别。为此,请将突出显示的行添加到NewRecipe成分:

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

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

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

export default NewRecipe;

In the stripHtmlEntities函数,你替换< and >字符及其转义值。这样,您就不会在数据库中存储原始 HTML。

接下来,添加突出显示的行以添加onChange and onSubmit函数到NewRecipe处理表单编辑和提交的组件:

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

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

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

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

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

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

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

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

export default NewRecipe;

The onChange函数接受用户输入event和状态设置器函数,然后它随后使用用户输入值更新状态。在里面onSubmit函数时,您检查所有必需的输入是否为空。然后,您构建一个包含创建新配方所需参数的对象。使用stripHtmlEntities函数,你替换< and >配方指令中的字符及其转义值,并用中断标记替换每个新行字符,从而保留用户输入的文本格式。最后,您发出 POST HTTP 请求来创建新配方,并在成功响应后重定向到其页面。

为了防止跨站请求伪造 (CSRF)攻击时,Rails 将 CSRF 安全令牌附加到 HTML 文档。每当非GET提出请求。随着token在前面的代码中,您的应用程序会验证服务器上的令牌,并在安全令牌与预期不匹配时引发异常。在里面onSubmit函数,应用程序检索CSRF代币通过 Rails 嵌入到 HTML 文档中,然后使用 JSON 字符串发出 HTTP 请求。如果成功创建菜谱,应用程序会将用户重定向到菜谱页面,他们可以在其中查看新创建的菜谱。

最后,返回呈现表单的标记,供用户输入用户希望创建的菜谱的详细信息。添加突出显示的行:

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

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

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

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

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

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

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

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

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

export default NewRecipe;

返回的标记包括一个包含三个输入字段的表单;各一个recipeName, recipeIngredients, and instruction。每个输入字段都有一个onChange调用的事件处理程序onChange功能。一个onSubmit事件处理程序也附加到提交按钮并调用onSubmit提交表单数据的函数。

保存并退出文件。

要在浏览器中访问此组件,请使用其路由更新您的路由文件:

  1. nano应用程序/javascript/routes/index.jsx

更新您的路线文件以包含这些突出显示的行:

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

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

路线就位后,保存并退出文件。

重新启动您的开发服务器并访问http://localhost:3000在您的浏览器中。导航到食谱页面并单击创建新食谱按钮。您将找到一个带有表单的页面,用于将食谱添加到您的数据库:

Create Recipe Page

输入所需的食谱详细信息,然后单击创建食谱按钮。新创建的菜谱将出现在页面上。准备好后,关闭服务器。

在此步骤中,您向食品食谱应用程序添加了创建食谱的功能。在下一步中,您将添加删除食谱的功能。

第 9 步 — 删除食谱

在本部分中,您将修改配方组件以包含用于删除配方的选项。当您单击菜谱页面上的删除按钮时,应用程序将发送请求以从数据库中删除菜谱。

首先,打开你的Recipe.jsx供编辑的文件:

  1. nano应用程序/javascript/组件/Recipe.jsx

In the Recipe组件,添加一个deleteRecipe具有突出显示的行的函数:

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

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

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

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

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

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

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

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

    return ingredientList;
  };

  const recipeInstruction = addHtmlEntities(recipe.instruction);

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

In the deleteRecipe函数,你得到id要删除的配方,然后构建您的 URL 并获取 CSRF 令牌。接下来,您制作一个DELETE请求给Recipes控制器删除配方。如果成功删除菜谱,应用程序会将用户重定向到菜谱页面。

运行以下代码deleteRecipe每当单击删除按钮时,都会将其作为单击事件处理程序传递给按钮。添加一个onClick组件中删除按钮元素的事件:

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

在本教程的此时,您已完成Recipe.jsx文件应与此文件匹配:

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

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

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

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

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

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

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

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

    return ingredientList;
  };

  const recipeInstruction = addHtmlEntities(recipe.instruction);

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

export default Recipe;

保存并退出文件。

重新启动应用服务器并导航至主页。点击查看食谱按钮访问所有现有食谱,然后打开任何特定食谱并单击删除食谱页面上的按钮可删除文章。您将被重定向到菜谱页面,并且删除的菜谱将不再存在。

随着删除按钮的工作,您现在拥有一个功能齐全的食谱应用程序!

结论

在本教程中,您使用 Ruby on Rails 和 React 前端创建了一个食品食谱应用程序,使用 PostgreSQL 作为数据库并使用 Bootstrap 进行样式设置。如果您想继续使用 Ruby on Rails 进行构建,请考虑遵循我们的使用 SSH 隧道保护三层 Rails 应用程序中的通信教程或访问我们的如何使用 Ruby 编码系列来刷新您的 Ruby 技能。要更深入地了解 React,请尝试如何使用 React 显示来自 DigitalOcean API 的数据.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何在 Ubuntu 20.04 上使用 React 前端设置 Ruby on Rails v7 项目 的相关文章

随机推荐

  • 如何在 Debian 9 上使用 UFW 设置防火墙

    Debian 包含多个软件包 这些软件包提供了用于管理防火墙的工具 其中 iptables 作为基本系统的一部分安装 对于初学者来说 学习如何使用 iptables 工具正确配置和管理防火墙可能很复杂 但 UFW 简化了它 UFW Unco
  • 如何在 Ubuntu 14.04 服务器上安装 ISPConfig3

    介绍 尽管命令行是一个功能强大的工具 可以让您在许多情况下快速轻松地工作 但在某些情况下 可视化界面会很有帮助 如果您要在一台计算机上配置许多不同的服务 或者为客户端管理系统的某些部分 则可以使用诸如ISP配置可以使这个任务变得更加简单 I
  • 如何在 CentOS 7 上安装 Git

    介绍 版本控制已成为现代软件开发中不可或缺的工具 版本控制系统允许您在源代码级别跟踪您的软件 您可以跟踪更改 恢复到之前的阶段以及从基本代码分支以创建文件和目录的替代版本 最流行的版本控制系统之一是git 许多项目在 Git 存储库中维护其
  • 什么是 Kubernetes?

    介绍 Kubernetes 是一个功能强大的开源系统 最初由 Google 开发 并得到云原生计算基金会 CNCF 的支持 用于在集群环境中管理容器化应用程序 它旨在提供更好的方法来管理跨不同基础设施的相关分布式组件和服务 要了解有关 Ku
  • 使用 Debian 9 进行初始服务器设置

    介绍 当您首次创建新的 Debian 9 服务器时 您应该尽早执行一些配置步骤作为基本设置的一部分 这将提高服务器的安全性和可用性 并为您后续的操作奠定坚实的基础 第一步 以 root 身份登录 要登录您的服务器 您需要知道您的服务器的公共
  • 如何使用 DigitalOcean 云服务器创建虚荣或品牌名称服务器

    介绍 托管提供商或经销商特别感兴趣 拥有品牌或 虚荣域名服务器为客户提供了更专业的外观 它 无需要求您的客户将其域名指向另一个域名 公司的域名服务器 本教程将概述两种创建方法 自定义域名服务器 i 虚荣和 ii 品牌 Types 虚荣名称服
  • 如何在 Ubuntu 18.04 上安装 MySQL

    本教程的先前版本由以下人员编写榛子维尔多 介绍 MySQL是一个开源数据库管理系统 通常作为流行的一部分安装LAMP Linux Apache MySQL PHP Python Perl 堆栈 它使用关系数据库和 SQL 结构化查询语言 来
  • 如何使用 passwd 和 adduser 在 Linux VPS 上管理密码

    介绍 密码和身份验证是每个用户在 Linux 环境中工作时必须处理的概念 这些主题涵盖许多不同的配置文件和工具 在本指南中 我们将探索一些基本文件 例如 etc passwd 和 etc shadow 以及用于配置身份验证的工具 例如名称恰
  • 如何在 VPS 上安装和使用 Logwatch 日志分析器和报告器

    介绍 应用程序创建所谓的 日志文件 来跟踪在任何给定时间发生的活动 这些文件远非简单的文本输出 浏览起来可能非常复杂 特别是当所管理的服务器很繁忙时 当需要参考日志文件时 例如 在发生故障 数据丢失等情况下 利用所有可用的帮助变得至关重要
  • 如何修改 DOM 中的属性、类和样式

    介绍 在本教程之前的教程中series 如何更改 DOM 我们介绍了如何使用内置方法从文档对象模型 DOM 中创建 插入 替换和删除元素 通过提高操作 DOM 的熟练程度 您可以更好地利用 JavaScript 的交互功能并修改 Web 元
  • 如何在 Ubuntu 22.04 上的 PostgreSQL 中静态加密数据库

    介绍 PostgreSQL是一个数据库管理系统 自 1996 年以来一直存在 就像其他数据库系统一样 SQL MySQL Oracle等 PostgreSQL的主要目的是为用户提供一种创建数据库用于存储和数据检索的方式 其突出的功能之一包括
  • 如何在 Ubuntu 12.04 上使用 Iptables 设置防火墙

    Status 已弃用 本文介绍不再受支持的 Ubuntu 版本 如果您当前运行的服务器运行 Ubuntu 12 04 我们强烈建议您升级或迁移到受支持的 Ubuntu 版本 升级到Ubuntu 14 04 从 Ubuntu 14 04 升级
  • Java 中的死锁示例

    java中的死锁是两个或多个线程永远被阻塞的一种编程情况 Java 死锁情况发生在至少两个线程和两个或更多资源的情况下 这里我写了一个简单的程序 该程序会导致java死锁场景 然后我们将看到如何分析它 Java 中的死锁 Let s hav
  • 深入探讨 Iptables 和 Netfilter 架构

    介绍 防火墙是一个重要的工具 可以配置它来保护您的服务器和基础设施 在Linux生态系统中 iptables是一种广泛使用的防火墙工具 与内核一起工作netfilter数据包过滤框架 由于复杂的语法和涉及的相互关联部分的数量 创建可靠的防火
  • 如何使用 Dovecot 设置 Postfix 电子邮件服务器:动态 Maildirs 和 LMTP

    Preface 本教程基于如何使用 Dovecot 设置 Postfix 电子邮件服务器并从第一部分结束的地方开始 请先阅读该教程 在本文中 我们将使用 dovecot 的 LMTP 服务器作为传递机制将邮箱与系统帐户分离 并使用 post
  • Java ArrayList 的数组、ArrayList 的 Array

    今天我们将学习如何创建Java数组ArrayList 我们还将学习如何创建数组元素的 ArrayList Java ArrayList 的数组 Creating array of list in java is not complex Be
  • 如何将本地 Django 应用程序部署到 VPS

    先决条件 本教程假设您已经使用所选操作系统设置了虚拟专用服务器 本教程使用 Debian 7 Ubuntu 也可以 如果您还没有这样做 您可以按照此操作tutorial 在开始之前 请确保您的云服务器已正确配置为托管 Django 应用程序
  • 如何在 Arch Linux 上安装 Linux、Apache、MySQL、PHP (LAMP) 堆栈

    关于兰普 LAMP 堆栈是一组用于启动和运行 Web 服务器的开源软件 该缩写词代表 Linux Apache MySQL 和 PHP Arch Linux 使用功能强大的 Pacman 安装程序 只需一个命令即可下载每个程序所需的所有最新
  • JSON 服务器(json-server)

    今天我们将研究一个非常方便的工具 json server 它可以在一分钟内为您提供一个模拟的 Rest json 服务器 在常规企业应用程序中 您需要与许多团队和第三方 API 合作 想象一下您必须致电第三方宁静的网络服务这将使您能够处理
  • 如何在 Ubuntu 20.04 上使用 React 前端设置 Ruby on Rails v7 项目

    作者选择了电子前沿基金会接受捐赠作为为捐款而写程序 介绍 红宝石 on Rails是一个流行的服务器端 Web 应用程序框架 它为当今网络上存在的许多流行应用程序提供支持 例如GitHub Basecamp 声云 Airbnb and Tw