如何使用React前端设置RubyonRails项目

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Electronic Frontier Foundation 来接受捐赠。

###介绍

Ruby on Rails 是一个流行的服务器端 Web 应用程序框架,在撰写本教程时, 在 GitHub 上已获得超过 42,000 颗星。 它为当今网络上存在的许多流行应用程序提供支持,例如 GitHubBasecampSoundCloudAirbnb 和 [ X156X]抽搐。 Ruby on Rails 强调程序员的经验和围绕它建立起来的热情社区,将为您提供构建和维护现代 Web 应用程序所需的工具。

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

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

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

如果您想查看此应用程序的代码,请查看 DigitalOcean 社区 GitHub 上本教程的 配套存储库

先决条件

要遵循本教程,您需要具备以下条件:

  • Node.jsnpm 安装在你的开发机器上。 本教程使用 Node.js 版本 10.16.0 和 npm 版本 6.9.0。 Node.js 是一个 JavaScript 运行时环境,允许您在浏览器之外运行代码。 它带有一个名为 npm 的预安装包管理器,可让您安装和更新包。 要在 macOS 或 Ubuntu 18.04 上安装这些,请按照 如何安装 Node.js 并在 macOS 上创建本地开发环境中的步骤或 如何安装节点的“使用 PPA 安装”部分中的步骤进行操作.js 在 Ubuntu 18.04 上。
  • Yarn 包管理器安装在你的开发机器上,它允许你下载 React 框架。 本教程在 1.16.0 版本上测试; 要安装此依赖项,请按照 官方 Yarn 安装指南
  • 安装 Ruby on Rails 框架。 为此,请按照我们的指南进行操作:如何在 Ubuntu 18.04 上使用 rbenv 安装 Ruby on Rails,或如何在 CentOS 7 上使用 rbenv 安装 Ruby on Rails。 如果您想在 macOS 上开发此应用程序,请参阅本教程 如何在 macOS 上使用 rbenv 安装 Ruby on Rails 。 本教程在 Ruby 2.6.3 版和 Rails 5.2.3 版上进行了测试,因此请确保在安装过程中指定这些版本。
  • 安装 PostgreSQL,如我们教程的第 1 步和第 2 步所示 如何在 Ubuntu 18.04 上将 PostgreSQL 与您的 Ruby on Rails 应用程序一起使用 如何在 macOS 上将 PostgreSQL 与您的 Ruby on Rails 应用程序一起使用 [ X226X]。 要遵循本教程,请使用 PostgreSQL 版本 10。 如果您希望在不同的 Linux 发行版或其他操作系统上开发此应用程序,请参阅 official PostgreSQL 下载页面 。 有关如何使用 PostgreSQL 的更多信息,请参阅我们的 如何安装和使用 PostgreSQL 教程。

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

在这一步中,您将在 Rails 应用程序框架上构建您的配方应用程序。 首先,您将创建一个新的 Rails 应用程序,该应用程序将被设置为与 React 一起工作,只需少量配置。

Rails 提供了许多称为生成器的脚本,可帮助创建构建现代 Web 应用程序所需的一切。 要查看这些命令的完整列表及其作用,请在终端窗口中运行以下命令:

rails -h

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

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

rails new rails_react_recipe -d=postgresql -T --webpack=react --skip-coffee

上述命令在名为 rails_react_recipe 的目录中创建一个新的 Rails 应用程序,安装所需的 Ruby 和 JavaScript 依赖项,并配置 Webpack。 让我们来看看与这个 new 生成器命令相关的标志:

  • -d 标志指定首选数据库引擎,在本例中为 PostgreSQL。
  • -T 标志指示 Rails 跳过测试文件的生成,因为您不会为了本教程的目的编写测试。 如果您想使用不同于 Rails 提供的 Ruby 测试工具,也建议使用此命令。
  • --webpack 指示 Rails 使用 webpack bundler 为 JavaScript 进行预配置,在这种情况下专门针对 React 应用程序。
  • --skip-coffee 要求 Rails 不要设置 CoffeeScript,本教程不需要。

命令运行完成后,进入 rails_react_recipe 目录,这是您的应用程序的根目录:

cd rails_react_recipe

接下来,列出目录的内容:

ls

这个根目录有许多自动生成的文件和文件夹,它们构成了 Rails 应用程序的结构,包括一个 package.json 文件,其中包含 React 应用程序的依赖项。

现在您已经成功地创建了一个新的 Rails 应用程序,您可以在下一步将其连接到数据库。

第 2 步 — 设置数据库

在运行新的 Rails 应用程序之前,您必须先将其连接到数据库。 在这一步中,您会将新创建的 Rails 应用程序连接到 PostgreSQL 数据库,以便可以在需要时存储和获取配方数据。

config/database.yml 中的 database.yml 文件包含数据库详细信息,例如不同开发环境的数据库名称。 Rails 通过在应用程序名称后面附加一个下划线 (_) 和环境名称来指定不同开发环境的数据库名称。 您始终可以将任何环境数据库名称更改为您喜欢的任何名称。

注意: 此时,您可以更改 config/database.yml 来设置希望 Rails 使用哪个 PostgreSQL 角色来创建数据库。 如果您遵循先决条件 How To Use PostgreSQL with Your Ruby on Rails Application 并创建了受密码保护的角色,则可以按照 Step 4 中的说明操作 macOSUbuntu 18.04


如前所述,Rails 提供了许多命令来简化 Web 应用程序的开发。 这包括使用数据库的命令,例如 createdropreset。 要为您的应用程序创建数据库,请在终端窗口中运行以下命令:

rails db:create

此命令创建 developmenttest 数据库,产生以下输出:

OutputCreated database 'rails_react_recipe_development'
Created database 'rails_react_recipe_test'

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

rails s --binding=127.0.0.1

sserver 命令启动 Puma,这是一个默认使用 Rails 分发的 Web 服务器,--binding=127.0.0.1 将服务器绑定到您的 [X155X ]。

运行此命令后,您的命令提示符将消失,您将看到以下输出:

Output=> Booting Puma
=> Rails 5.2.3 application starting in development 
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.1 (ruby 2.6.3-p62), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:3000
Use Ctrl-C to stop

要查看您的应用程序,请打开浏览器窗口并导航到 http://localhost:3000。 您将看到 Rails 默认的欢迎页面:

这意味着您已经正确设置了 Rails 应用程序。

要随时停止 Web 服务器,请在运行服务器的终端窗口中按 CTRL+C。 现在就去做吧; 您将收到来自 Puma 的告别信息:

Output^C- Gracefully stopping, waiting for requests to finish
=== puma shutdown: 2019-07-31 14:21:24 -0400 ===
- Goodbye!
Exiting

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

您已经成功地为您的食物食谱应用程序设置了一个数据库。 在下一步中,您将安装所有额外的 JavaScript 依赖项,以将您的 React 前端组合在一起。

第 3 步 — 安装前端依赖项

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

在终端窗口中运行以下命令,使用 Yarn 包管理器安装这些包:

yarn add react-router-dom bootstrap jquery popper.js

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

nano package.json

您将在 dependencies 键下看到已安装的软件包:

~/rails_react_recipe/package.json

{
  "name": "rails_react_recipe",
  "private": true,
  "dependencies": {
    "@babel/preset-react": "^7.0.0",
    "@rails/webpacker": "^4.0.7",
    "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
    "bootstrap": "^4.3.1",
    "jquery": "^3.4.1",
    "popper.js": "^1.15.0",
    "prop-types": "^15.7.2",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-router-dom": "^5.0.1"
  },
  "devDependencies": {
    "webpack-dev-server": "^3.7.2"
  }
}

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

第 4 步 — 设置主页

安装所有必需的依赖项后,在此步骤中,您将为应用程序创建一个主页。 当用户首次访问应用程序时,主页将作为登录页面。

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

Rails 提供了一个 controller 生成器来创建控制器。 controller 生成器接收控制器名称以及匹配的操作。 有关这方面的更多信息,请查看 官方 Rails 文档

本教程将调用控制器 Homepage。 在终端窗口中运行以下命令以创建具有 index 操作的主页控制器。

rails g controller Homepage index

注意: 在 Linux 上,如果您遇到错误 FATAL: Listen error: unable to monitor directories for changes.,这是由于系统限制了您的机器可以监控更改的文件数量。 运行以下命令来修复它:

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

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


运行此命令会生成以下文件:

  • 一个 homepage_controller.rb 文件,用于接收所有与主页相关的请求。 此文件包含您在命令中指定的 index 操作。
  • 一个 homepage.js 文件,用于添加与 Homepage 控制器相关的任何 JavaScript 行为。
  • 一个 homepage.scss 文件,用于添加与 Homepage 控制器相关的样式。
  • 一个 homepage_helper.rb 文件,用于添加与 Homepage 控制器相关的辅助方法。
  • 一个 index.html.erb 文件,它是用于呈现与主页相关的任何内容的视图页面。

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

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

nano config/routes.rb

在此文件中,将 get 'homepage/index' 替换为 root 'homepage#index',使文件如下所示:

~/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 控制器的 index 操作,该操作依次呈现位于 index.html.erb X201X] 到浏览器。

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

rails s --binding=127.0.0.1

在浏览器中打开应用程序,您将看到应用程序的新登录页面:

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

接下来,打开 ~/rails_react_recipe/app/views/homepage/index.html.erb 文件,删除文件中的代码,然后将文件保存为空。 通过这样做,您将确保 index.html.erb 的内容不会干扰前端的 React 渲染。

现在您已经为您的应用程序设置了主页,您可以转到下一部分,您将在其中配置应用程序的前端以使用 React。

第 5 步 — 将 React 配置为 Rails 前端

在这一步中,您将配置 Rails 以在应用程序的前端使用 React,而不是其模板引擎。 这将允许您利用 React 渲染来创建更具视觉吸引力的主页。

Rails 在 Webpacker gem 的帮助下,将所有 JavaScript 代码捆绑到 packs 中。 这些可以在 app/javascript/packs 的包目录中找到。 您可以使用 javascript_pack_tag 帮助器在 Rails 视图中链接这些包,并且可以使用 stylesheet_pack_tag 帮助器链接导入到包中的样式表。 要为您的 React 环境创建一个入口点,您需要将其中一个包添加到您的应用程序布局中。

首先,将 ~/rails_react_recipe/app/javascript/packs/hello_react.jsx 文件重命名为 ~/rails_react_recipe/app/javascript/packs/Index.jsx

mv ~/rails_react_recipe/app/javascript/packs/hello_react.jsx ~/rails_react_recipe/app/javascript/packs/Index.jsx

重命名文件后,打开【X30X】【X34X】,应用布局文件:

nano ~/rails_react_recipe/app/views/layouts/application.html.erb

在应用程序布局文件的 head 标记末尾添加以下突出显示的代码行:

~/rails_react_recipe/app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>RailsReactRecipe</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <%= javascript_pack_tag 'Index' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

将 JavaScript 包添加到应用程序的标头可让您的所有 JavaScript 代码可用,并在您运行应用程序时执行页面上 Index.jsx 文件中的代码。 除了 JavaScript 包,您还添加了 meta viewport 标签来控制应用程序页面的尺寸和缩放。

保存并退出文件。

现在您的入口文件已加载到页面上,为您的主页创建一个 React 组件。 首先在 app/javascript 目录下创建一个 components 目录:

mkdir ~/rails_react_recipe/app/javascript/components

components 目录将包含主页组件以及应用程序中的其他 React 组件。 主页将包含一些文本和一个号召性用语按钮来查看所有食谱。

在您的编辑器中,在 components 目录中创建一个 Home.jsx 文件:

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目录:

mkdir ~/rails_react_recipe/app/javascript/routes

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

routes目录下,创建一个Index.jsx文件:

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, Route, Switch } from "react-router-dom";
import Home from "../components/Home";

export default (
  <Router>
    <Switch>
      <Route path="/" exact component={Home} />
    </Switch>
  </Router>
);

在这个 Index.jsx 路由文件中,您导入了几个模块:允许我们使用 React 的 React 模块,以及 BrowserRouterRoute 和来自 React Router 的 Switch 模块,它们共同帮助我们从一条路线导航到另一条路线。 最后,您导入了 Home 组件,只要请求与根 (/) 路由匹配,就会呈现该组件。 每当您想向应用程序添加更多页面时,您需要做的就是在此文件中声明一个路由并将其与您要为该页面呈现的组件匹配。

保存并退出文件。

您现在已经使用 React Router 成功设置了路由。 为了让 React 知道可用的路由并使用它们,这些路由必须在应用程序的入口点可用。 为此,您将在 React 将在您的入口文件中呈现的组件中呈现您的路线。

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

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

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

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

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

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

App.jsx 文件中,您导入了 React 和您刚刚创建的路由文件。 然后,您导出了一个在 fragments 中呈现路由的组件。 该组件将在应用程序的入口点呈现,从而在加载应用程序时使路由可用。

现在你已经设置了 App.jsx,是时候在你的入口文件中渲染它了。 打开条目 Index.jsx 文件:

nano ~/rails_react_recipe/app/javascript/packs/Index.jsx

将那里的代码替换为以下代码:

~/rails_react_recipe/app/javascript/packs/Index.jsx

import React from "react";
import { render } from "react-dom";
import 'bootstrap/dist/css/bootstrap.min.css';
import $ from 'jquery';
import Popper from 'popper.js';
import 'bootstrap/dist/js/bootstrap.bundle.min';
import App from "../components/App";

document.addEventListener("DOMContentLoaded", () => {
  render(
    <App />,
    document.body.appendChild(document.createElement("div"))
  );
});

在此代码片段中,您导入了 React、来自 ReactDOM、Bootstrap、jQuery、Popper.js 的渲染方法以及您的 App 组件。 使用 ReactDOM 的 render 方法,您将 App 组件渲染到 div 元素中,该元素附加到页面主体。 每当加载应用程序时,React 都会在页面上的 div 元素内呈现 App 组件的内容。

保存并退出文件。

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

~/rails_react_recipe/app/assets/stylesheets 目录中打开 application.css

nano ~/rails_react_recipe/app/assets/stylesheets/application.css

接下来,将 application.css 文件的内容替换为以下代码:

~/rails_react_recipe/app/assets/stylesheets/application.css

.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;
}
.custom-button.btn:hover {
  color: #FFF !important;
  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;
}

这将为 英雄图像 或网站首页上的大型 Web 横幅创建框架,稍后您将添加该框架。 此外,这设置了用户用于进入应用程序的按钮的样式。

准备好 CSS 样式后,保存并退出文件。 接下来,重新启动应用程序的 Web 服务器,然后在浏览器中重新加载应用程序。 您将看到一个全新的主页:

在这一步中,您配置了您的应用程序,以便它使用 React 作为其前端。 在下一节中,您将创建允许您创建、读取、更新和删除配方的模型和控制器。

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

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

首先使用 Rails 提供的 generate model 子命令创建一个 Recipe 模型,并指定模型的名称及其列和数据类型。 在终端窗口中运行以下命令以创建 Recipe 模型:

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

上述命令指示 Rails 创建一个 Recipe 模型以及一个 string 类型的 name 列、一个 ingredientsinstruction 类型的列类型 text,以及 string 类型的 image 列。 本教程将模型命名为 Recipe,因为按照惯例,Rails 中的模型使用单数名称,而它们对应的数据库表使用复数名称。

运行 generate model 命令会创建两个文件:

  • 一个包含所有模型相关逻辑的 recipe.rb 文件。
  • 20190407161357_create_recipes.rb 文件(文件开头的数字可能因运行命令的日期而异)。 这是一个迁移文件,其中包含创建数据库结构的说明。

接下来,编辑配方模型文件以确保仅将有效数据保存到数据库中。 您可以通过向模型添加一些数据库验证来实现这一点。 打开位于 app/models/recipe.rb 的配方模型:

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

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

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

在此代码中,您添加了模型验证,用于检查 nameingredientsinstruction 字段是否存在。 如果没有这三个字段,配方是无效的,不会被保存到数据库中。

保存并退出文件。

要让 Rails 在数据库中创建 recipes 表,您必须运行 migration,这在 Rails 中是一种以编程方式更改数据库的方法。 为确保迁移适用于您设置的数据库,有必要对 20190407161357_create_recipes.rb 文件进行更改。

在编辑器中打开此文件:

nano ~/rails_react_recipe/db/migrate/20190407161357_create_recipes.rb

添加以下突出显示的行,使文件如下所示:

db/migrate/20190407161357_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

此迁移文件包含一个带有 change 方法的 Ruby 类,以及用于创建名为 recipes 的表以及列及其数据类型的命令。 您还通过添加 null: falsenameingredientsinstruction 列上使用 NOT NULL 约束更新了 20190407161357_create_recipes.rb ,确保这些列在更改数据库之前具有值。 最后,您为图片列添加了默认图片 URL; 如果您想使用不同的图像,这可能是另一个 URL。

通过这些更改,保存并退出文件。 您现在已准备好运行迁移并实际创建表。 在终端窗口中,运行以下命令:

rails db:migrate

在这里,您使用了 database migrate 命令,该命令执行迁移文件中的指令。 命令成功运行后,您将收到类似于以下内容的输出:

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

使用您的配方模型,创建您的配方控制器并添加用于创建、读取和删除配方的逻辑。 在终端窗口中,运行以下命令:

rails generate controller api/v1/Recipes index create show destroy -j=false -y=false --skip-template-engine --no-helper

在此命令中,您在 api/v1 目录中创建了一个 Recipes 控制器,其中包含 indexcreateshowdestroy 动作。 index 动作将处理获取所有配方,create 动作将负责创建新配方,show 动作将获取单个配方,而 destroy 动作将保存删除配方的逻辑。

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

  • -j=false 指示 Rails 跳过生成关联的 JavaScript 文件。
  • -y=false 指示 Rails 跳过生成关联的样式表文件。
  • --skip-template-engine,指示 Rails 跳过生成 Rails 视图文件,因为 React 正在处理您的前端需求。
  • --no-helper,指示 Rails 跳过为控制器生成帮助文件。

运行该命令还会使用 Recipes 控制器中每个操作的路由更新您的路由文件。 要使用这些路由,请更改您的 config/routes.rb 文件。

在文本编辑器中打开路由文件:

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'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

在此路由文件中,您修改了 createdestroy 路由的 HTTP 动词,以便它可以接收 postdelete 数据。 您还通过在路由中添加 :id 参数来修改 showdestroy 操作的路由。 :id 将保存您要读取或删除的配方的标识号。

您还使用 get '/*path' 添加了一条捕获所有路由,该路由会将与现有路由不匹配的任何其他请求定向到 homepage 控制器的 index 操作。 这样,前端的路由将处理与创建、读取或删除配方无关的请求。

保存并退出文件。

要查看应用程序中可用的路由列表,请在终端窗口中运行以下命令:

rails routes

运行此命令会显示项目的 URI 模式、动词和匹配的控制器或操作的列表。

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

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

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

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

在您的 index 操作中,使用 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

create 动作中,您使用 ActiveRecord 的 create 方法来创建新配方。 create 方法能够一次性分配所有提供给模型的控制器参数。 这使得创建记录变得容易,但也打开了恶意使用的可能性。 这可以通过使用 Rails 提供的称为 强参数 的功能来防止。 这样,除非参数被列入白名单,否则无法分配参数。 在您的代码中,您将 recipe_params 参数传递给 create 方法。 recipe_params 是一种 private 方法,您可以在其中将控制器参数列入白名单,以防止错误或恶意内容进入您的数据库。 在这种情况下,您允许 nameimageingredientsinstruction 参数有效使用 create 方法。

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

~/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
    if recipe
      render json: recipe
    else
      render json: recipe.errors
    end
  end

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

  private

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

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

在新的代码行中,您创建了一个私有的 recipe 方法。 recipe 方法使用 ActiveRecord 的 find 方法查找 id params 中提供的 id 匹配的配方,并将其分配给实例变量 @recipe。 在 show 操作中,您检查了配方是否由 recipe 方法返回并将其作为 JSON 响应发送,如果不是则发送错误。

destroy 操作中,您使用 Ruby 的安全导航运算符 &. 做了类似的事情,这避免了调用方法时出现 nil 错误。 这让您只有在配方存在时才删除它,然后发送一条消息作为响应。

现在您已完成对 recipes_controller.rb 的这些更改,保存文件并退出文本编辑器。

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

第 7 步 - 查看食谱

在本节中,您将创建用于查看配方的组件。 首先,您将创建一个页面,您可以在其中查看所有现有食谱,然后再创建一个页面来查看各个食谱。

您将首先创建一个页面来查看所有食谱。 但是,在您执行此操作之前,您需要使用配方,因为您的数据库当前是空的。 Rails 为我们提供了为您的应用程序创建种子数据的机会。

打开种子文件seeds.rb进行编辑:

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 创建九个具有 nameingredientsinstruction 的配方。 保存并退出文件。

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

rails db:seed

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

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

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

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

打开文件后,通过添加以下行将 React 和 Link 模块导入其中:

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

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

接下来,创建一个扩展 React.Component 类的 Recipes 类。 添加以下突出显示的代码以创建扩展 React.Component 的 React 组件:

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

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

class Recipes extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      recipes: []
    };
  }

}
export default Recipes;

构造函数 中,我们正在初始化一个 state 对象,该对象保存配方的状态,初始化时它是一个空数组 ([])。

接下来,在 Recipe 类中添加一个 componentDidMount 方法。 componentDidMount 方法是一个 React 生命周期方法,在安装组件后立即调用。 在此生命周期方法中,您将调用以获取所有食谱。 为此,请添加以下行:

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

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

class Recipes extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      recipes: []
    };
  }

  componentDidMount() {
      const url = "/api/v1/recipes/index";
      fetch(url)
        .then(response => {
          if (response.ok) {
            return response.json();
          }
          throw new Error("Network response was not ok.");
        })
        .then(response => this.setState({ recipes: response }))
        .catch(() => this.props.history.push("/"));
  }

}
export default Recipes;

在您的 componentDidMount 方法中,您使用 Fetch API 进行了 HTTP 调用以获取所有配方。 如果响应成功,则应用程序将配方数组保存到配方状态。 如果有错误,它会将用户重定向到主页。

最后,在 Recipe 类中添加一个 render 方法。 render 方法保存 React 元素,这些元素将在渲染组件时被评估并显示在浏览器页面上。 在这种情况下,render 方法将从组件状态渲染配方卡片。 将以下突出显示的行添加到 Recipes.jsx

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

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

class Recipes extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      recipes: []
    };
  }

  componentDidMount() {
    const url = "/api/v1/recipes/index";
    fetch(url)
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(response => this.setState({ recipes: response }))
      .catch(() => this.props.history.push("/"));
  }
  render() {
    const { recipes } = this.state;
    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-right mb-3">
              <Link to="/recipe" className="btn custom-button">
                Create New Recipe
              </Link>
            </div>
            <div className="row">
              {recipes.length > 0 ? allRecipes : noRecipe}
            </div>
            <Link to="/" className="btn btn-link">
              Home
            </Link>
          </main>
        </div>
      </>
    );
  }
}
export default Recipes;

保存并退出Recipes.jsx

现在您已经创建了一个组件来显示所有配方,下一步是为它创建一个路由。 打开位于app/javascript/routes/Index.jsx的前端路由文件:

nano app/javascript/routes/Index.jsx

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

~/rails_react_recipe/app/javascript/routes/Index.jsx

import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";

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

保存并退出文件。

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

rails s --binding=127.0.0.1

继续并在浏览器中打开该应用程序。 通过单击主页上的 View Recipe 按钮,您将看到带有种子配方的显示:

在终端窗口中使用 CTRL+C 停止服务器并恢复提示。

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

nano app/javascript/components/Recipe.jsx

Recipes 组件一样,通过添加以下行来导入 React 和 Link 模块:

~/rails_react_recipe/app/javascript/components/Recipe.jsx

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

接下来通过添加突出显示的代码行来创建扩展 React.Component 类的 Recipe 类:

~/rails_react_recipe/app/javascript/components/Recipe.jsx

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

class Recipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = { recipe: { ingredients: "" } };

    this.addHtmlEntities = this.addHtmlEntities.bind(this);
  }
}

export default Recipe;

与您的 Recipes 组件一样,在构造函数中,您初始化了一个包含配方状态的状态对象。 您还将 addHtmlEntities 方法绑定到 this 以便可以在组件内访问它。 addHtmlEntities 方法将用于在组件中将字符实体替换为 HTML 实体

为了找到特定的配方,您的应用程序需要配方的 id。 这意味着您的 Recipe 组件需要 id param。 您可以通过传递给组件的 props 来访问它。

接下来,添加一个 componentDidMount 方法,您将从 props 对象的 match 键访问 id param。 一旦获得 id,您将发出 HTTP 请求以获取配方。 将以下突出显示的行添加到您的文件中:

~/rails_react_recipe/app/javascript/components/Recipe.jsx

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

class Recipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = { recipe: { ingredients: "" } };

    this.addHtmlEntities = this.addHtmlEntities.bind(this);
  }

  componentDidMount() {
    const {
      match: {
        params: { id }
      }
    } = this.props;

    const url = `/api/v1/show/${id}`;

    fetch(url)
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(response => this.setState({ recipe: response }))
      .catch(() => this.props.history.push("/recipes"));
  }

}

export default Recipe;

componentDidMount 方法中,使用 对象解构 ,从 props 对象中获取 id param,然后使用 Fetch API ,您发出 HTTP 请求以获取拥有 id 的配方并使用 setState 方法将其保存到组件状态。 如果食谱不存在,应用程序会将用户重定向到食谱页面。

现在添加 addHtmlEntities 方法,该方法接受一个字符串并将所有转义的左括号和右括号替换为其 HTML 实体。 这将帮助我们转换保存在您的配方指令中的任何转义字符:

~/rails_react_recipe/app/javascript/components/Recipe.jsx

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

class Recipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = { recipe: { ingredients: "" } };

    this.addHtmlEntities = this.addHtmlEntities.bind(this);
  }

  componentDidMount() {
    const {
      match: {
        params: { id }
      }
    } = this.props;

    const url = `/api/v1/show/${id}`;

    fetch(url)
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(response => this.setState({ recipe: response }))
      .catch(() => this.props.history.push("/recipes"));
  }

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

export default Recipe;

最后,添加一个 render 方法,该方法从状态中获取配方并将其呈现在页面上。 为此,请添加以下突出显示的行:

~/rails_react_recipe/app/javascript/components/Recipe.jsx

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

class Recipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = { recipe: { ingredients: "" } };

    this.addHtmlEntities = this.addHtmlEntities.bind(this);
  }

  componentDidMount() {
    const {
      match: {
        params: { id }
      }
    } = this.props;

    const url = `/api/v1/show/${id}`;

    fetch(url)
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(response => this.setState({ recipe: response }))
      .catch(() => this.props.history.push("/recipes"));
  }

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

  render() {
    const { recipe } = this.state;
    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>
        ));
    }
    const recipeInstruction = this.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;

在这个 render 方法中,您将逗号分隔的成分拆分为一个数组并在其上映射,从而创建一个成分列表。 如果没有成分,应用程序会显示一条消息,显示 没有可用的成分 。 它还将配方图像显示为主图像,在配方说明旁边添加一个删除配方按钮,并添加一个链接回配方页面的按钮。

保存并退出文件。

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

nano app/javascript/routes/Index.jsx

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

~/rails_react_recipe/app/javascript/routes/Index.jsx

import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";

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

在此路由文件中,您导入了 Recipe 组件并为其添加了路由。 它的路线有一个 :id param 将被您要查看的配方的 id 替换。

使用 rails s 命令再次启动服务器,然后在浏览器中访问 http://localhost:3000。 单击查看食谱按钮导航到食谱页面。 在配方页面上,通过单击其查看配方按钮查看任何配方。 您将看到一个填充了数据库数据的页面:

在本节中,您向数据库中添加了九个配方,并创建了组件来查看这些配方,无论是单独的还是作为一个集合。 在下一部分中,您将添加一个组件来创建配方。

第 8 步 - 创建食谱

拥有一个可用的食物食谱应用程序的下一步是创建新食谱的能力。 在此步骤中,您将创建一个用于创建配方的组件。 该组件将包含一个表单,用于从用户那里收集所需的配方详细信息,并将向 Recipe 控制器中的 create 操作发出请求以保存配方数据。

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

nano app/javascript/components/NewRecipe.jsx

在新文件中,导入您目前在其他组件中使用的 React 和 Link 模块:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

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

接下来创建一个扩展 React.Component 类的 NewRecipe 类。 添加以下突出显示的代码以创建扩展 react.Component 的 React 组件:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

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

class NewRecipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: "",
      ingredients: "",
      instruction: ""
    };

    this.onChange = this.onChange.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
  }
}

export default NewRecipe;

NewRecipe 组件的构造函数中,您使用空的 nameingredientsinstruction 字段初始化了状态对象。 这些是创建有效配方所需的字段。 你也有三种方法; onChangeonSubmitstripHtmlEntities,您绑定到 this。 这些方法将分别处理更新状态、表单提交以及将特殊字符(如 <)转换为其转义/编码值(如 &lt;)。

接下来,通过将突出显示的行添加到 NewRecipe 组件来创建 stripHtmlEntities 方法本身:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

class NewRecipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: "",
      ingredients: "",
      instruction: ""
    };

    this.onChange = this.onChange.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
  }

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

}

export default NewRecipe;

stripHtmlEntities 方法中,您将 <> 字符替换为其转义值。 这样您就不会在数据库中存储原始 HTML。

接下来将 onChangeonSubmit 方法添加到 NewRecipe 组件以处理表单的编辑和提交:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

class NewRecipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: "",
      ingredients: "",
      instruction: ""
    };

    this.onChange = this.onChange.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
  }

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

  onChange(event) {
    this.setState({ [event.target.name]: event.target.value });
  }

  onSubmit(event) {
    event.preventDefault();
    const url = "/api/v1/recipes/create";
    const { name, ingredients, instruction } = this.state;

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

    const body = {
      name,
      ingredients,
      instruction: instruction.replace(/\n/g, "<br> <br>")
    };

    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 => this.props.history.push(`/recipe/${response.id}`))
      .catch(error => console.log(error.message));
  }

}

export default NewRecipe;

onChange 方法中,您使用 ES6 计算属性名称 将每个用户输入的值设置为您所在状态的相应键。 在 onSubmit 方法中,您检查了所有必需的输入都不是空的。 然后构建一个对象,其中包含配方控制器创建新配方所需的参数。 使用 正则表达式 ,您将指令中的每个换行符替换为一个中断标记,这样您就可以保留用户输入的文本格式。

为了防止 Cross-Site Request Forgery (CSRF) 攻击,Rails 将 CSRF 安全令牌附加到 HTML 文档。 每当发出非 GET 请求时,都需要此令牌。 使用前面代码中的 token 常量,您的应用程序会验证服务器上的令牌,如果安全令牌与预期不匹配,则会引发异常。 在 onSubmit 方法中,应用程序通过 Rails 检索嵌入在 HTML 文档中的 CSRF 令牌,并使用 JSON 字符串发出 HTTP 请求。 如果成功创建配方,应用程序会将用户重定向到配方页面,他们可以在其中查看他们新创建的配方。

最后,添加一个 render 方法,该方法呈现一个表单,供用户输入用户希望创建的配方的详细信息:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

class NewRecipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: "",
      ingredients: "",
      instruction: ""
    };

    this.onChange = this.onChange.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
  }

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

  onChange(event) {
    this.setState({ [event.target.name]: event.target.value });
  }

  onSubmit(event) {
    event.preventDefault();
    const url = "/api/v1/recipes/create";
    const { name, ingredients, instruction } = this.state;

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

    const body = {
      name,
      ingredients,
      instruction: instruction.replace(/\n/g, "<br> <br>")
    };

    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 => this.props.history.push(`/recipe/${response.id}`))
      .catch(error => console.log(error.message));
  }

  render() {
    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={this.onSubmit}>
              <div className="form-group">
                <label htmlFor="recipeName">Recipe name</label>
                <input
                  type="text"
                  name="name"
                  id="recipeName"
                  className="form-control"
                  required
                  onChange={this.onChange}
                />
              </div>
              <div className="form-group">
                <label htmlFor="recipeIngredients">Ingredients</label>
                <input
                  type="text"
                  name="ingredients"
                  id="recipeIngredients"
                  className="form-control"
                  required
                  onChange={this.onChange}
                />
                <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={this.onChange}
              />
              <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;

在 render 方法中,您有一个包含三个输入字段的表单; 一种用于 recipeNamerecipeIngredientsinstruction。 每个输入字段都有一个调用 onChange 方法的 onChange 事件处理程序。 此外,提交按钮上有一个 onSubmit 事件处理程序,它调用 onSubmit 方法,然后提交表单数据。

保存并退出文件。

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

nano app/javascript/routes/Index.jsx

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

~/rails_react_recipe/app/javascript/routes/Index.jsx

import React from "react";
import { BrowserRouter as Router, Route, Switch } 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>
    <Switch>
      <Route path="/" exact component={Home} />
      <Route path="/recipes" exact component={Recipes} />
      <Route path="/recipe/:id" exact component={Recipe} />
      <Route path="/recipe" exact component={NewRecipe} />
    </Switch>
  </Router>
);

路线到位后,保存并退出您的文件。 重新启动您的开发服务器并在浏览器中访问 http://localhost:3000。 导航到配方页面并单击创建新配方按钮。 您将找到一个页面,其中包含将食谱添加到数据库的表单:

输入所需的配方详细信息,然后单击创建配方按钮; 您将在页面上看到新创建的食谱。

在这一步中,您通过添加创建食谱的功能使您的食物食谱应用程序栩栩如生。 在下一步中,您将添加删除配方的功能。

第 9 步 — 删除食谱

在本节中,您将修改您的食谱组件以能够删除食谱。

当您单击配方页面上的删除按钮时,应用程序将发送从数据库中删除配方的请求。 为此,请打开您的 Recipe.jsx 文件:

nano app/javascript/components/Recipe.jsx

Recipe 组件的构造函数中,将 this 绑定到 deleteRecipe 方法:

~/rails_react_recipe/app/javascript/components/Recipe.jsx

class Recipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = { recipe: { ingredients: "" } };
    this.addHtmlEntities = this.addHtmlEntities.bind(this);
    this.deleteRecipe = this.deleteRecipe.bind(this);
  }
...

现在向 Recipe 组件添加一个 deleteRecipe 方法:

~/rails_react_recipe/app/javascript/components/Recipe.jsx

class Recipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = { recipe: { ingredients: "" } };

    this.addHtmlEntities = this.addHtmlEntities.bind(this);
    this.deleteRecipe = this.deleteRecipe.bind(this);
  }

  componentDidMount() {
    const {
      match: {
        params: { id }
      }
    } = this.props;
    const url = `/api/v1/show/${id}`;
    fetch(url)
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(response => this.setState({ recipe: response }))
      .catch(() => this.props.history.push("/recipes"));
  }

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

  deleteRecipe() {
    const {
      match: {
        params: { id }
      }
    } = this.props;
    const url = `/api/v1/destroy/${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(() => this.props.history.push("/recipes"))
      .catch(error => console.log(error.message));
  }

  render() {
    const { recipe } = this.state;
    let ingredientList = "No ingredients available";
... 

deleteRecipe 方法中,您获取要删除的配方的 id,然后构建您的 url 并获取 CSRF 令牌。 接下来,您向 Recipes 控制器发出 DELETE 请求以删除配方。 如果成功删除配方,应用程序会将用户重定向到配方页面。

要在单击删除按钮时运行 deleteRecipe 方法中的代码,请将其作为单击事件处理程序传递给按钮。 在render方法中为删除按钮添加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={this.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 from "react";
import { Link } from "react-router-dom";

class Recipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = { recipe: { ingredients: "" } };

    this.addHtmlEntities = this.addHtmlEntities.bind(this);
    this.deleteRecipe = this.deleteRecipe.bind(this);
  }

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

  componentDidMount() {
    const {
      match: {
        params: { id }
      }
    } = this.props;
    const url = `/api/v1/show/${id}`;
    fetch(url)
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(response => this.setState({ recipe: response }))
      .catch(() => this.props.history.push("/recipes"));
  }

  deleteRecipe() {
    const {
      match: {
        params: { id }
      }
    } = this.props;
    const url = `/api/v1/destroy/${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(() => this.props.history.push("/recipes"))
      .catch(error => console.log(error.message));
  }

  render() {
    const { recipe } = this.state;
    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>
        ));
    }

    const recipeInstruction = this.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={this.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 的数据一文。