如何在Ubuntu 20.04上设置一个带有React前端的Ruby on Rails v7项目

引言

Ruby on Rails是一款热门的服务器端Web应用框架。它驱动着今天网络上许多流行的应用程序,如GitHub、Basecamp、SoundCloud、Airbnb和Twitch。凭借注重程序员体验和围绕它建立的充满激情的社区,Ruby on Rails将为您提供构建和维护现代Web应用程序所需的工具。

React是一个用于创建前端用户界面的JavaScript库。由Facebook支持,它是当今最流行的前端库之一。React提供了诸如虚拟DOM、组件架构和状态管理等功能,使前端开发过程更有组织和高效。

随着网络前端逐渐从服务器端代码中分离出来,将Rails的优雅与React的高效结合,您将能够构建强大而现代的应用程序,并接收到当前趋势的信息。通过在Rails视图中使用React来渲染组件(而不是Rails模板引擎),您的应用程序将获益于JavaScript和前端开发的最新进展,同时利用了Ruby on Rails的表现力。

在本教程中,您将创建一个Ruby on Rails应用程序,用于存储您喜爱的食谱,然后使用React前端将其显示出来。当您完成后,您将能够使用Bootstrap风格的React界面创建、查看和删除食谱。

Screencapture of the completed recipe app home page

前提条件

按照本教程,您需要:

  • Node.js and npm installed on your development machine. This tutorial uses Node.js version 16.14.0 and npm version 8.3.1. Node.js is a JavaScript run-time environment that allows you to run your code outside the browser. It comes with a pre-installed Package Manager called npm, which lets you install and update packages. To install these on Ubuntu 20.04 or macOS, follow the “Installing Using a PPA” section of How To Install Node.js on Ubuntu 20.04 or the steps in How to Install Node.js and Create a Local Development Environment on macOS.
  • The Yarn package manager installed on your development machine, which will allow you to download the React framework. This tutorial was tested on version 1.22.10; to install this dependency, follow the official Yarn installation guide.
  • Ruby on Rails installed. To get this, follow our guide on How to Install Ruby on Rails with rbenv on Ubuntu 20.04. If you would like to develop this application on macOS, you can use How To Install Ruby on Rails with rbenv on macOS. This tutorial was tested on version 3.1.2 of Ruby and version 7.0.4 of Rails, so make sure to specify these versions during the installation process.

Note

注意:Rails7 版本不向后兼容。 如果您正在使用 Rails5 版本,请访问有关在 Ubuntu 18.04 上如何设置使用 React 前端的 Ruby on Rails v5 项目的教程。
  • PostgreSQL installed, as described in Steps 1 and 2 How To Use PostgreSQL with Your Ruby on Rails Application on Ubuntu 20.04 or How To Use PostgreSQL with Your Ruby on Rails Application on macOS. To follow this tutorial, you can use PostgreSQL version 12 or higher. If you want to develop this application on a different distribution of Linux or another OS, see the official PostgreSQL downloads page. For more information on how to use PostgreSQL, see How To Install and Use PostgreSQL.

步骤一 – 创建一个新的Rails应用程序

在这一步中,你将使用Rails应用程序框架构建你的食谱应用。首先,你将创建一个新的Rails应用程序,并设置为与React一起工作。

Rails提供了几个名为生成器的脚本,用于创建构建现代Web应用所需的一切。要查看完整的命令列表及其功能,请在终端中运行以下命令。

  1. rails -h

这个命令将提供一个全面的选项列表,允许你设置应用程序的参数。其中一个列出的命令是新命令,用于创建一个新的Rails应用程序。

现在,你将使用新的生成器创建一个新的Rails应用程序。在你的终端中运行以下命令:

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

前面的命令在名为rails_react_recipe的目录中创建一个新的Rails应用程序,安装所需的Ruby和JavaScript依赖,并配置Webpack。与这个新的生成器命令相关的标志包括以下内容:

  • The -d flag specifies the preferred database engine, which in this case is PostgreSQL.
  • The -j flag specifies the application’s JavaScript approach. Rails offers a few different ways to handle Javascript code in Rails applications. The esbuild option passed to the -j flag instructs Rails to preconfigure esbuild as the preferred JavaScript bundler.
  • The -c flag specifies the application’s CSS processor. Bootstrap is the preferred option in this case.
  • The -T flag instructs Rails to skip the generation of test files since you won’t be writing tests for this tutorial. This command is also suggested if you want to use a Ruby testing tool different from the one Rails provides.

命令完成后,转到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应用程序,下一步将是将其连接到一个数据库。

第二步 — 设置数据库

在运行新的Rails应用程序之前,你必须首先将其连接到一个数据库。在这一步中,你将把新创建的Rails应用程序连接到一个PostgreSQL数据库,以便可以根据需要存储和获取食谱数据。

在config/database.yml中找到的database.yml文件包含了不同开发环境的数据库名称等数据库细节。Rails通过在环境名称后面添加下划线(_)来指定各个开发环境的数据库名称。在本教程中,您将使用默认的数据库配置值,但如果需要,可以更改配置值。

Note

注意:在这一点上,你可以修改config/database.yml来设置你希望Rails使用哪个PostgreSQL角色来创建你的数据库。在前提条件中,你根据《如何在你的Ruby on Rails应用程序中使用PostgreSQL》教程创建了一个由密码保护的角色。如果你还没有设置用户,现在你可以按照同样前提条件教程中的第4步——配置和创建你的数据库的说明来操作。

Rails提供了许多命令,使得开发Web应用程序变得简单,包括用于与数据库交互的命令,例如创建、删除和重置。要为您的应用程序创建一个数据库,请在终端中运行以下命令。

  1. rails db:create

这个命令会创建一个开发和测试数据库,并产生以下输出:

Output
Created database 'rails_react_recipe_development' Created database 'rails_react_recipe_test'

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

  1. bin/dev

Rails 提供了一个替代的 bin/dev 脚本,通过使用 Foreman gem 在应用的根目录中执行 Procfile.dev 文件中的命令来启动 Rails 应用程序。

一旦你执行这个命令,你的命令提示符将会消失,然后下面的输出将会出现在原来的位置上。

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

要停止网页服务器,在运行服务器的终端上按下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前端。

第三步 — 安装前端依赖

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

  • React for building user interfaces.
  • React DOM to enable React to interact with the browser DOM.
  • React Router for handling navigation in a React application.

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

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

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

  1. nano package.json

安装的软件包将在依赖项键下列出。

~/rails_react_recipe/package.json的中文原生解释:
~/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键关闭文件。

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

步骤四 — 设置主页

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

Rails遵循应用程序的模型-视图-控制器架构模式。在MVC模式中,控制器的目的是接收特定请求并将其传递到适当的模型或视图。当前应用程序在浏览器加载根URL时显示Rails欢迎页。为了改变这一点,您需要创建一个控制器和视图来处理主页,并将其与一个路由进行匹配。

Rails 提供了一个控制器生成器来创建控制器。控制器生成器接收一个控制器名称和相应的操作。要了解更多信息,请查阅 Rails 文档。

本教程将调用控制器为Homepage。运行以下命令创建一个带有index行为的Homepage控制器。

  1. rails g controller Homepage index

Note

注意:
在Linux上,错误FATAL:侦听错误:无法监视目录中的更改可能是由于系统限制导致的,您的机器可以监视更改的文件数量有限。运行以下命令修复它:
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

这个命令将永久增加您可以使用Listen监视的目录数为524288。您可以通过运行相同的命令并将524288替换为您所需的数字来再次更改此设置。

运行控制器命令会生成以下文件:

  • A homepage_controller.rb file for receiving all homepage-related requests. This file contains the index action you specified in the command.
  • A homepage_helper.rb file for adding helper methods related to the Homepage controller.
  • An index.html.erb file as the view page for rendering anything related to the homepage.

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

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

  1. nano config/routes.rb

在这个文件中,将get ‘homepage/index’替换为root ‘homepage#index’,以便使文件与以下内容匹配。

~/rails_react_recipe/config/routes.rb
~/rails_react_recipe/config/routes.rb 路由配置文件
~/rails_react_recipe/config/routes.rb 路由文件
~/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

这个修改指示Rails将应用程序的根请求映射到Homepage控制器的索引操作,然后在浏览器中呈现位于app/views/homepage/index.html.erb的index.html.erb文件中的内容。

保存并关闭文件。

为了验证这个是否有效,请启动你的应用程序。

  1. bin/dev

当您在浏览器中打开或刷新应用程序时,将加载一个新的应用程序主页。

The

一旦您确认应用程序运行正常,按下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。

第五步 — 将React配置为您的Rails前端

在这一步中,您将配置Rails使用React作为应用程序的前端,而不是使用其模板引擎。这个新的配置将使您能够使用React创建一个更具视觉吸引力的主页。

通过在生成Rails应用程序时指定的esbuild选项的帮助下,已经完成了大部分设置,使JavaScript能够与Rails无缝配合。现在只需要将React应用程序的入口点加载到JavaScript文件的esbuild入口点中。要做到这一点,请从app/javascript目录中开始创建一个components目录。

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

组件目录将包含首页的组件,以及应用程序中的其他React组件,包括React应用程序的入口文件。

接下来,在位于app/javascript/application.js的文件中打开application.js。

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

将下划线标记的代码行添加到文件中。

将以下内容以中文进行本地化改写(仅提供一种选项):
~/rails_react_recipe/app/javascript/application.js

~/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文件中添加的代码行将会导入entry index.jsx文件中的代码,使其能够被esbuild用于打包。通过将/components目录导入到Rails应用的JavaScript入口点,您可以为主页创建一个React组件。主页将包含一些文本和一个调用的操作按钮,以查看所有的食谱。

保存并关闭文件。

然后,在组件目录中创建一个Home.jsx文件。

  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 Router中导入了React和Link组件。Link组件可以创建超链接,用于在页面之间导航。然后你创建并导出了一个功能组件,其中包含一些标记语言用于你的主页,并使用了Bootstrap类进行样式化。

保存并关闭文件。

在设置了你的Home组件后,你现在可以使用React Router来设置路由。在app/javascript目录中创建一个routes文件夹。

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

路线目录将包含几个具有相应组件的路线。每当加载任何指定的路线时,它将渲染相应的组件到浏览器。

在路由目录中,创建一个 index.jsx 文件:

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

在这个index.jsx路由文件中,你导入了以下模块:React模块允许你使用React,以及从React Router导入的BrowserRouter、Routes和Route模块,它们共同帮助你在不同的路由之间导航。最后,你导入了Home组件,当请求与根路径(/)匹配时将渲染该组件。当你想要在应用程序中添加更多页面时,你可以在这个文件中声明一个路由,并将其匹配到你想要为该页面渲染的组件。

保存并退出文件。

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

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

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

将以下代码添加到App.jsx文件中

以下是对~/rails_react_recipe/app/javascript/components/App.jsx的本地化中文解释:

~/rails_react_recipe/app/javascript/components/App.jsx是一个文件路径,表示该文件位于项目目录中的rails_react_recipe文件夹下的app文件夹下的javascript文件夹中的components文件夹中的App.jsx文件。

import React from "react";
import Routes from "../routes";

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

在App.jsx文件中,你导入React和刚创建的路由文件。然后,你导出一个组件来渲染这些路由片段。这个组件将在应用程序的入口点呈现,使得每当应用程序加载时都可以使用这些路由。

保存并关闭文件。

现在您已经设置好了您的 App.jsx,您可以在入口文件中渲染它。在组件目录中创建一个 index.jsx 文件。

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

将以下代码添加到index.js文件中。

~/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 />);
});

在导入行中,你导入了React库,从ReactDOM中导入createRoot函数和你的App组件。使用ReactDOM的createRoot函数,你创建一个根元素作为附加到页面的div元素,并将你的App组件渲染在其中。当应用程序加载时,React会将App组件的内容渲染到页面上的div元素中。

保存并退出文件。

最后,你将给你的主页添加一些CSS样式。

打开位于~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss目录下的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样式设置好后,保存并关闭文件。

接下来,重新启动您的应用程序的网络服务器。

  1. bin/dev

然后在您的浏览器中重新加载应用程序,一个全新的主页将加载出来。

The homepage with its new styling

使用CTRL+C停止网络服务器。

在这一步中,您配置了您的应用程序以使用React作为其前端。在下一步中,您将创建能够创建,读取,更新和删除食谱的模型和控制器。

第六步 – 创建菜谱控制器和模型

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

从Rails提供的generate model子命令开始创建一个Recipe模型,指定模型的名称以及其列和数据类型。运行以下命令:

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

上述命令指示Rails创建一个Recipe模型,它包括一个名为name的字符串类型列、一个名为ingredients和instruction的文本类型列,以及一个名为image的字符串类型列。本教程将模型命名为Recipe,因为Rails中的模型使用单数名称,而它们对应的数据库表使用复数名称。

运行生成模型命令会创建两个文件并打印以下输出:

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

创建的两个文件有:

  • A recipe.rb file that holds all the model-related logic.
  • A 20221017220817_create_recipes.rb file (the number at the beginning of the file may differ depending on the date when you run the command). This migration file contains the instruction for creating the database structure.

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

打开位于app/models/recipe.rb的配方模型。

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

请将以下代码中的突出部分添加到文件中。

~/rails_react_recipe/app/models/recipe.rb 可以被原生地 paraphrase 成为以下中文选项之一:

~/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表,你需要运行一个迁移(migration),这是一种以编程方式对数据库进行更改的方法。为了确保迁移与你设置的数据库配合工作,你必须对20221017220817_create_recipes.rb 文件进行修改。

请在你的编辑器中打开这个文件。

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

添加被标记的材料以使您的文件与下列内容相匹配。

以下是对应的中文翻译:

db/migrate/20221017220817_create_recipes.rb
日期/时间/迁移编号_创建菜谱.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的表以及列和它们的数据类型的命令。你还通过添加null: false,确保这些列在更改数据库之前都有一个值,更新了20221017220817_create_recipes.rb,在name、ingredients和instruction列上添加了一个NOT NULL约束。最后,你为image列添加了一个默认的图片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

在这个命令中,你会在api/v1目录中创建一个Recipes控制器,包括index、create、show和destroy四个动作。 index动作将负责获取所有的食谱; create动作将负责创建新的食谱; show动作将获取单个食谱,而destroy动作将处理删除食谱的逻辑。

您还可以传入一些标志来使控制器更轻量化,包括:

  • –skip-template-engine, which instructs Rails to skip generating Rails view files since React handles your front-end needs.
  • –no-helper, which instructs Rails to skip generating a helper file for your controller.

运行该命令还会在路由文件中为 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_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谓词,以便可以发送和删除数据。你还通过在路由中添加:id参数,修改了展示和删除操作的路由。:id将保存你想要读取或删除的食谱的标识号。

你可以使用 get ‘/*path’ 添加一个捕获所有路径的路由,将所有不匹配现有路由的其他请求重定向到主页控制器的索引动作。前端路由将处理与创建、阅读或删除食谱无关的请求。

保存并退出文件。

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

  1. rails routes

运行这个命令会显示项目中的URI模式、动词和匹配的控制器或动作的一个长列表。

接下来,您将添加一种逻辑来一次性获取所有的菜谱。Rails使用ActiveRecord库来处理与数据库相关的任务。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

在你的索引操作中,你使用了ActiveRecord的all方法来获取数据库中的所有菜谱。使用order方法,你按照创建日期的降序进行排序,这样最新的菜谱会排在首位。最后,你使用render将菜谱列表以JSON响应的形式发送出去。

接下来,你将添加有关创建新食谱的逻辑。与获取所有食谱一样,你将依赖于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

在创建操作中,您可以使用ActiveRecord的create方法来创建新的食谱。create方法可以一次将所有控制器参数分配给模型。该方法使记录的创建变得简单,但也存在恶意使用的可能性。可以通过使用Rails提供的强参数功能来防止恶意使用。这样,只有被允许的参数才能被分配。您在代码中将一个recipe_params参数传递给create方法。recipe_params是一个私有方法,在其中允许控制器参数,以防止错误或恶意内容进入您的数据库。在这种情况下,您允许使用name、image、ingredients和instruction参数来正确使用create方法。

您的配方控制器现在可以读取和创建配方了。剩下的就是读取和删除单个配方的逻辑。使用下面的代码更新您的配方控制器:

~/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
  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方法,在show和delete操作匹配请求时由before_action调用。set_recipe方法使用ActiveRecord的find方法找到一个id与params中提供的id匹配的recipe,并将其赋值给实例变量@recipe。在show操作中,你将由set_recipe方法设置的@recipe对象作为JSON响应返回。

在销毁操作中,你使用了Ruby的安全导航运算符&.,它在调用方法时避免了空值错误。这个附加功能允许你仅在存在时才删除一个菜谱,并作为响应发送一条消息。

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

在这一步中,你为你的食谱创建了一个模型和控制器。你已经编写了所有与后端食谱操作相关的逻辑。在接下来的部分中,你将创建组件来查看你的食谱。

第七步 – 浏览食谱

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

首先,您将创建一个页面来查看所有的食谱。在创建页面之前,由于您的数据库当前为空,您需要一些食谱供您使用。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创建九个配方,包括名称、配料和指导部分。保存并退出文件。

为了用这些数据填充数据库,请在终端中运行以下命令。

  1. rails db:seed

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

查看所有食谱的组件将向 RecipesController 的 index 操作发出 HTTP 请求,以获取所有食谱的列表。然后将在页面上以卡片的形式显示这些食谱。

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

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

一旦文件打开,通过添加以下代码,导入React、useState、useEffect、Link和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会调用useNavigate钩子。React的useState钩子将初始化recipes状态,该状态是一个空数组([]),以及一个用于更新recipes状态的setRecipes函数。

接下来,在useEffect钩子中,您将发送一个HTTP请求来获取所有的食谱。为了实现这一点,请添加以下突出显示的代码行:

~ /rails_react_recipe/app/javascript/components/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("/"));
  }, []);
};

export default Recipes;

在你的 useEffect hook 中,你使用 Fetch API 发起一个 HTTP 调用来获取所有的食谱。如果响应成功,应用程序会将食谱数组保存到 recipes 状态中。如果发生错误,它会将用户重定向到主页。

最后,当组件被渲染时,在浏览器页面上返回将被评估和显示的元素的标记。在这种情况下,组件将从recipes状态渲染一个食谱卡片。将这些高亮行添加到Recipes.jsx中。

~/rails_react_recipe/app/javascript/components/食谱.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

在文件中添加被突出显示的行。

~/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

在终端中使用CTRL+C停止服务器并返回提示符。

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

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

与“Recipes”组件一样,通过添加以下行来导入React、useState、useEffect、Link、useNavigate和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 可以重新表示为以下中文方案:
~/rails_react_recipe/app/javascript/components/菜谱组件.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;

与“菜谱”组件一样,您可以使用 useNavigate 钩子来初始化 React 路由导航。useState 钩子会使用一个 recipe 状态和 setRecipe 函数来更新状态。另外,您还可以调用 useParams 钩子,它会返回一个对象,该对象的键/值对是 URL 参数。

为了找到特定的食谱,您的应用程序需要知道食谱的id,这意味着您的食谱组件需要在URL中接收一个id参数。您可以通过params对象访问这个参数,该对象保存useParams钩子函数的返回值。

接下来,在useEffect钩子中声明一个变量,通过params对象获取id参数。一旦获取到食谱的id参数,你将发送一个HTTP请求来获取这个食谱。请将下面的代码添加到你的文件中:

~/rails_react_recipe/app/javascript/components/食谱.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;

在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;

通过ingredientList函数,你可以将逗号分隔的食谱原料分成数组,并对其进行映射以创建原料列表。如果没有原料,应用程序会显示一个提示消息,表示无可用原料。接下来,你通过addHtmlEntities函数替换食谱指令中的所有开放和关闭括号。最后,该代码将食谱图像显示为主要图像,并在食谱指令旁边添加一个删除食谱按钮,以及一个返回食谱页面的按钮。

Note

注意:使用React的dangerouslySetInnerHTML属性是有风险的,因为它会使你的应用暴露给跨站脚本攻击。通过确保在创建食谱时使用NewRecipe组件中声明的stripHtmlEntities函数替换输入的特殊字符,可以减少这种风险。

保存并退出文件。

要在页面上查看食谱组件,您需要将其添加到您的路由文件中。打开您的路由文件进行编辑。

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

你需要在这个路由文件中引入你的食谱组件并添加一个路由。此路由包含一个:id参数,将被你想要查看的食谱的id替换。

保存并关闭文件。

使用bin/dev脚本重新启动服务器,然后在浏览器中访问http://localhost:3000。点击“查看食谱”按钮,转到食谱页面。在食谱页面中,通过点击“查看食谱”按钮访问任意食谱。你将会看到一个根据数据库数据填充的页面。

Single Recipe Page

你可以使用CTRL+C停止服务器。

在这一步中,你向你的数据库添加了九个食谱,并创建了可以单独查看这些食谱和作为集合查看的组件。在下一步中,你将添加一个创建食谱的组件。

步骤8 —— 制作食谱

拥有可用食谱应用的下一步是能够创建新的食谱。在这一步中,您将创建一个用于此功能的组件。该组件将包含一个表单,用于从用户处收集所需的食谱详细信息,然后向食谱控制器中的创建操作发出请求,保存食谱数据。

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

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

在新文件中,导入你在其他组件中使用的React、useState、Link和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;

与之前的组件一样,您可以使用useNavigate钩子来初始化React路由导航,然后使用useState钩子来初始化名称、配料和指导说明的状态,并分别使用其对应的更新函数。这些是您需要创建有效的食谱所需的字段。

然后,创建一个stripHtmlEntities函数,将特殊字符(如<)转换为其转义/编码值(如<)。为此,将以下突出显示的行添加到NewRecipe组件中。

请用中文将以下内容进行释义:~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

在~/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;

在stripHtmlEntities函数中,你用它们的转义值替换了<和>字符。这样,你就不会在数据库中存储原始的HTML。

接下来,将以下高亮代码添加到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;

onChange函数接收用户输入的事件和状态设置函数,然后使用用户输入的值更新状态。在onSubmit函数中,您检查所有必填输入框不能为空。然后,您构建一个包含创建新食谱所需参数的对象。使用stripHtmlEntities函数,您将食谱说明中的<和>字符替换为它们的转义值,并将每个换行字符替换为换行标签,以保留用户输入的文本格式。最后,您发送POST HTTP请求来创建新食谱,并在成功响应时重定向到其页面。

为了防止跨站请求伪造(CSRF)攻击,Rails会将一个CSRF安全标记附加到HTML文档上。只要发起一个非GET请求,就需要这个标记。通过之前代码中的标记,你的应用程序可以在服务器上验证标记,并在安全标记与预期不符时抛出异常。在onSubmit函数中,应用程序会从Rails嵌入在HTML文档中的CSRF标记中检索出标记并发送一个带有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),一个用于指令(instruction)。每个输入字段都有一个onChange事件处理器,调用onChange函数。还有一个onSubmit事件处理器附加到提交按钮上,并调用提交表单数据的onSubmit函数。

保存并退出文件。

为了在浏览器中访问此组件,请在您的路由文件中更新它的路由设置。

  1. nano 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";
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

输入所需的食谱细节,然后点击创建食谱按钮。新创建的食谱将显示在页面上。准备好后,关闭服务器。

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

第九步——删除配方

在这一部分中,您将修改您的菜谱组件,以包括删除菜谱的选项。当您在菜谱页面上点击删除按钮时,应用程序将发送一个删除菜谱的请求到数据库。

首先,打开您的Recipe.jsx文件进行编辑。

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

在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="">
...

在deleteRecipe函数中,您获取要删除的食谱的id,然后构建URL并获取CSRF令牌。接下来,您向Recipes控制器发送DELETE请求来删除该食谱。如果成功删除食谱,应用程序会将用户重定向到食谱页面。

在组件中的 delete 按钮元素上添加一个 onClick 事件,并将 deleteRecipe 函数作为点击事件处理程序传递给该按钮,以便在点击删除按钮时运行该函数。

~/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/食谱.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展示来自Silicon Cloud API的数据”教程。

发表回复 0

Your email address will not be published. Required fields are marked *


广告
将在 10 秒后关闭
bannerAds