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

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

介绍

Ruby on Rails是一个流行的服务器侧 Web 应用程序框架,它支持当今网络上存在的许多流行的应用程序,如 GitHub, Basecamp, SoundCloud, Airbnb,和 Twitch

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

随着网络的前端向服务器侧代码分离的框架移动,将 Rails 的优雅性与 React 的效率相结合,将使您能够根据当前趋势创建强大而现代化的应用程序。

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

Screencapture of the completed recipe app home page

前提条件

要遵循这个教程,你需要:

  • 安装在您的开发机上的 [Node.js] (https://nodejs.org/en/)和 [npm] (https://www.npmjs.com/). 这个教程使用Node.js版本16.14.0和npm版本8.3.1. Node.js是一个JavaScript运行时环境,允许您在浏览器外运行您的代码. 它带有一个预安装的软件包管理器,名为 [npm] (https://www.npmjs.com/),它允许您安装并更新软件包. 要在 Ubuntu 20.04 或 macOS 上安装这些设备,请遵循 [如何在 Ubuntu 20.04 上安装节点.js] (https://andsky.com/tech/tutorials/how-to-install-node-js-on-ubuntu-20-04#option-3-installing-node-using-the-node-version-manager) 或 [如何在 macOS 上安装节点.js并创建一个本地开发环境] (https://andsky.com/tech/tutorials/how-to-install-node-js-and-create-a-local-development-environment-on-macos) 的"综合使用 PPA" 部分.
  • Yarn 软件包管理器安装在您的开发机器上,可以下载回放框架. 此教程在1.22. 10版本上进行了测试;为了安装此依赖性,遵循[官方Yarn安装指 (https://yarnpkg.com/en/docs/install#debian-stable). *已安装铁路上的Ruby. 为了获得这一点,请遵循我们的指南如何在铁路上安装Ruby,在Ubuntu 20.04上安装rbenv(https://andsky.com/tech/tutorials/how-to-install-ruby-on-rails-with-rbenv-on-ubuntu-20-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的3.1.2版本和Rails的7.0.4版本上进行了测试,因此在安装过程中确保指定这些版本. (英语)

<$>[注] 注: Rails 版本 7 不兼容反向. 如果您正在使用 Rails 版本 5,请访问 如何在 Ubuntu 18.04 上使用 React Frontend 设置一个 Ruby on Rails v5 项目的教程

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

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

Rails 提供几种名为 generator 的脚本,创建构建现代Web应用程序所需的一切。 要查看这些命令的完整列表以及它们所做的,请在终端中运行以下命令:

1rails -h

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

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

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

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

*「-d」旗指明了偏好的資料庫引擎,在這種情況下是PostgreSQL。 *「-j」旗指明了應用程式的JavaScript方法。 Rails提供了在Rails應用程式中處理JavaScript代碼的幾種不同的方法。「esbuild」選項轉移到「-j」旗指示Rails以預先設定 esbuild為偏好JavaScript包裝器。 *「-c」旗指示應用程式的CSS處理器。 Bootstrap是此情況中偏好選項。 *「-T」旗指示Rails跳過測試檔案的生成,因為你不會為本教程寫測試。

一旦命令完成,转到rails_react_recipe目录,该目录是您的应用程序的根目录:

1cd rails_react_recipe

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

1ls

内容将打印类似于此:

1[secondary_label Output]
2Gemfile README.md bin db node_modules storage yarn.lock
3Gemfile.lock Rakefile config lib package.json tmp
4Procfile.dev app config.ru log public vendor

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

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

步骤二:建立数据库

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

在config/database.yml中发现的 database.yml 文件包含数据库细节,如不同开发环境的数据库名称。Rails 通过附加一个 underscore (_) 然后是环境名称来指定各种开发环境的数据库名称. 在本教程中,您将使用默认数据库配置值,但如果需要,您可以更改配置值。

<$>[注] 注: 在此时刻,您可以更改 config/database.yml 以设置您希望 Rails 使用哪些 PostgreSQL 角色来创建您的数据库。 在前提条件下,您在 如何使用 PostgreSQL 与您的 Ruby on Rails 应用程序 教程中创建了由密码保护的角色。 如果您尚未设置用户,您现在可以在同一前提教程中遵循 步骤 4 — 配置和创建您的数据库 的说明。 <$>

Rails 提供许多让开发 Web 应用程序变得容易的命令,包括使用创建放下重置等数据库的工作命令。

1rails db:create

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

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

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

1bin/dev

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

一旦您运行此命令,您的命令提示将消失,然后在其位置打印以下输出:

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

要访问您的应用程序,打开一个浏览器窗口,并导航到 http://localhost:3000. Rails 默认欢迎页面将加载,这意味着您已正确设置 Rails 应用程序:

Screencapture of the Rails welcome page

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

 1[secondary_label Output]
 2^C SIGINT received, starting shutdown
 3- Gracefully stopping, waiting for requests to finish
 4=== puma shutdown: 2019-07-31 14:21:24 -0400 ===
 5- Goodbye!
 6Exiting
 7sending SIGTERM to all processes
 8terminated by SIGINT
 9terminated by SIGINT
10exited with code 0

然后你的终端快递会再次出现。

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

步骤3 - 安装前端依赖

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

  • React用于构建用户界面。
  • React DOM用于允许React与浏览器DOM进行交互。

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

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

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

1nano package.json

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

 1[label ~/rails_react_recipe/package.json]
 2{
 3  "name": "app",
 4  "private": "true",
 5  "dependencies": {
 6    "@hotwired/stimulus": "^3.1.0",
 7    "@hotwired/turbo-rails": "^7.1.3",
 8    "@popperjs/core": "^2.11.6",
 9    "bootstrap": "^5.2.1",
10    "bootstrap-icons": "^1.9.1",
11    "esbuild": "^0.15.7",
12    "react": "^18.2.0",
13    "react-dom": "^18.2.0",
14    "react-router-dom": "^6.3.0",
15    "sass": "^1.54.9"
16  },
17  "scripts": {
18    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets",
19    "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
20  }
21}

关闭文件,按CTRL+X

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

步骤四:创建主页

安装了所需的依赖性,您现在将创建一个应用程序的主页,当用户第一次访问应用程序时作为目标页面。

Rails 遵循应用程序的 Model-View-Controller]架构模式。在 MVC 模式中,控制器的目的是接收特定请求,并将其传递到适当的模型或视图中。 应用程序目前在浏览器中加载根 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

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

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

  • 用于添加与主页控制器相关的辅助方法的homepage_helper.rb文件.
  • 作为视图页的index.html.erb文件,以显示与主页相关的任何内容。

除了通过运行 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.erbindex.html.erb文件中的任何东西。

保存并关闭文件。

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

1bin/dev

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

The "Homepage#index" Application page will load

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

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

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

删除文件内部的代码,然后将文件保存为空. 这样,您可以确保 index.html.erb 的内容不会干扰您的前端的 React 渲染。

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

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

在此步骤中,您将配置 Rails 以在应用程序的前端使用 React,而不是模板引擎。

借助在生成 Rails 应用程序时指定的esbuild选项,允许 JavaScript 与 Rails 无缝工作所需的大部分设置已经完成了。 剩下的只是将 React 应用程序的入口点加载到 JavaScript 文件的esbuild入口点。

1mkdir ~/rails_react_recipe/app/javascript/components

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

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

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

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

1[label ~/rails_react_recipe/app/javascript/application.js]
2// Entry point for the build script in your package.json
3import "@hotwired/turbo-rails"
4import "./controllers"
5import * as bootstrap from "bootstrap"
6import "./components"

添加到application.js文件的代码行将导入index.jsx文件中的代码,使其可用于esbuild组合。 随着/components目录导入到 Rails 应用程序的 JavaScript 入口点,您可以为您的主页创建一个 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 和链接组件,而链接组件会创建一个超链接,以便您可以从一个页面导航到另一个页面。

保存并关闭文件。

使用您的主页组件集,您现在将使用 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, Routes, Route } from "react-router-dom";
 4import Home from "../components/Home";
 5
 6export default (
 7  <Router>
 8    <Routes>
 9      <Route path="/" element={<Home />} />
10    </Routes>
11  </Router>
12);

在这个index.jsx路由文件中,您导入以下模块:允许您使用 React 的React模块,以及 React Router 的BrowserRouter,RoutesRoute模块,这些模块可以帮助您从一个路由导航到另一个路由。最后,您导入您的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";
4
5export default props => <>{Routes}</>;

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

保存并关闭文件。

现在你已经设置了App.jsx,你可以在输入文件中渲染它,在组件目录中创建一个index.jsx文件:

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

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

 1[label ~/rails_react_recipe/app/javascript/components/index.jsx]
 2import React from "react";
 3import { createRoot } from "react-dom/client";
 4import App from "./App";
 5
 6document.addEventListener("turbo:load", () => {
 7  const root = createRoot(
 8    document.body.appendChild(document.createElement("div"))
 9  );
10  root.render(<App />);
11});

导入行中,您将导入 React 库、ReactDOM 的CreateRoot函数和您的应用组件。使用 ReactDOM 的CreateRoot函数,您将创建一个根元素作为附加到页面的div元素,并将您的App组件转换到页面中。

保存和退出文件。

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

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

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

接下来,用下面的代码替application.bootstrap.scss文件的内容:

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

您为页面设置了一些自定义颜色。.hero 部分将创建一个 _hero 图像的框架,或在您的网站的前面的大型网页标志,您将稍后添加。

有了你的CSS风格,保存和退出文件。

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

1bin/dev

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

The homepage with its new styling

点击CTRL+C关闭 Web 服务器。

在此步骤中,您将创建模型和控制器,允许您创建,阅读,更新和删除配方。

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

现在您已经为您的应用程序设置了 React 前端,您将创建一个食谱模型和控制器。食谱模型将代表包含用户食谱信息的数据库表,而控制器将接收和处理创建、阅读、更新或删除食谱的请求。当用户请求食谱时,食谱控制器接收此请求并将其传输到食谱模型,该模型从数据库中获取所需的数据。

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

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

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

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

1[secondary_label Output]
2      invoke active_record
3      create db/migrate/20221017220817_create_recipes.rb
4      create app/models/recipe.rb

创建的两个文件是:

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

接下来,您将编辑配方模型文件,以确保只有有效的数据存储到数据库中。

打开位于app/models/recipe.rb的食谱模型:

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

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

1[label ~/rails_react_recipe/app/models/recipe.rb]
2class Recipe < ApplicationRecord
3  validates :name, presence: true
4  validates :ingredients, presence: true
5  validates :instruction, presence: true
6end

在此代码中,您添加了模型验证,该验证检查名称成分说明字段,如果没有这三个字段,食谱将无效,不会保存到数据库中。

保存并关闭文件。

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

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

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

添加突出的材料,以便您的文件匹配如下:

 1[label db/migrate/20221017220817_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
10      t.timestamps
11    end
12  end
13end

此迁移文件包含一个 Ruby 类,具有更改方法和命令,用于创建一个名为食谱的表,以及列及其数据类型。您还会通过添加null: false来更新20221017220817_create_recipes.rbNOT NULL限制名称,成分说明列,以确保这些列在更改数据库之前具有值。最后,您会为您的图像列添加默认的图像 URL;如果您想要使用不同的图像,这可能是另一个 URL。

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

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  --skip-template-engine --no-helper

在这个命令中,你在一个api/v1目录中创建一个食谱控制器,其中包含一个索引,创建,显示摧毁操作。

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

  • --skip-template-engine,指示 Rails 跳过生成 Rails 视图文件,因为 React 处理您的前端需求。

运行该命令还会更新您的路由文件,并为食谱控制器中的每个操作提供路由。

当命令运行时,它将打印这样的输出:

 1[secondary_label Output]
 2      create app/controllers/api/v1/recipes_controller.rb
 3       route namespace :api do
 4                namespace :v1 do
 5                  get 'recipes/index'
 6                  get 'recipes/create'
 7                  get 'recipes/show'
 8                  get 'recipes/destroy'
 9                end
10              end

要使用这些路径,您将对您的 config/routes.rb 文件进行更改. 在文本编辑器中打开 routes.rb 文件:

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  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
14
15  # Defines the root path route ("/")
16  # root "articles#index"
17end

在这个路由文件中,你修改了创建破坏路径的HTTP口号,以便它可以发布删除数据.你还可以通过向路径添加一个:id参数来修改显示破坏操作的路径。

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

保存和退出文件。

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

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  before_action :set_recipe, only: %i[show destroy]
 4
 5  def index
 6    recipe = Recipe.all.order(created_at: :desc)
 7    render json: recipe
 8  end
 9
10  def create
11    recipe = Recipe.create!(recipe_params)
12    if recipe
13      render json: recipe
14    else
15      render json: recipe.errors
16    end
17  end
18
19  def show
20    render json: @recipe
21  end
22
23  def destroy
24    @recipe&.destroy
25    render json: { message: 'Recipe deleted!' }
26  end
27
28  private
29
30  def recipe_params
31    params.permit(:name, :image, :ingredients, :instruction)
32  end
33
34  def set_recipe
35    @recipe = Recipe.find(params[:id])
36  end
37end

在新代码行中,您只在显示删除操作匹配请求时创建一个私人set_recipe方法,称为before_actionset_recipe方法使用ActiveRecord的find方法来找到一种id匹配idparams中提供的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,useState,useEffect,LinkuseNavigate模块:

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

接下来,添加突出的行来创建和导出一个名为食谱的功能 React 组件:

 1[label ~/rails_react_recipe/app/javascript/components/Recipes.jsx]
 2import React, { useState, useEffect } from "react";
 3import { Link, useNavigate } from "react-router-dom";
 4
 5const Recipes = () => {
 6  const navigate = useNavigate();
 7  const [recipes, setRecipes] = useState([]);
 8};
 9
10export default Recipes;

食谱组件中,React Router 的导航 API 会调用 useNavigate的链接,而 React 的 useState的链接会初始化食谱状态,这是一个空数组([]),以及一个更新食谱状态的setRecipes`函数。

接下来,在一个 useEffect 链接中,您将发出一个 HTTP 请求,以获取所有食谱。

 1[label ~/rails_react_recipe/app/javascript/components/Recipes.jsx]
 2import React, { useState, useEffect } from "react";
 3import { Link, useNavigate } from "react-router-dom";
 4
 5const Recipes = () => {
 6  const navigate = useNavigate();
 7  const [recipes, setRecipes] = useState([]);
 8
 9  useEffect(() => {
10    const url = "/api/v1/recipes/index";
11    fetch(url)
12      .then((res) => {
13        if (res.ok) {
14          return res.json();
15        }
16        throw new Error("Network response was not ok.");
17      })
18      .then((res) => setRecipes(res))
19      .catch(() => navigate("/"));
20  }, []);
21};
22
23export default Recipes;

在您的useEffect链接中,您会发出HTTP呼叫以使用Fetch API(https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)获取所有食谱。如果响应成功,应用程序会将食谱的组合保存到食谱状态。

最后,返回将被评估和显示在浏览器页面的元素标记,当元素被渲染时。在这种情况下,元素将渲染从食谱状态的食谱卡。

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

保存和退出文件。

在此时,检查您的代码是否按预期工作是很好的想法. 如前所述,请使用以下命令启动您的服务器:

1bin/dev

然后在浏览器中打开应用程序. 在主页上按下 ** 查看食谱 ** 按钮,以访问您的种子食谱的显示页面:

Screencapture with the seed recipes page

使用您的终端中的CTRL+C来停止服务器并返回您的提示。

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

1nano app/javascript/components/Recipe.jsx

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

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

接下来,添加突出的行来创建和导出一个名为食谱的功能 React 组件:

 1[label ~/rails_react_recipe/app/javascript/components/Recipe.jsx]
 2import React, { useState, useEffect } from "react";
 3import { Link, useNavigate, useParams } from "react-router-dom";
 4
 5const Recipe = () => {
 6  const params = useParams();
 7  const navigate = useNavigate();
 8  const [recipe, setRecipe] = useState({ ingredients: "" });
 9};
10
11export default Recipe;

食谱组件一样,您将 React Router 导航以useNavigate链接进行初始化。一个食谱状态和一个setRecipe函数将用useState链接更新该状态。

要找到特定食谱,您的应用程序需要知道食谱的id,这意味着您的食谱组件预计URL中的id``param

接下来,声明一个useEffect链接,您将从params对象访问id``param。一旦您获得了id配方,您将发出一个HTTP请求来获取配方。

 1[label ~/rails_react_recipe/app/javascript/components/Recipe.jsx]
 2import React, { useState, useEffect } from "react";
 3import { Link, useNavigate, useParams } from "react-router-dom";
 4
 5const Recipe = () => {
 6  const params = useParams();
 7  const navigate = useNavigate();
 8  const [recipe, setRecipe] = useState({ ingredients: "" });
 9
10  useEffect(() => {
11    const url = `/api/v1/show/${params.id}`;
12    fetch(url)
13      .then((response) => {
14        if (response.ok) {
15          return response.json();
16        }
17        throw new Error("Network response was not ok.");
18      })
19      .then((response) => setRecipe(response))
20      .catch(() => navigate("/recipes"));
21  }, [params.id]);
22};
23
24export default Recipe;

useEffect链接中,您使用params.id值进行 GET HTTP 请求,以获取拥有id的食谱,然后使用setRecipe函数将其保存到组件状态。

接下来,添加一个addHtmlEntities函数,该函数将用于在组件中代替字符实体的 HTML实体。 该addHtmlEntities函数将采取一个字符串,并用其HTML实体代替所有逃脱的打开和关闭列。

 1[label ~/rails_react_recipe/app/javascript/components/Recipe.jsx]
 2import React, { useState, useEffect } from "react";
 3import { Link, useNavigate, useParams } from "react-router-dom";
 4
 5const Recipe = () => {
 6  const params = useParams();
 7  const navigate = useNavigate();
 8  const [recipe, setRecipe] = useState({ ingredients: "" });
 9
10  useEffect(() => {
11    const url = `/api/v1/show/${params.id}`;
12    fetch(url)
13      .then((response) => {
14        if (response.ok) {
15          return response.json();
16        }
17        throw new Error("Network response was not ok.");
18      })
19      .then((response) => setRecipe(response))
20      .catch(() => navigate("/recipes"));
21  }, [params.id]);
22
23  const addHtmlEntities = (str) => {
24    return String(str).replace(/&lt;/g, "<").replace(/&gt;/g, ">");
25  };
26};
27
28export default Recipe;

最后,返回标记以将食谱放到页面上的组件状态,添加突出的行:

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

使用成分列表函数,你可以将分开的食谱成分分成一个数组,并将其绘制成一个成分列表。如果没有食谱,应用程序会显示一个消息,说没有食谱可用**。你还可以通过添加HtmlEntities函数来代替食谱指令中的所有打开和关闭支架。最后,代码将食谱图像显示为英雄图像,在食谱指令旁边添加一个删除食谱按钮,并添加一个链接到食谱页面的按钮。

<$>[注] 注: 使用 React 的 dangerouslySetInnerHTML 属性是危险的,因为它会将您的应用暴露在 跨站点脚本攻击中。

保存和退出文件。

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

1nano app/javascript/routes/index.jsx

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

 1[label ~/rails_react_recipe/app/javascript/routes/index.jsx]
 2import React from "react";
 3import { BrowserRouter as Router, Routes, Route } 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    <Routes>
11      <Route path="/" exact component={Home} />
12      <Route path="/recipes" exact component={Recipes} />
13      <Route path="/recipe/:id" element={<Recipe />} />
14    </Routes>
15  </Router>
16);

你将你的食谱组件导入这个路线文件并添加一条路线,它的路线有一个:id``param,将被你想要查看的食谱的id取代。

保存并关闭文件。

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

Single Recipe Page

您可以用CTRL+C阻止服务器。

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

第8步:制作食谱

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

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

1nano app/javascript/components/NewRecipe.jsx

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

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

接下来,通过添加突出的行来创建和导出功能的NewRecipe组件:

 1[label ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx]
 2import React, { useState } from "react";
 3import { Link, useNavigate } from "react-router-dom";
 4
 5const NewRecipe = () => {
 6  const navigate = useNavigate();
 7  const [name, setName] = useState("");
 8  const [ingredients, setIngredients] = useState("");
 9  const [instruction, setInstruction] = useState("");
10};
11
12export default NewRecipe;

与之前的组件一样,您将React路由器导航初始化为useNavigate链接,然后使用useState链接初始化为名称,成分指令状态,每一个都具有相应的更新功能。

接下来,创建一个‘stripHtmlEntities’函数,将特殊字符(如‘<)转换为它们的逃避/编码值(如‘&lt;)。

 1[label ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx]
 2import React, { useState } from "react";
 3import { Link, useNavigate } from "react-router-dom";
 4
 5const NewRecipe = () => {
 6  const navigate = useNavigate();
 7  const [name, setName] = useState("");
 8  const [ingredients, setIngredients] = useState("");
 9  const [instruction, setInstruction] = useState("");
10
11  const stripHtmlEntities = (str) => {
12    return String(str)
13      .replace(/\n/g, "<br> <br>")
14      .replace(/</g, "&lt;")
15      .replace(/>/g, "&gt;");
16  };
17};
18
19export default NewRecipe;

stripHtmlEntities函数中,你将<>字符替换为它们的错误值,这样你就不会在数据库中存储原始HTML。

接下来,添加突出的行,将onChangeonSubmit函数添加到NewRecipe组件中,以处理表单的编辑和提交:

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

onChange函数接受用户输入事件和状态设置函数,然后用用户输入值更新状态。在onSubmit函数中,您检查任何所需的输入都不是空的。您然后构建一个包含创建新食谱所需参数的对象。使用stripHtmlEntities函数,您将食谱指令中的<>字符替换为其错过的值,并用破解标签替换每个新字符行,从而保留用户输入的文本格式。

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

随着路线的位置,保存和退出您的文件。

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

Create Recipe Page

输入所需的食谱细节,然后点击创建食谱按钮。新创建的食谱将出现在页面上。

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

步骤 9 - 删除食谱

在本节中,您将更改您的食谱组件,以包括删除食谱的选项. 当您点击食谱页面上的删除按钮时,应用程序将发送请求从数据库中删除食谱。

首先,打开您的Recipe.jsx文件来编辑:

1nano app/javascript/components/Recipe.jsx

食谱组件中,添加一个删除食谱函数,并列出突出的行:

 1[label ~/rails_react_recipe/app/javascript/components/Recipe.jsx]
 2import React, { useState, useEffect } from "react";
 3import { Link, useNavigate, useParams } from "react-router-dom";
 4
 5const Recipe = () => {
 6  const params = useParams();
 7  const navigate = useNavigate();
 8  const [recipe, setRecipe] = useState({ ingredients: "" });
 9
10  useEffect(() => {
11    const url = `/api/v1/show/${params.id}`;
12    fetch(url)
13      .then((response) => {
14        if (response.ok) {
15          return response.json();
16        }
17        throw new Error("Network response was not ok.");
18      })
19      .then((response) => setRecipe(response))
20      .catch(() => navigate("/recipes"));
21  }, [params.id]);
22
23  const addHtmlEntities = (str) => {
24    return String(str).replace(/&lt;/g, "<").replace(/&gt;/g, ">");
25  };
26
27  const deleteRecipe = () => {
28    const url = `/api/v1/destroy/${params.id}`;
29    const token = document.querySelector('meta[name="csrf-token"]').content;
30
31    fetch(url, {
32      method: "DELETE",
33      headers: {
34        "X-CSRF-Token": token,
35        "Content-Type": "application/json",
36      },
37    })
38      .then((response) => {
39        if (response.ok) {
40          return response.json();
41        }
42        throw new Error("Network response was not ok.");
43      })
44      .then(() => navigate("/recipes"))
45      .catch((error) => console.log(error.message));
46  };
47
48  const ingredientList = () => {
49    let ingredientList = "No ingredients available";
50
51    if (recipe.ingredients.length > 0) {
52      ingredientList = recipe.ingredients
53        .split(",")
54        .map((ingredient, index) => (
55          <li key={index} className="list-group-item">
56            {ingredient}
57          </li>
58        ));
59    }
60
61    return ingredientList;
62  };
63
64  const recipeInstruction = addHtmlEntities(recipe.instruction);
65
66  return (
67    <div className="">
68...

删除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
34              type="button"
35              className="btn btn-danger"
36              onClick={deleteRecipe}
37            >
38              Delete Recipe
39            </button>
40          </div>
41        </div>
42        <Link to="/recipes" className="btn btn-link">
43          Back to recipes
44        </Link>
45      </div>
46    </div>
47  );
48...

在本教程的这一点上,你的完整的Recipe.jsx文件应该匹配这个文件:

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

保存和退出文件。

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

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

结论

在本教程中,您创建了Ruby on Rails和React前端的食品配方应用程序,使用PostgreSQL作为您的数据库和Bootstrap来设计。如果您想继续使用Ruby on Rails进行构建,请考虑遵循我们的(使用SSH隧道使用三层铁路应用程序中的安全通信)教程,或者访问我们的(How To Code in Ruby)(LINK1)系列以更新您的Ruby技能。

Published At
Categories with 技术
comments powered by Disqus