如何在 Ubuntu 18.04 上设置带有 React 前端的 Ruby on Rails v5 项目

作者选择了 Electronic Frontier Foundation作为 Write for Donations计划的一部分接受捐款。

介绍

Ruby on Rails是一个流行的服务器侧 Web 应用程序框架,有 超过 42000 星在 GitHub在写本教程时. 它支持许多在当今网络上存在的流行的应用程序,如 GitHub, Basecamp, SoundCloud, Airbnb,和 Twitch. 凭借其对程序员经验和围绕它建立的热情社区的重视,Ruby on Rails将为您提供构建和维护现代Web应用程序所需的工具。

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

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

在本教程中,您将创建一个Ruby on Rails应用程序,存储您最喜欢的食谱,然后使用React前端显示它们。

Completed Recipe App

如果你想看看这个应用程序的代码,请参阅DigitalOcean 社区 GitHub(https://github.com/do-community)上的同伴库(https://github.com/do-community/react_rails_recipe)这个教程。

前提条件

要遵循这个教程,你需要有以下内容:

  • 安装在您的开发机上的 [Node.js] (https://nodejs.org/en/)和 [npm] (https://www.npmjs.com/). 此教程使用"节点"(Node.js)版本 10.16.0和"npm"版本 6.9.0. Node.js是一个JavaScript运行时环境,允许您在浏览器外运行代码. 它带有一个预安装的软件包管理器,名为 [npm] (https://www.npmjs.com/),它允许您安装并更新软件包. 要在macOS或Ubuntu 18.04上安装这些设备,请遵循如何在macOS上安装节点.js并创建本地开发环境如何在Ubuntu 18.04上安装节点.js的"Installing use a PPA"部分的步骤。
  • Yarn 软件包管理器安装在您的开发机器上,可以下载回放框架. 此教程在1.16.0版本上进行了测试;为了安装此依赖,遵循[官方Yarn安装指 (https://yarnpkg.com/en/docs/install#debian-stable). *在铁路上安装Ruby框架. 为了获得这一点,请遵循我们的指南:[如何在铁路上安装Ruby,在Ubuntu 18.04 (https://andsky.com/tech/tutorials/how-to-install-ruby-on-rails-with-rbenv-on-ubuntu-18-04),或[如何在铁路上安装Ruby,在CentOS 7上安装rbenv (https://andsky.com/tech/tutorials/how-to-install-ruby-on-rails-with-rbenv-on-ubuntu-18-04)。 如果您想在 macOS 上开发此应用程序,请参见此教程 : [How To Instruction Ruby on Rails with rbenv on macOS] (https://andsky.com/tech/tutorials/how-to-install-ruby-on-rails-with-rbenv-on-macos). 这个教程在Ruby的2.6.3版本和Rails的5.2.3版本上进行了测试,因此在安装过程中确保指定这些版本.
  • 安装 PostgreSQL, 如我们的教程步骤1和2所显示 [如何在Ubuntu 18.04的铁路应用上使用PostgreSQL(https://andsky.com/tech/tutorials/how-to-use-postgresql-with-your-ruby-on-rails-application-on-ubuntu-18-04)或 [如何在macOS的铁路应用上使用PostgreSQL(https://andsky.com/tech/tutorials/how-to-use-postgresql-with-your-ruby-on-rails-application-on-macos). 要跟踪此教程, 请使用 PostgreSQL 10 版本 。 如果您想在 Linux 或另一个 OS 的不同发售上开发此应用程序,请参见 [官方 PostgreSQL 下载页] (https://www.postgresql.org/download/). 关于如何使用 PostgreSQL 的更多信息,请参见我们的[如何安装和使用 PostgreSQL] (https://andsky.com/tech/tutorials/how-to-install-and-use-postgresql-on-ubuntu-18-04) 教程. (英语)

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

在此步骤中,您将在 Rails 应用程序框架上构建您的食谱应用程序. 首先,您将创建一个新的 Rails 应用程序,该应用程序将被设置为在很少的配置下与 React 工作。

Rails 提供一系列被称为生成器的脚本,这些脚本有助于创建构建现代Web应用程序所需的一切内容. 若要查看这些命令的完整列表以及它们的功能,请在终端窗口中运行以下命令:

1rails -h

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

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

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

上一个命令在名为rails_react_recipe的目录中创建了一个新的 Rails 应用程序,安装所需的 Ruby 和 JavaScript 依赖,并配置 Webpack。

  • -d 旗指明了您偏好的数据库引擎,在这种情况下是 PostgreSQL。
  • -T 旗指示 Rails 跳过测试文件的生成,因为您不会为本教程编写测试。 如果您想使用 Ruby 测试工具不同于 Rails 提供的工具,此命令也被建议使用。
  • The --webpack instructs Rails to preconfigure for JavaScript with the webpack bundler,在这种情况下,专门用于 React 应用程序。

一旦命令完成运行,请进入rails_react_recipe目录,该目录是您的应用程序的根目录:

1cd rails_react_recipe

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

1ls

这个 root 目录包含一系列自动生成的文件和文件夹,构成 Rails 应用程序的结构,包括包含 React 应用程序依赖性的 package.json 文件。

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

步骤二:建立数据库

在运行新 Rails 应用程序之前,您必须先将其连接到数据库. 在此步骤中,您将新创建的 Rails 应用程序连接到 PostgreSQL 数据库,以便在需要时存储和检索配方数据。

在config/database.yml中找到的 database.yml 文件包含数据库细节,例如不同开发环境的数据库名称。Rails 通过将环境名称附加到您的应用程序名称,指定不同开发环境的数据库名称。

<$>[注] 注: 在此时,您可以更改 config/database.yml 以设置您希望 Rails 使用哪些 PostgreSQL 角色来创建您的数据库。 如果您遵循前提 如何使用 PostgreSQL 与您的 Ruby on Rails 应用程序,并创建了一个由密码保护的角色,您可以遵循 Step 4 for macOSUbuntu 18.04中的说明。

如前面所述,Rails 提供了许多命令,以便轻松开发 Web 应用程序. 这包括用于与数据库工作的命令,如创建,放下重置

1rails db:create

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

1[secondary_label Output]
2Created database 'rails_react_recipe_development'
3Created database 'rails_react_recipe_test'

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

1rails s --binding=127.0.0.1

命令sserver会启动 Puma,这是默认情况下与 Rails 分布的 Web 服务器,而--binding=127.0.0.1会将服务器连接到您的本地主机

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

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

要查看你的应用程序,打开一个浏览器窗口,并导航到 http://localhost:3000. 你会看到 Rails 的默认欢迎页面:

Rails welcome page

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

要随时停止 Web 服务器,请在服务器正在运行的终端窗口按CTRL+C

1[secondary_label Output]
2^C- Gracefully stopping, waiting for requests to finish
3=== puma shutdown: 2019-07-31 14:21:24 -0400 ===
4- Goodbye!
5Exiting

你的速度会再次出现。

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

步骤3 - 安装前端依赖

在此步骤中,您将在食品配方应用程序的前端安装所需的 JavaScript 依赖性,其中包括:

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

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

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

1nano package.json

您将看到在依赖键下列出的安装包:

 1[label ~/rails_react_recipe/package.json]
 2{
 3  "name": "rails_react_recipe",
 4  "private": true,
 5  "dependencies": {
 6    "@babel/preset-react": "^7.0.0",
 7    "@rails/webpacker": "^4.0.7",
 8    "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
 9    "bootstrap": "^4.3.1",
10    "jquery": "^3.4.1",
11    "popper.js": "^1.15.0",
12    "prop-types": "^15.7.2",
13    "react": "^16.8.6",
14    "react-dom": "^16.8.6",
15    "react-router-dom": "^5.0.1"
16  },
17  "devDependencies": {
18    "webpack-dev-server": "^3.7.2"
19  }
20}

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

步骤四:创建主页

安装了所有必要的依赖性,在此步骤中,您将为应用程序创建一个主页,当用户第一次访问应用程序时,主页将作为目标页面。

Rails 遵循应用程序的 Model-View-Controller]架构模式。在 MVC 模式中,控制器的目的是接收特定请求,并将其传递到适当的模型或视图中。 现在,应用程序在 root URL 被加载到浏览器时会显示 Rails 欢迎页面。

Rails 提供一个控制器生成器来创建一个控制器。 控制器生成器收到一个控制器名称,以及一个匹配的操作。 有关更多信息,请参阅 官方 Rails 文档

本教程将命名控制器为主页。在终端窗口中运行以下命令,以创建一个索引操作的主页控制器。

1rails g controller Homepage index

<$>[注] 注: 在 Linux 上,如果您遇到错误 FATAL: Listen error: unable to monitor directories for changes.,这是由于系统限制了您的机器可以监控变更的文件数量。

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

这将永久增加你可以通过倾听监控的目录数量到524288

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

此文件包含您在命令中指定的索引操作。

  • 用于添加与主页控制器相关的任何JavaScript行为的homepage.js文件.
  • 用于添加与主页控制器相关的风格的homepage.scss文件.
  • 用于添加与主页控制器相关的辅助方法的homepage_helper.rb文件。

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

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

1nano config/routes.rb

在此檔案內,替換「get 'homepage/index'」為「root 'homepage#index'」,讓檔案看起來如下:

1[label ~/rails_react_recipe/config/routes.rb]
2Rails.application.routes.draw do
3  root 'homepage#index'
4  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
5end

此修改指示 Rails 将请求地图到应用程序的根源到主页控制器的索引操作,这反过来会将位于app/views/homepage/index.html.erb位置的index.html.erb文件中的任何东西返回浏览器。

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

1rails s --binding=127.0.0.1

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

Application Homepage

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

接下来,打开 ~/rails_react_recipe/app/views/homepage/index.html.erb 文件,删除文件内部的代码,然后将文件保存为空。

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

步骤 5 — 配置 React 作为您的 Rails Frontend

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

Rails 使用 Webpacker gem组合您的所有 JavaScript 代码到 packs. 这些可在app/javascript/packs的包目录中找到。 您可以在 Rails 视图中使用javascript_pack_tag辅助程序链接这些包,并且可以使用stylesheet_pack_tag辅助程序链接导入到包中的风格表。 为了创建您的 React 环境的入口点,您将将其中一个包添加到您的应用程序布局中。

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

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

重新命名文件后,打开application.html.erb,即应用程序布局文件:

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

在应用程序布局文件中的头标签末尾添加以下突出的代码行:

 1[label ~/rails_react_recipe/app/views/layouts/application.html.erb]
 2<!DOCTYPE html>
 3<html>
 4  <head>
 5    <title>RailsReactRecipe</title>
 6    <%= csrf_meta_tags %>
 7    <%= csp_meta_tag %>
 8
 9    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
10    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
11    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
12    <%= javascript_pack_tag 'Index' %>
13  </head>
14
15  <body>
16    <%= yield %>
17  </body>
18</html>

将JavaScript包添加到应用程序的标题中,使您的所有JavaScript代码可用,并在每次运行应用程序时在页面上的Index.jsx文件中执行代码。

保存和退出文件。

现在,您的入门文件已上传到页面上,为您的主页创建一个 React 组件,开始在应用程序/javascript目录中创建一个组件目录:

1mkdir ~/rails_react_recipe/app/javascript/components

组件目录将容纳主页的组件,以及应用程序中的其他 React 组件. 主页将包含一些文本和呼吁行动按钮,以查看所有食谱。

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

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

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

 1[label ~/rails_react_recipe/app/javascript/components/Home.jsx]
 2import React from "react";
 3import { Link } from "react-router-dom";
 4
 5export default () => (
 6  <div className="vw-100 vh-100 primary-color d-flex align-items-center justify-content-center">
 7    <div className="jumbotron jumbotron-fluid bg-transparent">
 8      <div className="container secondary-color">
 9        <h1 className="display-4">Food Recipes</h1>
10        <p className="lead">
11          A curated list of recipes for the best homemade meal and delicacies.
12        </p>
13        <hr className="my-4" />
14        <Link
15          to="/recipes"
16          className="btn btn-lg custom-button"
17          role="button"
18        >
19          View Recipes
20        </Link>
21      </div>
22    </div>
23  </div>
24);

在此代码中,您从 React Router 中导入了 React 以及Link 组件,而Link 组件会创建一个超链接来导航页面。

有了您的主页组件,您现在将使用 React Router 设置路由器,在应用/javascript目录中创建一个路由器目录:

1mkdir ~/rails_react_recipe/app/javascript/routes

路线目录将包含几个路线及其相应组件. 每次加载任何指定的路线,它将其相应的组件返回浏览器。

路线目录中,创建一个Index.jsx文件:

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

添加以下代码:

 1[label ~/rails_react_recipe/app/javascript/routes/Index.jsx]
 2import React from "react";
 3import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
 4import Home from "../components/Home";
 5
 6export default (
 7  <Router>
 8    <Switch>
 9      <Route path="/" exact component={Home} />
10    </Switch>
11  </Router>
12);

在这个Index.jsx路由文件中,你导入了几个模块:允许我们使用React的React模块,以及React Router的BrowserRouter,RouteSwitch模块,这些模块一起帮助我们从一个路由导航到另一个路由。最后,你导入了你的Home组件,每当请求与根(/)路由相匹配时,它就会被渲染。

保存和退出文件。

您现在已经成功设置了使用 React Router 的路由器。为了让 React 了解可用的路由器并使用它们,路由器必须在应用程序的入口处可用。

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

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

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

1[label ~/rails_react_recipe/app/javascript/components/App.jsx]
2import React from "react";
3import Routes from "../routes/Index";
4
5export default props => <>{Routes}</>;

App.jsx文件中,您导入了 React 和您刚刚创建的路由文件,然后导出了一组件,该组件在 fragments内渲染路由。

现在你已经设置了App.jsx,现在是时候将其渲染到你的入门文件中了。

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

将代码替换为以下代码:

 1[label ~/rails_react_recipe/app/javascript/packs/Index.jsx]
 2import React from "react";
 3import { render } from "react-dom";
 4import 'bootstrap/dist/css/bootstrap.min.css';
 5import $ from 'jquery';
 6import Popper from 'popper.js';
 7import 'bootstrap/dist/js/bootstrap.bundle.min';
 8import App from "../components/App";
 9
10document.addEventListener("DOMContentLoaded", () => {
11  render(
12    <App />,
13    document.body.appendChild(document.createElement("div"))
14  );
15});

在本代码片段中,您已导入 React、ReactDOM、Bootstrap、jQuery、Popper.js 和您的应用程序组件的渲染方法. 使用 ReactDOM 的渲染方法,您将您的应用程序组件渲染为一个div元素,该元素附加到页面体内。

保存和退出文件。

最后,添加一些CSS风格到您的主页。

在您的 ~/rails_react_recipe/app/assets/stylesheets 目录中打开您的 application.css:

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

接下来,用application.css文件的内容代替跟踪代码:

 1[label ~/rails_react_recipe/app/assets/stylesheets/application.css]
 2.bg_primary-color {
 3  background-color: #FFFFFF;
 4}
 5.primary-color {
 6  background-color: #FFFFFF;
 7}
 8.bg_secondary-color {
 9  background-color: #293241;
10}
11.secondary-color {
12  color: #293241;
13}
14.custom-button.btn {
15  background-color: #293241;
16  color: #FFF;
17  border: none;
18}
19.custom-button.btn:hover {
20  color: #FFF !important;
21  border: none;
22}
23.hero {
24  width: 100vw;
25  height: 50vh;
26}
27.hero img {
28  object-fit: cover;
29  object-position: top;
30  height: 100%;
31  width: 100%;
32}
33.overlay {
34  height: 100%;
35  width: 100%;
36  opacity: 0.4;
37}

这将创建一个 hero image 框架,或在您的网站首页上一个大的网页标志,您将稍后添加。

有了你的CSS风格,保存并退出文件。接下来,重新启动你的应用程序的Web服务器,然后在你的浏览器中重新加载应用程序。

Homepage Style

在此步骤中,您将配置您的应用程序以使用 React 作为其前端. 在下一部分,您将创建模型和控制器,允许您创建,阅读,更新和删除配方。

步骤 6 – 创建食谱控制器和模型

现在您已经为您的应用程序设置了 React 前端,在此步骤中您将创建一个食谱模型和控制器。食谱模型将代表数据库表,该表将包含有关用户食谱的信息,而控制器将接收和处理创建、阅读、更新或删除食谱的请求。

首先,使用 Rails 提供的生成模型子命令创建食谱模型,并指定该模型的名称以及其列和数据类型。

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

之前的命令指示 Rails 创建一个食谱模型,加上一个名称类型的字符串列,一个成分指令类型的文本列,以及一个图像类型的字符串列。

运行生成模型命令会创建两个文件:

  • 包含所有与模型相关的逻辑的 recipe.rb 文件.
  • A 20190407161357_create_recipes.rb 文件(文件开始时的号码可能因执行命令的日期而有所不同)。

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

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

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

1class Recipe < ApplicationRecord
2  validates :name, presence: true
3  validates :ingredients, presence: true
4  validates :instruction, presence: true
5end

在此代码中,您添加了模型验证,该验证对名称成分说明字段的存在进行检查。

保存并删除文件。

要让 Rails 在您的数据库中创建食谱表,您必须运行 迁移,这在 Rails 中是对您的数据库进行编程更改的一种方式。 为了确保迁移与您设置的数据库工作,您需要对 20190407161357_create_recipes.rb 文件进行更改。

在您的编辑器中打开此文件:

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

添加以下突出的行,使文件看起来像这样:

 1[label db/migrate/20190407161357_create_recipes.rb]
 2class CreateRecipes < ActiveRecord::Migration[5.2]
 3  def change
 4    create_table :recipes do |t|
 5      t.string :name, null: false
 6      t.text :ingredients, null: false
 7      t.text :instruction, null: false
 8      t.string :image, default: 'https://raw.githubusercontent.com/do-community/react_rails_recipe/master/app/assets/images/Sammy_Meal.jpg'
 9      t.timestamps
10    end
11  end
12end

此迁移文件包含一个 Ruby 类,具有更改方法,以及创建一个名为食谱的表命令,以及列和它们的数据类型。您还更新了20190407161357_create_recipes.rb并对名称,成分说明列添加了null: false,确保这些列在更改数据库之前具有值。

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

1rails db:migrate

在这里,您使用了数据库迁移命令,该命令执行迁移文件中的指令.一旦命令成功运行,您将收到类似于以下的输出:

1[secondary_label Output]
2== 20190407161357 CreateRecipes: migrating ====================================
3-- create_table(:recipes)
4   -> 0.0140s
5== 20190407161357 CreateRecipes: migrated (0.0141s) ===========================

使用您的食谱模型,创建食谱控制器,并添加创建、阅读和删除食谱的逻辑。

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

在这个命令中,你在一个api/v1的目录中创建了一个食谱控制器,其中包含一个索引,创建,显示摧毁操作,索引操作将处理你所有的食谱,创建操作将负责创建新食谱,显示操作将收集单个食谱,而摧毁操作将保持删除食谱的逻辑。

您还通过了一些旗帜,使控制器更轻,包括:

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

运行该命令还更新了您的路线文件,在食谱控制器中为每个操作提供一条路线。

在您的文本编辑器中打开路线文件:

1nano ~/rails_react_recipe/config/routes.rb

一旦打开,更新它看起来像以下代码,更改或添加突出的行:

 1[label ~/rails_react_recipe/config/routes.rb]
 2Rails.application.routes.draw do
 3  namespace :api do
 4    namespace :v1 do
 5      get 'recipes/index'
 6      post 'recipes/create'
 7      get '/show/:id', to: 'recipes#show'
 8      delete '/destroy/:id', to: 'recipes#destroy'
 9    end
10  end
11  root 'homepage#index'
12  get '/*path' => 'homepage#index'
13  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
14end

在此路线文件中,您修改了创建破坏路线的HTTP口号,以便它可以发布删除数据。

您还添加了一种捕捉所有路径的获取 / * 路径,该路径将将不匹配现有路径的任何其他请求导向到主页控制器的索引`操作中。

保存和退出文件。

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

1rails routes

运行此命令会显示 URI 模式、口号和对应控制器或项目操作的列表。

接下来,添加逻辑以同时获取所有食谱。Rails 使用 ActiveRecord库来处理类似此类的数据库相关任务。

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

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

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

将以下突出的代码行添加到食谱控制器中:

 1[label ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb]
 2class Api::V1::RecipesController < ApplicationController
 3  def index
 4    recipe = Recipe.all.order(created_at: :desc)
 5    render json: recipe
 6  end
 7
 8  def create
 9  end
10
11  def show
12  end
13
14  def destroy
15  end
16end

在你的索引操作中,使用ActiveRecord提供的所有方法,你会得到你的数据库中的所有食谱。使用订单方法,你会按照创建日期的下行顺序排列它们。

接下来,添加创建新食谱的逻辑。与收集所有食谱一样,您将依赖 ActiveRecord 来验证和保存所提供的食谱细节。

 1[label ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb]
 2class Api::V1::RecipesController < ApplicationController
 3  def index
 4    recipe = Recipe.all.order(created_at: :desc)
 5    render json: recipe
 6  end
 7
 8  def create
 9    recipe = Recipe.create!(recipe_params)
10    if recipe
11      render json: recipe
12    else
13      render json: recipe.errors
14    end
15  end
16
17  def show
18  end
19
20  def destroy
21  end
22
23  private
24
25  def recipe_params
26    params.permit(:name, :image, :ingredients, :instruction)
27  end
28end

创建操作中,你可以使用 ActiveRecord 的创建方法创建一个新配方。创建方法有能力同时分配所有控制器参数到模型中。这使得创建记录很容易,但也打开了恶意使用的可能性。这可以通过使用 Rails 提供的功能来防止,称为 强度参数。这样,除非它们已被白列表,否则无法分配参数。在您的代码中,您将一个recipe_params参数转移到创建方法。recipe_params是一种私有方法,您将控制器参数标注为防止错误或恶意内容进入您的数据库。在这种情况下,您正在允许使用有效的创建方法的名称,`图

您的食谱控制器现在可以读取和创建食谱. 剩下的只是阅读和删除单一食谱的逻辑。

 1[label ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb]
 2class Api::V1::RecipesController < ApplicationController
 3  def index
 4    recipe = Recipe.all.order(created_at: :desc)
 5    render json: recipe
 6  end
 7
 8  def create
 9    recipe = Recipe.create!(recipe_params)
10    if recipe
11      render json: recipe
12    else
13      render json: recipe.errors
14    end
15  end
16
17  def show
18    if recipe
19      render json: recipe
20    else
21      render json: recipe.errors
22    end
23  end
24
25  def destroy
26    recipe&.destroy
27    render json: { message: 'Recipe deleted!' }
28  end
29
30  private
31
32  def recipe_params
33    params.permit(:name, :image, :ingredients, :instruction)
34  end
35
36  def recipe
37    @recipe ||= Recipe.find(params[:id])
38  end
39end

在新的代码行中,您创建了一种私人食谱方法。食谱方法使用ActiveRecord的查找方法来找到一个食谱,其id匹配了params中提供的id,并将其分配给一个实例变量@recipe

摧毁操作中,您使用 Ruby 的安全导航操作器&做了一些类似的事情,在调用方法时避免错误。

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

在此步骤中,您为您的食谱创建了一个模型和控制器. 您已经写下了在后端工作与食谱所需的所有逻辑. 在下一节中,您将创建组件以查看食谱。

步骤7 - 查看食谱

在本节中,您将创建用于查看食谱的组件. 首先,您将创建一个页面,在那里您可以查看所有现有的食谱,然后另一个页面,以查看单个食谱。

您将首先创建一个页面,以查看所有食谱,但是,在您可以这样做之前,您需要使用食谱,因为您的数据库目前是空的。

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

1nano ~/rails_react_recipe/db/seeds.rb

用以下代码取代本种子文件的内容:

1[label ~/rails_react_recipe/db/seeds.rb]
29.times do |i|
3  Recipe.create(
4    name: "Recipe #{i + 1}",
5    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)',
6    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.'
7  )
8end

在这个代码中,你使用循环指示 Rails 创建九种配方,其中包含一个名称,一个成分和一个说明

若要将此数据播放到数据库中,请在终端窗口中运行以下命令:

1rails db:seed

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

查看所有食谱的组件将对RecipesController中的索引操作进行HTTP请求,以获取所有食谱的列表。

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

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

一旦文件打开,请通过添加以下行来导入 React 和 Link 模块:

1[label ~/rails_react_recipe/app/javascript/components/Recipes.jsx]
2import React from "react";
3import { Link } from "react-router-dom";

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

 1[label ~/rails_react_recipe/app/javascript/components/Recipes.jsx]
 2import React from "react";
 3import { Link } from "react-router-dom";
 4
 5class Recipes extends React.Component {
 6  constructor(props) {
 7    super(props);
 8    this.state = {
 9      recipes: []
10    };
11  }
12
13}
14export default Recipes;

在 [constructor] ( https://reactjs.org/docs/react-component.html#constructor) 中,我们正在初始化一个 [state] ( https://reactjs.org/docs/react-component.html#state) 对象,该对象持有您的食谱的状态,在初始化时是一个空数组([])。

接下来,在 Recipe 类中添加一个 componentDidMount 方法。 componentDidMount 方法是一种 React 生命周期方法,在组件安装后立即调用。

 1[label ~/rails_react_recipe/app/javascript/components/Recipes.jsx]
 2import React from "react";
 3import { Link } from "react-router-dom";
 4
 5class Recipes extends React.Component {
 6  constructor(props) {
 7    super(props);
 8    this.state = {
 9      recipes: []
10    };
11  }
12
13  componentDidMount() {
14      const url = "/api/v1/recipes/index";
15      fetch(url)
16        .then(response => {
17          if (response.ok) {
18            return response.json();
19          }
20          throw new Error("Network response was not ok.");
21        })
22        .then(response => this.setState({ recipes: response }))
23        .catch(() => this.props.history.push("/"));
24  }
25
26}
27export default Recipes;

在您的componentDidMount方法中,您通过 HTTP 调用了使用 Fetch API的所有食谱。如果响应成功,应用程序会将食谱的组合保存到食谱状态。

最后,在食谱类中添加一个渲染方法。 渲染方法将包含在浏览器页面上进行评估并显示的 React 元素。

 1[label ~/rails_react_recipe/app/javascript/components/Recipes.jsx]
 2import React from "react";
 3import { Link } from "react-router-dom";
 4
 5class Recipes extends React.Component {
 6  constructor(props) {
 7    super(props);
 8    this.state = {
 9      recipes: []
10    };
11  }
12
13  componentDidMount() {
14    const url = "/api/v1/recipes/index";
15    fetch(url)
16      .then(response => {
17        if (response.ok) {
18          return response.json();
19        }
20        throw new Error("Network response was not ok.");
21      })
22      .then(response => this.setState({ recipes: response }))
23      .catch(() => this.props.history.push("/"));
24  }
25  render() {
26    const { recipes } = this.state;
27    const allRecipes = recipes.map((recipe, index) => (
28      <div key={index} className="col-md-6 col-lg-4">
29        <div className="card mb-4">
30          <img
31            src={recipe.image}
32            className="card-img-top"
33            alt={`${recipe.name} image`}
34          />
35          <div className="card-body">
36            <h5 className="card-title">{recipe.name}</h5>
37            <Link to={`/recipe/${recipe.id}`} className="btn custom-button">
38              View Recipe
39            </Link>
40          </div>
41        </div>
42      </div>
43    ));
44    const noRecipe = (
45      <div className="vw-100 vh-50 d-flex align-items-center justify-content-center">
46        <h4>
47          No recipes yet. Why not <Link to="/new_recipe">create one</Link>
48        </h4>
49      </div>
50    );
51
52    return (
53      <>
54        <section className="jumbotron jumbotron-fluid text-center">
55          <div className="container py-5">
56            <h1 className="display-4">Recipes for every occasion</h1>
57            <p className="lead text-muted">
58              Weve pulled together our most popular recipes, our latest
59              additions, and our editors picks, so theres sure to be something
60              tempting for you to try.
61            </p>
62          </div>
63        </section>
64        <div className="py-5">
65          <main className="container">
66            <div className="text-right mb-3">
67              <Link to="/recipe" className="btn custom-button">
68                Create New Recipe
69              </Link>
70            </div>
71            <div className="row">
72              {recipes.length > 0 ? allRecipes : noRecipe}
73            </div>
74            <Link to="/" className="btn btn-link">
75              Home
76            </Link>
77          </main>
78        </div>
79      </>
80    );
81  }
82}
83export default Recipes;

保存并退出Recipes.jsx

现在你已经创建了一个组件来显示所有食谱,下一步是为它创建一条路线. 打开位于app/javascript/routes/Index.jsx的前端路线文件:

1nano app/javascript/routes/Index.jsx

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

 1[label ~/rails_react_recipe/app/javascript/routes/Index.jsx]
 2import React from "react";
 3import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
 4import Home from "../components/Home";
 5import Recipes from "../components/Recipes";
 6
 7export default (
 8  <Router>
 9    <Switch>
10      <Route path="/" exact component={Home} />
11      <Route path="/recipes" exact component={Recipes} />
12    </Switch>
13  </Router>
14);

保存和退出文件。

在此时刻,检查您的代码是否正常工作是很好的想法,就像你以前一样,使用以下命令启动您的服务器:

1rails s --binding=127.0.0.1

继续,并在您的浏览器中打开应用程序. 点击主页上的 ** 查看食谱**按钮,您将看到您的种子食谱的显示:

Recipes Page

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

现在你可以查看应用程序中存在的所有食谱,现在是时候创建一个第二个组件来查看单个食谱。

1nano app/javascript/components/Recipe.jsx

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

1[label ~/rails_react_recipe/app/javascript/components/Recipe.jsx]
2import React from "react";
3import { Link } from "react-router-dom";

然后创建一个食谱类,通过添加突出的代码行来扩展React.Component类:

 1[label ~/rails_react_recipe/app/javascript/components/Recipe.jsx]
 2import React from "react";
 3import { Link } from "react-router-dom";
 4
 5class Recipe extends React.Component {
 6  constructor(props) {
 7    super(props);
 8    this.state = { recipe: { ingredients: "" } };
 9
10    this.addHtmlEntities = this.addHtmlEntities.bind(this);
11  }
12}
13
14export default Recipe;

与您的食谱组件一样,在构建器中,您已初始化一个具有食谱状态的状态对象,您还将一个addHtmlEntities方法绑定到this,这样它可以在组件中访问。

为了找到一个特定的食谱,你的应用程序需要食谱的id。这意味着你的食谱组件预计一个idparam

接下来,添加一个componentDidMount方法,您将从props对象的匹配键中访问id``param。一旦您收到id,您将发出一个HTTP请求以获取配方。

 1[label ~/rails_react_recipe/app/javascript/components/Recipe.jsx]
 2import React from "react";
 3import { Link } from "react-router-dom";
 4
 5class Recipe extends React.Component {
 6  constructor(props) {
 7    super(props);
 8    this.state = { recipe: { ingredients: "" } };
 9
10    this.addHtmlEntities = this.addHtmlEntities.bind(this);
11  }
12
13  componentDidMount() {
14    const {
15      match: {
16        params: { id }
17      }
18    } = this.props;
19
20    const url = `/api/v1/show/${id}`;
21
22    fetch(url)
23      .then(response => {
24        if (response.ok) {
25          return response.json();
26        }
27        throw new Error("Network response was not ok.");
28      })
29      .then(response => this.setState({ recipe: response }))
30      .catch(() => this.props.history.push("/recipes"));
31  }
32
33}
34
35export default Recipe;

componentDidMount方法中,使用 object destructuring,您从props对象中获取id``param,然后使用Fetch API,您将提出HTTP请求,以获取拥有id的配方,并使用setState方法将其保存到组件状态。

现在添加addHtmlEntities方法,它需要一个字符串,并用其HTML实体代替所有逃开和关闭的字符串,这将有助于我们在您的食谱说明中转换任何逃脱的字符:

 1[label ~/rails_react_recipe/app/javascript/components/Recipe.jsx]
 2import React from "react";
 3import { Link } from "react-router-dom";
 4
 5class Recipe extends React.Component {
 6  constructor(props) {
 7    super(props);
 8    this.state = { recipe: { ingredients: "" } };
 9
10    this.addHtmlEntities = this.addHtmlEntities.bind(this);
11  }
12
13  componentDidMount() {
14    const {
15      match: {
16        params: { id }
17      }
18    } = this.props;
19
20    const url = `/api/v1/show/${id}`;
21
22    fetch(url)
23      .then(response => {
24        if (response.ok) {
25          return response.json();
26        }
27        throw new Error("Network response was not ok.");
28      })
29      .then(response => this.setState({ recipe: response }))
30      .catch(() => this.props.history.push("/recipes"));
31  }
32
33  addHtmlEntities(str) {
34    return String(str)
35      .replace(/&lt;/g, "<")
36      .replace(/&gt;/g, ">");
37  }
38}
39
40export default Recipe;

最后,添加一种渲染方法,从状态中获取配方并将其渲染到页面上。

 1[label ~/rails_react_recipe/app/javascript/components/Recipe.jsx]
 2import React from "react";
 3import { Link } from "react-router-dom";
 4
 5class Recipe extends React.Component {
 6  constructor(props) {
 7    super(props);
 8    this.state = { recipe: { ingredients: "" } };
 9
10    this.addHtmlEntities = this.addHtmlEntities.bind(this);
11  }
12
13  componentDidMount() {
14    const {
15      match: {
16        params: { id }
17      }
18    } = this.props;
19
20    const url = `/api/v1/show/${id}`;
21
22    fetch(url)
23      .then(response => {
24        if (response.ok) {
25          return response.json();
26        }
27        throw new Error("Network response was not ok.");
28      })
29      .then(response => this.setState({ recipe: response }))
30      .catch(() => this.props.history.push("/recipes"));
31  }
32
33  addHtmlEntities(str) {
34    return String(str)
35      .replace(/&lt;/g, "<")
36      .replace(/&gt;/g, ">");
37  }
38
39  render() {
40    const { recipe } = this.state;
41    let ingredientList = "No ingredients available";
42
43    if (recipe.ingredients.length > 0) {
44      ingredientList = recipe.ingredients
45        .split(",")
46        .map((ingredient, index) => (
47          <li key={index} className="list-group-item">
48            {ingredient}
49          </li>
50        ));
51    }
52    const recipeInstruction = this.addHtmlEntities(recipe.instruction);
53
54    return (
55      <div className="">
56        <div className="hero position-relative d-flex align-items-center justify-content-center">
57          <img
58            src={recipe.image}
59            alt={`${recipe.name} image`}
60            className="img-fluid position-absolute"
61          />
62          <div className="overlay bg-dark position-absolute" />
63          <h1 className="display-4 position-relative text-white">
64            {recipe.name}
65          </h1>
66        </div>
67        <div className="container py-5">
68          <div className="row">
69            <div className="col-sm-12 col-lg-3">
70              <ul className="list-group">
71                <h5 className="mb-2">Ingredients</h5>
72                {ingredientList}
73              </ul>
74            </div>
75            <div className="col-sm-12 col-lg-7">
76              <h5 className="mb-2">Preparation Instructions</h5>
77              <div
78                dangerouslySetInnerHTML={{
79                  __html: `${recipeInstruction}`
80                }}
81              />
82            </div>
83            <div className="col-sm-12 col-lg-2">
84              <button type="button" className="btn btn-danger">
85                Delete Recipe
86              </button>
87            </div>
88          </div>
89          <Link to="/recipes" className="btn btn-link">
90            Back to recipes
91          </Link>
92        </div>
93      </div>
94    );
95  }
96
97}
98
99export default Recipe;

在这种渲染方法中,你将你的comma分开的成分分成一个阵列,并在其上绘制,创建一个成分列表.如果没有成分,应用程序会显示一个消息,说 没有成分可用.它还将食谱图像显示为英雄图像,在食谱说明书旁边添加删除食谱按钮,并添加一个链接到食谱页面的按钮。

保存和退出文件。

要查看页面上的食谱组件,请将其添加到您的路线文件中。

1nano app/javascript/routes/Index.jsx

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

 1[label ~/rails_react_recipe/app/javascript/routes/Index.jsx]
 2import React from "react";
 3import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
 4import Home from "../components/Home";
 5import Recipes from "../components/Recipes";
 6import Recipe from "../components/Recipe";
 7
 8export default (
 9  <Router>
10    <Switch>
11      <Route path="/" exact component={Home} />
12      <Route path="/recipes" exact component={Recipes} />
13      <Route path="/recipe/:id" exact component={Recipe} />
14    </Switch>
15  </Router>
16);

在此路线文件中,您已导入您的食谱组件并为其添加一条路线,它的路线有一个:id``param,将被您想要查看的食谱的id取代。

使用rails s命令重新启动您的服务器,然后访问您的浏览器中的http://localhost:3000。 点击View Recipes**按钮导航到食谱页面。 在食谱页面上,通过点击View Recipe**按钮查看任何食谱。 您将收到来自您的数据库的数据填充的页面:

Single Recipe Page

在本节中,您已将九个食谱添加到您的数据库中,并创建了组件以单独查看这些食谱和作为一组合。

第8步:制作食谱

使用可用的食谱应用程序的下一步是创建新食谱的能力. 在此步骤中,您将创建创建食谱的组件. 此组件将包含从用户那里收集所需的食谱细节的表单,并将向食谱控制器中的创建操作提出请求,以便保存食谱数据。

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

1nano app/javascript/components/NewRecipe.jsx

在新文件中,导入您迄今为止在其他组件中使用的 React 和 Link 模块:

1[label ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx]
2import React from "react";
3import { Link } from "react-router-dom";

然后创建一个扩展React.Component类的NewRecipe类,添加以下突出的代码来创建一个扩展react.Component的React组件:

 1[label ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx]
 2import React from "react";
 3import { Link } from "react-router-dom";
 4
 5class NewRecipe extends React.Component {
 6  constructor(props) {
 7    super(props);
 8    this.state = {
 9      name: "",
10      ingredients: "",
11      instruction: ""
12    };
13
14    this.onChange = this.onChange.bind(this);
15    this.onSubmit = this.onSubmit.bind(this);
16    this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
17  }
18}
19
20export default NewRecipe;

NewRecipe组件的构造器中,你用空的名称成分说明字段初始化了状态对象,这些字段是你需要创建一个有效的配方的字段。你还有三个方法:onChange,onSubmitstripHtmlEntities,你将这些字段绑定到this。这些方法将处理更新状态,形成提交,并将特殊字符(如<)转换为它们的逃出/编码值(如&lt;),分别。

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

 1[label ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx]
 2class NewRecipe extends React.Component {
 3  constructor(props) {
 4    super(props);
 5    this.state = {
 6      name: "",
 7      ingredients: "",
 8      instruction: ""
 9    };
10
11    this.onChange = this.onChange.bind(this);
12    this.onSubmit = this.onSubmit.bind(this);
13    this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
14  }
15
16  stripHtmlEntities(str) {
17    return String(str)
18      .replace(/</g, "&lt;")
19      .replace(/>/g, "&gt;");
20  }
21
22}
23
24export default NewRecipe;

stripHtmlEntities方法中,你正在用它们的逃脱值代替<>字符,这样你就不会在你的数据库中存储原始HTML。

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

 1[label ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx]
 2class NewRecipe extends React.Component {
 3  constructor(props) {
 4    super(props);
 5    this.state = {
 6      name: "",
 7      ingredients: "",
 8      instruction: ""
 9    };
10
11    this.onChange = this.onChange.bind(this);
12    this.onSubmit = this.onSubmit.bind(this);
13    this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
14  }
15
16  stripHtmlEntities(str) {
17    return String(str)
18      .replace(/</g, "&lt;")
19      .replace(/>/g, "&gt;");
20  }
21
22  onChange(event) {
23    this.setState({ [event.target.name]: event.target.value });
24  }
25
26  onSubmit(event) {
27    event.preventDefault();
28    const url = "/api/v1/recipes/create";
29    const { name, ingredients, instruction } = this.state;
30
31    if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
32      return;
33
34    const body = {
35      name,
36      ingredients,
37      instruction: instruction.replace(/\n/g, "<br> <br>")
38    };
39
40    const token = document.querySelector('meta[name="csrf-token"]').content;
41    fetch(url, {
42      method: "POST",
43      headers: {
44        "X-CSRF-Token": token,
45        "Content-Type": "application/json"
46      },
47      body: JSON.stringify(body)
48    })
49      .then(response => {
50        if (response.ok) {
51          return response.json();
52        }
53        throw new Error("Network response was not ok.");
54      })
55      .then(response => this.props.history.push(`/recipe/${response.id}`))
56      .catch(error => console.log(error.message));
57  }
58
59}
60
61export default NewRecipe;

onChange方法中,您使用 ES6 计算属性名称将每个用户输入的值设置为其状态中的相应密钥。在onSubmit方法中,您检查了任何所需的输入都不是空的。

为了防止 Cross-Site Request Forgery (CSRF)攻击,Rails 将一个 CSRF 安全令牌附加到 HTML 文档中。 每当提出非GET请求时,这个令牌是必要的。 使用前代代码中的令牌常数,您的应用程序会在服务器上验证该令牌,并在安全令牌不符合预期的情况下抛出一个例外。 在OnSubmit方法中,该应用程序会检索 Rails 嵌入您的 HTML 文档中的 CSRF 令牌并使用 JSON 字符串进行 HTTP 请求。 如果成功创建了食谱,该应用程序会将用户重定向到新创建的食谱页

最后,添加一个渲染方法,以便用户输入用户想要创建的食谱的详细信息:

  1[label ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx]
  2class NewRecipe extends React.Component {
  3  constructor(props) {
  4    super(props);
  5    this.state = {
  6      name: "",
  7      ingredients: "",
  8      instruction: ""
  9    };
 10
 11    this.onChange = this.onChange.bind(this);
 12    this.onSubmit = this.onSubmit.bind(this);
 13    this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
 14  }
 15
 16  stripHtmlEntities(str) {
 17    return String(str)
 18      .replace(/</g, "&lt;")
 19      .replace(/>/g, "&gt;");
 20  }
 21
 22  onChange(event) {
 23    this.setState({ [event.target.name]: event.target.value });
 24  }
 25
 26  onSubmit(event) {
 27    event.preventDefault();
 28    const url = "/api/v1/recipes/create";
 29    const { name, ingredients, instruction } = this.state;
 30
 31    if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
 32      return;
 33
 34    const body = {
 35      name,
 36      ingredients,
 37      instruction: instruction.replace(/\n/g, "<br> <br>")
 38    };
 39
 40    const token = document.querySelector('meta[name="csrf-token"]').content;
 41    fetch(url, {
 42      method: "POST",
 43      headers: {
 44        "X-CSRF-Token": token,
 45        "Content-Type": "application/json"
 46      },
 47      body: JSON.stringify(body)
 48    })
 49      .then(response => {
 50        if (response.ok) {
 51          return response.json();
 52        }
 53        throw new Error("Network response was not ok.");
 54      })
 55      .then(response => this.props.history.push(`/recipe/${response.id}`))
 56      .catch(error => console.log(error.message));
 57  }
 58
 59  render() {
 60    return (
 61      <div className="container mt-5">
 62        <div className="row">
 63          <div className="col-sm-12 col-lg-6 offset-lg-3">
 64            <h1 className="font-weight-normal mb-5">
 65              Add a new recipe to our awesome recipe collection.
 66            </h1>
 67            <form onSubmit={this.onSubmit}>
 68              <div className="form-group">
 69                <label htmlFor="recipeName">Recipe name</label>
 70                <input
 71                  type="text"
 72                  name="name"
 73                  id="recipeName"
 74                  className="form-control"
 75                  required
 76                  onChange={this.onChange}
 77                />
 78              </div>
 79              <div className="form-group">
 80                <label htmlFor="recipeIngredients">Ingredients</label>
 81                <input
 82                  type="text"
 83                  name="ingredients"
 84                  id="recipeIngredients"
 85                  className="form-control"
 86                  required
 87                  onChange={this.onChange}
 88                />
 89                <small id="ingredientsHelp" className="form-text text-muted">
 90                  Separate each ingredient with a comma.
 91                </small>
 92              </div>
 93              <label htmlFor="instruction">Preparation Instructions</label>
 94              <textarea
 95                className="form-control"
 96                id="instruction"
 97                name="instruction"
 98                rows="5"
 99                required
100                onChange={this.onChange}
101              />
102              <button type="submit" className="btn custom-button mt-3">
103                Create Recipe
104              </button>
105              <Link to="/recipes" className="btn btn-link mt-3">
106                Back to recipes
107              </Link>
108            </form>
109          </div>
110        </div>
111      </div>
112    );
113  }
114
115}
116
117export default NewRecipe;

在渲染方法中,您有一个表单,其中包含三个输入字段;一个用于recipeName,recipeIngredientsinstruction。每一个输入字段都有一个onChange事件处理器,它呼叫onChange方法。

保存和退出文件。

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

1nano app/javascript/routes/Index.jsx

更新您的路线文件以包括这些突出线条:

 1[label ~/rails_react_recipe/app/javascript/routes/Index.jsx]
 2import React from "react";
 3import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
 4import Home from "../components/Home";
 5import Recipes from "../components/Recipes";
 6import Recipe from "../components/Recipe";
 7import NewRecipe from "../components/NewRecipe";
 8
 9export default (
10  <Router>
11    <Switch>
12      <Route path="/" exact component={Home} />
13      <Route path="/recipes" exact component={Recipes} />
14      <Route path="/recipe/:id" exact component={Recipe} />
15      <Route path="/recipe" exact component={NewRecipe} />
16    </Switch>
17  </Router>
18);

随着路线的到来,保存并退出您的文件。重新启动您的开发服务器,并访问您的浏览器中的http://localhost:3000。 导航到食谱页面,然后点击创建新食谱按钮。 您将找到一个页面以添加食谱到您的数据库的表格:

Create Recipe Page

输入所需的食谱细节并点击创建食谱按钮;您将在页面上看到新创建的食谱。

在此步骤中,您通过添加创建食谱的能力,为您的食谱应用带来了生命,在下一步,您将添加删除食谱的功能。

步骤 9 - 删除食谱

在本节中,您将更改您的食谱组件,以便可以删除食谱。

当您在食谱页面上点击删除按钮时,应用程序将发送请求从数据库中删除食谱。

1nano app/javascript/components/Recipe.jsx

食谱组件的构造器中,将绑定到删除食谱方法:

1[label ~/rails_react_recipe/app/javascript/components/Recipe.jsx]
2class Recipe extends React.Component {
3  constructor(props) {
4    super(props);
5    this.state = { recipe: { ingredients: "" } };
6    this.addHtmlEntities = this.addHtmlEntities.bind(this);
7    this.deleteRecipe = this.deleteRecipe.bind(this);
8  }
9...

现在,将deleteRecipe方法添加到Recipe组件中:

 1[label ~/rails_react_recipe/app/javascript/components/Recipe.jsx]
 2class Recipe extends React.Component {
 3  constructor(props) {
 4    super(props);
 5    this.state = { recipe: { ingredients: "" } };
 6
 7    this.addHtmlEntities = this.addHtmlEntities.bind(this);
 8    this.deleteRecipe = this.deleteRecipe.bind(this);
 9  }
10
11  componentDidMount() {
12    const {
13      match: {
14        params: { id }
15      }
16    } = this.props;
17    const url = `/api/v1/show/${id}`;
18    fetch(url)
19      .then(response => {
20        if (response.ok) {
21          return response.json();
22        }
23        throw new Error("Network response was not ok.");
24      })
25      .then(response => this.setState({ recipe: response }))
26      .catch(() => this.props.history.push("/recipes"));
27  }
28
29  addHtmlEntities(str) {
30    return String(str)
31      .replace(/&lt;/g, "<")
32      .replace(/&gt;/g, ">");
33  }
34
35  deleteRecipe() {
36    const {
37      match: {
38        params: { id }
39      }
40    } = this.props;
41    const url = `/api/v1/destroy/${id}`;
42    const token = document.querySelector('meta[name="csrf-token"]').content;
43
44    fetch(url, {
45      method: "DELETE",
46      headers: {
47        "X-CSRF-Token": token,
48        "Content-Type": "application/json"
49      }
50    })
51      .then(response => {
52        if (response.ok) {
53          return response.json();
54        }
55        throw new Error("Network response was not ok.");
56      })
57      .then(() => this.props.history.push("/recipes"))
58      .catch(error => console.log(error.message));
59  }
60
61  render() {
62    const { recipe } = this.state;
63    let ingredientList = "No ingredients available";
64...

删除Recipe方法中,你会得到要删除的食谱的id,然后构建你的URL并抓住CSRF代码。接下来,你会向食谱控制器发出一个DELETE请求,以删除食谱。

要在每次点击删除按钮时在删除Recipe方法中运行代码,请将其作为点击事件处理器传递给按钮。

 1[label ~/rails_react_recipe/app/javascript/components/Recipe.jsx]
 2...
 3return (
 4  <div className="">
 5    <div className="hero position-relative d-flex align-items-center justify-content-center">
 6      <img
 7        src={recipe.image}
 8        alt={`${recipe.name} image`}
 9        className="img-fluid position-absolute"
10      />
11      <div className="overlay bg-dark position-absolute" />
12      <h1 className="display-4 position-relative text-white">
13        {recipe.name}
14      </h1>
15    </div>
16    <div className="container py-5">
17      <div className="row">
18        <div className="col-sm-12 col-lg-3">
19          <ul className="list-group">
20            <h5 className="mb-2">Ingredients</h5>
21            {ingredientList}
22          </ul>
23        </div>
24        <div className="col-sm-12 col-lg-7">
25          <h5 className="mb-2">Preparation Instructions</h5>
26          <div
27            dangerouslySetInnerHTML={{
28              __html: `${recipeInstruction}`
29            }}
30          />
31        </div>
32        <div className="col-sm-12 col-lg-2">
33          <button type="button" className="btn btn-danger" onClick={this.deleteRecipe}>
34            Delete Recipe
35          </button>
36        </div>
37      </div>
38      <Link to="/recipes" className="btn btn-link">
39        Back to recipes
40      </Link>
41    </div>
42  </div>
43);
44...

在本教程的这一点上,您的完整的Recipe.jsx文件将看起来如下:

  1[label ~/rails_react_recipe/app/javascript/components/Recipe.jsx]
  2import React from "react";
  3import { Link } from "react-router-dom";
  4
  5class Recipe extends React.Component {
  6  constructor(props) {
  7    super(props);
  8    this.state = { recipe: { ingredients: "" } };
  9
 10    this.addHtmlEntities = this.addHtmlEntities.bind(this);
 11    this.deleteRecipe = this.deleteRecipe.bind(this);
 12  }
 13
 14  addHtmlEntities(str) {
 15    return String(str)
 16      .replace(/&lt;/g, "<")
 17      .replace(/&gt;/g, ">");
 18  }
 19
 20  componentDidMount() {
 21    const {
 22      match: {
 23        params: { id }
 24      }
 25    } = this.props;
 26    const url = `/api/v1/show/${id}`;
 27    fetch(url)
 28      .then(response => {
 29        if (response.ok) {
 30          return response.json();
 31        }
 32        throw new Error("Network response was not ok.");
 33      })
 34      .then(response => this.setState({ recipe: response }))
 35      .catch(() => this.props.history.push("/recipes"));
 36  }
 37
 38  deleteRecipe() {
 39    const {
 40      match: {
 41        params: { id }
 42      }
 43    } = this.props;
 44    const url = `/api/v1/destroy/${id}`;
 45    const token = document.querySelector('meta[name="csrf-token"]').content;
 46    fetch(url, {
 47      method: "DELETE",
 48      headers: {
 49        "X-CSRF-Token": token,
 50        "Content-Type": "application/json"
 51      }
 52    })
 53      .then(response => {
 54        if (response.ok) {
 55          return response.json();
 56        }
 57        throw new Error("Network response was not ok.");
 58      })
 59      .then(() => this.props.history.push("/recipes"))
 60      .catch(error => console.log(error.message));
 61  }
 62
 63  render() {
 64    const { recipe } = this.state;
 65    let ingredientList = "No ingredients available";
 66    if (recipe.ingredients.length > 0) {
 67      ingredientList = recipe.ingredients
 68        .split(",")
 69        .map((ingredient, index) => (
 70          <li key={index} className="list-group-item">
 71            {ingredient}
 72          </li>
 73        ));
 74    }
 75
 76    const recipeInstruction = this.addHtmlEntities(recipe.instruction);
 77
 78    return (
 79      <div className="">
 80        <div className="hero position-relative d-flex align-items-center justify-content-center">
 81          <img
 82            src={recipe.image}
 83            alt={`${recipe.name} image`}
 84            className="img-fluid position-absolute"
 85          />
 86          <div className="overlay bg-dark position-absolute" />
 87          <h1 className="display-4 position-relative text-white">
 88            {recipe.name}
 89          </h1>
 90        </div>
 91        <div className="container py-5">
 92          <div className="row">
 93            <div className="col-sm-12 col-lg-3">
 94              <ul className="list-group">
 95                <h5 className="mb-2">Ingredients</h5>
 96                {ingredientList}
 97              </ul>
 98            </div>
 99            <div className="col-sm-12 col-lg-7">
100              <h5 className="mb-2">Preparation Instructions</h5>
101              <div
102                dangerouslySetInnerHTML={{
103                  __html: `${recipeInstruction}`
104                }}
105              />
106            </div>
107            <div className="col-sm-12 col-lg-2">
108              <button type="button" className="btn btn-danger" onClick={this.deleteRecipe}>
109                Delete Recipe
110              </button>
111            </div>
112          </div>
113          <Link to="/recipes" className="btn btn-link">
114            Back to recipes
115          </Link>
116        </div>
117      </div>
118    );
119  }
120}
121
122export default Recipe;

保存和退出文件。

重新启动应用程序服务器并导航到主页. 点击 ** 查看食谱 ** 按钮查看所有现有食谱,查看任何单独食谱,然后点击页面上的 ** 删除食谱 ** 按钮删除文章。

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

结论

在本教程中,您创建了Ruby on Rails和React前端的食品食谱应用程序,使用PostgreSQL作为您的数据库和Bootstrap来设计。如果您想通过更多Ruby on Rails内容运行,请查看我们的(使用SSH隧道使用三层铁路应用程序中的安全通信)(https://andsky.com/tech/tutorials/securing-communications-three-tier-rails-application-using-ssh-tunnels)教程,或者转到我们的(How To Code in Ruby](https://www.digitalocean.com/community/tutorial_series/how-to-code-in-ruby)系列以更新您的Ruby技能。

Published At
Categories with 技术
comments powered by Disqus