作者选择了电子前沿基金会接受捐赠作为为捐款而写程序。
红宝石 on Rails是一个流行的服务器端 Web 应用程序框架。它为当今网络上存在的许多流行应用程序提供支持,例如GitHub, Basecamp, 声云, Airbnb, and Twitch。 Ruby on Rails 强调程序员经验以及围绕它构建的热情社区,将为您提供构建和维护现代 Web 应用程序所需的工具。
React是一个用于创建前端用户界面的 JavaScript 库。在 Facebook 的支持下,它是当今网络上最流行的前端库之一。 React 提供了类似的功能虚拟文档对象模型 (DOM), 组件架构, and 状态管理,使得前端开发的过程更加有组织、高效。
随着 Web 前端向与服务器端代码分离的框架发展,将 Rails 的优雅与 React 的高效相结合将使您能够根据当前趋势构建强大且现代的应用程序。通过使用 React 从 Rails 视图(而不是 Rails 模板引擎)中渲染组件,您的应用程序将受益于 JavaScript 和前端开发的最新进展,同时利用 Ruby on Rails 的表现力。
在本教程中,您将创建一个 Ruby on Rails 应用程序,用于存储您最喜欢的食谱,然后使用 React 前端显示它们。完成后,您将能够使用 React 界面创建、查看和删除食谱,样式为引导程序:
要学习本教程,您需要:
-
Node.js and npm安装在您的开发机器上。本教程使用 Node.js 版本 16.14.0 和 npm 版本 8.3.1。 Node.js 是一个 JavaScript 运行时环境,允许您在浏览器之外运行代码。它带有一个预安装的包管理器,称为npm,它允许您安装和更新软件包。要在 Ubuntu 20.04 或 macOS 上安装这些软件,请按照“使用 PPA 安装”部分进行操作如何在 Ubuntu 20.04 上安装 Node.js或中的步骤如何在 macOS 上安装 Node.js 并创建本地开发环境.
-
Yarn 包管理器安装在您的开发计算机上,它允许您下载 React 框架。本教程在1.22.10版本上测试;要安装此依赖项,请按照官方 Yarn 安装指南.
-
安装了 Ruby on Rails。要获得此信息,请按照我们的指南进行操作如何在 Ubuntu 20.04 上使用 rbenv 安装 Ruby on Rails。如果您想在 macOS 上开发此应用程序,您可以使用如何在 macOS 上使用 rbenv 安装 Ruby on Rails。本教程在 Ruby 版本 3.1.2 和 Rails 版本 7.0.4 上进行了测试,因此请确保在安装过程中指定这些版本。
Note:Rails 版本 7 不向后兼容。如果您使用的是 Rails 版本 5,请访问教程如何在 Ubuntu 18.04 上使用 React 前端设置 Ruby on Rails v5 项目.
- 已安装 PostgreSQL,如步骤 1 和 2 中所述如何在 Ubuntu 20.04 上将 PostgreSQL 与 Ruby on Rails 应用程序结合使用 or 如何在 macOS 上将 PostgreSQL 与 Ruby on Rails 应用程序结合使用。要学习本教程,您可以使用 PostgreSQL 版本 12 或更高版本。如果您想在不同的 Linux 发行版或其他操作系统上开发此应用程序,请参阅官方 PostgreSQL 下载页面。有关如何使用 PostgreSQL 的更多信息,请参阅如何安装和使用 PostgreSQL.
在此步骤中,您将在 Rails 应用程序框架上构建菜谱应用程序。首先,您将创建一个新的 Rails 应用程序,将其设置为与 React 一起使用。
Rails 提供了几个脚本,称为发电机它创建了构建现代 Web 应用程序所需的一切。要查看这些命令及其用途的完整列表,请在终端中运行以下命令:
该命令将产生一个完整的选项列表,允许您设置应用程序的参数。列出的命令之一是new
命令,该命令创建一个新的 Rails 应用程序。
现在,您将使用以下命令创建一个新的 Rails 应用程序new
发电机。在终端中运行以下命令:
- rails new rails_react_recipe -d postgresql -j esbuild -c bootstrap -T
The preceding command creates a new Rails application in a directory named rails_react_recipe
, installs the required Ruby and JavaScript dependencies, and configures Webpack. The flags associated with this new
generator command include the following:
- The
-d
flag 指定首选数据库引擎,在本例中为 PostgreSQL。
- The
-j
flag 指定应用程序的 JavaScript 方法。 Rails 提供了几种不同的方法来处理 Rails 应用程序中的 Javascript 代码。这esbuild
选项传递给-j
flag 指示 Rails 进行预配置esbuild作为首选的 JavaScript 捆绑器。
- The
-c
flag 指定应用程序的 CSS 处理器。在这种情况下,Bootstrap 是首选。
- The
-T
flag 指示 Rails 跳过测试文件的生成,因为您不会为本教程编写测试。如果您想使用与 Rails 提供的工具不同的 Ruby 测试工具,也建议使用此命令。
Once the command has finished, move to the rails_react_recipe
directory, which is the root directory of your app:
接下来,列出该目录的内容:
打印的内容与此类似:
Output
Gemfile README.md bin db node_modules storage yarn.lock
Gemfile.lock Rakefile config lib package.json tmp
Procfile.dev app config.ru log public vendor
该根目录有几个自动生成的文件和文件夹,它们构成了 Rails 应用程序的结构,其中包括package.json
包含 React 应用程序依赖项的文件。
现在您已经成功创建了一个新的 Rails 应用程序,您将在下一步中将其连接到数据库。
在运行新的 Rails 应用程序之前,必须首先将其连接到数据库。在此步骤中,您将新创建的 Rails 应用程序连接到 PostgreSQL 数据库,以便可以根据需要存储和获取配方数据。
The database.yml
文件发现于config/database.yml
包含数据库详细信息,例如不同开发环境的数据库名称。 Rails 通过附加下划线指定各种开发环境的数据库名称 (_
) 后跟环境名称。在本教程中,您将使用默认的数据库配置值,但如果需要,您可以更改配置值。
Note:此时,您可以更改config/database.yml
设置您希望 Rails 使用哪个 PostgreSQL 角色来创建数据库。在先决条件期间,您创建了一个由密码保护的角色如何将 PostgreSQL 与 Ruby on Rails 应用程序结合使用教程。如果您尚未设置用户,现在可以按照以下说明进行操作第 4 步 — 配置和创建数据库在相同的先决条件教程中。
Rails 提供了许多命令,使开发 Web 应用程序变得容易,包括使用数据库的命令,例如create
, drop
, and reset
。要为您的应用程序创建数据库,请在终端中运行以下命令:
该命令创建一个development
and test
数据库,产生以下输出:
Output
Created database 'rails_react_recipe_development'
Created database 'rails_react_recipe_test'
现在应用程序已连接到数据库,请通过运行以下命令启动应用程序:
Rails 提供了一种替代方案bin/dev
通过执行以下命令来启动 Rails 应用程序的脚本Procfile.dev
使用以下命令在应用程序的根目录中创建文件Foreman gem.
运行此命令后,命令提示符将消失,并且将在其位置打印以下输出:
Output
started with pid 70099
started with pid 70100
started with pid 70101
yarn run v1.22.10
yarn run v1.22.10
$ esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets --watch
$ sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules --watch
=> Booting Puma
=> Rails 7.0.4 application starting in development
=> Run `bin/rails server --help` for more startup options
[watch] build finished, watching for changes...
Puma starting in single mode...
* Puma version: 5.6.5 (ruby 3.1.2-p20) ("Birdie's Version")
* Min threads: 5
* Max threads: 5
* Environment: development
* PID: 70099
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
Sass is watching for changes. Press Ctrl-C to stop.
要访问您的应用程序,请打开浏览器窗口并导航到http://localhost:3000
。 Rails 默认欢迎页面将会加载,这意味着您已经正确设置了 Rails 应用程序:
要停止 Web 服务器,请按CTRL+C
在服务器运行的终端中。您将收到来自 Puma 的告别消息:
Output
^C SIGINT received, starting shutdown
- Gracefully stopping, waiting for requests to finish
=== puma shutdown: 2019-07-31 14:21:24 -0400 ===
- Goodbye!
Exiting
sending SIGTERM to all processes
terminated by SIGINT
terminated by SIGINT
exited with code 0
然后您的终端提示将重新出现。
您已成功为您的食品配方应用程序设置了数据库。在下一步中,您将安装组装 React 前端所需的 JavaScript 依赖项。
在此步骤中,您将安装食品食谱应用程序前端所需的 JavaScript 依赖项。他们包括:
-
React用于构建用户界面。
-
反应 DOM使 React 能够与浏览器 DOM 交互。
-
反应路由器用于处理 React 应用程序中的导航。
运行以下命令以使用 Yarn 包管理器安装这些包:
-
yarn add反应反应-dom反应-路由器-dom
该命令使用 Yarn 安装指定的包并将它们添加到package.json
文件。要验证这一点,请打开package.json
文件位于项目根目录:
已安装的软件包将列在dependencies
key:
〜/rails_react_recipe/package.json
{
"name": "app",
"private": "true",
"dependencies": {
"@hotwired/stimulus": "^3.1.0",
"@hotwired/turbo-rails": "^7.1.3",
"@popperjs/core": "^2.11.6",
"bootstrap": "^5.2.1",
"bootstrap-icons": "^1.9.1",
"esbuild": "^0.15.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
"sass": "^1.54.9"
},
"scripts": {
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets",
"build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
}
}
按关闭文件CTRL+X
.
您已经为您的应用程序安装了一些前端依赖项。接下来,您将为食品食谱应用程序设置主页。
安装所需的依赖项后,您现在将为应用程序创建一个主页,以作为用户首次访问应用程序时的登录页面。
Rails 遵循模型-视图-控制器应用程序的架构模式。在 MVC 模式中,控制器的目的是接收特定请求并将它们传递到适当的模型或视图。当前,当浏览器中加载根 URL 时,应用程序会显示 Rails 欢迎页面。要更改此设置,您将为主页创建一个控制器和视图,然后将其与路线匹配。
Rails 提供了一个controller
用于创建控制器的生成器。这controller
生成器接收控制器名称和匹配的操作。有关这方面的更多信息,您可以查看Rails 文档.
This tutorial will call the controller Homepage
. Run the following command to create a Homepage
controller with an index
action:
- rails g controller Homepage index
Note:在 Linux 上,错误FATAL: Listen error: unable to monitor directories for changes.
可能是由于系统对您的计算机可以监视更改的文件数量进行了限制。运行以下命令来修复它:
-
echo fs.inotify.max_user_watches=524288 | sudo tee -a/etc/sysctl.conf&& sudo sysctl -p
此命令将永久增加您可以监视的目录数量Listen
to 524288
。您可以通过运行相同的命令并替换来再次更改此设置524288
与您想要的号码。
运行controller
命令生成以下文件:
- A
homepage_controller.rb
用于接收所有与主页相关的请求的文件。该文件包含index
您在命令中指定的操作。
- A
homepage_helper.rb
file for adding helper methods related to the Homepage
controller.
- An
index.html.erb
文件作为视图页面,用于呈现与主页相关的任何内容。
除了通过运行 Rails 命令创建的这些新页面之外,Rails 还会更新位于以下位置的路由文件:config/routes.rb
,添加一个get
您的主页的路由,您将其修改为根路由。
Rails 中的根路由指定当用户访问应用程序的根 URL 时将显示的内容。在这种情况下,您希望用户看到您的主页。打开位于以下位置的路由文件config/routes.rb
在你最喜欢的编辑器中:
In this file, replace get 'homepage/index'
with root 'homepage#index'
so that the file matches the following:
〜/rails_react_recipe/config/routes.rb
Rails.application.routes.draw do
root 'homepage#index'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
This modification instructs Rails to map requests to the root of the application to the index
action of the Homepage
controller, which in turn renders in the browser whatever is in the index.html.erb
file located at app/views/homepage/index.html.erb
.
保存并关闭文件。
要验证这是否有效,请启动您的应用程序:
当您在浏览器中打开或刷新应用程序时,将加载应用程序的新登录页面:
一旦您确认您的应用程序正在运行,请按CTRL+C
停止服务器。
接下来,打开~/rails_react_recipe/app/views/homepage/index.html.erb
file:
-
nano〜/rails_react_recipe/app/views/homepage/index.html.erb
删除文件内的代码,然后将文件另存为空。通过这样做,您可以确保index.html.erb
不要干扰前端的 React 渲染。
现在您已经为应用程序设置了主页,您可以转到下一部分,在其中您将配置应用程序的前端以使用 React。
在此步骤中,您将配置 Rails 以在应用程序的前端使用 React,而不是其模板引擎。这个新配置将允许您使用 React 创建更具视觉吸引力的主页。
在的帮助下esbuild
生成 Rails 应用程序时指定的选项,允许 JavaScript 与 Rails 无缝协作所需的大部分设置已经就位。剩下的就是将 React 应用程序的入口点加载到esbuild
JavaScript 文件的入口点。为此,首先在app/javascript
目录:
-
mkdir〜/rails_react_recipe/app/javascript/components
The components
目录将容纳主页的组件以及应用程序中的其他 React 组件,包括 React 应用程序的入口文件。
接下来,打开application.js
文件位于app/javascript/application.js
:
-
nano〜/rails_react_recipe/app/javascript/application.js
将突出显示的代码行添加到文件中:
〜/rails_react_recipe/app/javascript/application.js
// Entry point for the build script in your package.json
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
import "./components"
添加到的代码行application.js
文件将导入条目中的代码index.jsx
文件,使其可用esbuild
用于捆绑。随着/components
导入到 Rails 应用程序的 JavaScript 入口点的目录中,您可以为主页创建一个 React 组件。主页将包含一些文本和一个号召性用语按钮来查看所有食谱。
保存并关闭文件。
然后,创建一个Home.jsx
文件在components
目录:
-
nano〜/rails_react_recipe/app/javascript/components/Home.jsx
将以下代码添加到文件中:
〜/rails_react_recipe/app/javascript/components/Home.jsx
import React from "react";
import { Link } from "react-router-dom";
export default () => (
<div className="vw-100 vh-100 primary-color d-flex align-items-center justify-content-center">
<div className="jumbotron jumbotron-fluid bg-transparent">
<div className="container secondary-color">
<h1 className="display-4">Food Recipes</h1>
<p className="lead">
A curated list of recipes for the best homemade meal and delicacies.
</p>
<hr className="my-4" />
<Link
to="/recipes"
className="btn btn-lg custom-button"
role="button"
>
View Recipes
</Link>
</div>
</div>
</div>
);
在此代码中,您导入 React 和Link
来自 React Router 的组件。这Link
组件创建一个超链接以从一个页面导航到另一页面。然后,您可以为主页创建并导出包含某种标记语言的功能组件,并使用 Bootstrap 类进行样式设置。
保存并关闭文件。
和你的Home
组件集,您现在将使用 React Router 设置路由。创建一个routes
目录中的app/javascript
目录:
-
mkdir〜/rails_react_recipe/app/javascript/routes
The routes
目录将包含一些路由及其相应的组件。每当加载任何指定的路由时,它都会将其相应的组件呈现给浏览器。
In the routes
目录,创建一个index.jsx
file:
-
nano〜/rails_react_recipe/app/javascript/routes/index.jsx
添加以下代码:
〜/rails_react_recipe/app/javascript/routes/index.jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
export default (
<Router>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</Router>
);
In this index.jsx
路由文件中,您导入以下模块:React
允许您使用 React 的模块,以及BrowserRouter
, Routes
, and Route
React Router 的模块,它们一起帮助您从一条路线导航到另一条路线。最后,您导入您的Home
组件,每当请求与根(/
) 路线。当您想要向应用程序添加更多页面时,您可以在此文件中声明一个路由,并将其与您想要为该页面呈现的组件相匹配。
保存并退出文件。
您现在已经使用 React Router 设置了路由。为了让 React 了解可用的路由并使用它们,这些路由必须在应用程序的入口点可用。为了实现这一点,您将在一个组件中渲染您的路由,React 将在您的入口文件中渲染该组件。
创建一个App.jsx
文件在app/javascript/components
目录:
-
nano〜/rails_react_recipe/app/javascript/components/App.jsx
将以下代码添加到App.jsx
file:
〜/rails_react_recipe/app/javascript/components/App.jsx
import React from "react";
import Routes from "../routes";
export default props => <>{Routes}</>;
In the App.jsx
文件中,您导入 React 和刚刚创建的路由文件。然后导出一个组件来渲染其中的路线碎片。该组件将在应用程序的入口点呈现,使路由在应用程序加载时可用。
保存并关闭文件。
现在你有了你的App.jsx
设置后,您可以将其呈现在您的条目文件中。创建一个index.jsx
文件在components
目录:
-
nano〜/rails_react_recipe/app/javascript/components/index.jsx
将以下代码添加到index.js
file:
〜/rails_react_recipe/app/javascript/components/index.jsx
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
document.addEventListener("turbo:load", () => {
const root = createRoot(
document.body.appendChild(document.createElement("div"))
);
root.render(<App />);
});
In the import
行,导入 React 库,createRoot
来自 ReactDOM 的函数,以及你的App
成分。使用 ReactDOMcreateRoot
函数,您创建一个根元素作为div
元素附加到页面,然后渲染您的App
其中的组件。当应用程序加载时,React 将渲染应用程序的内容App
里面的组件div
页面上的元素。
保存并退出文件。
最后,您将向主页添加一些 CSS 样式。
打开application.bootstrap.scss
文件在你的~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
目录:
-
nano〜/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
接下来,将内容替换为application.bootstrap.scss
文件包含以下代码:
〜/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
@import 'bootstrap/scss/bootstrap';
@import 'bootstrap-icons/font/bootstrap-icons';
.bg_primary-color {
background-color: #FFFFFF;
}
.primary-color {
background-color: #FFFFFF;
}
.bg_secondary-color {
background-color: #293241;
}
.secondary-color {
color: #293241;
}
.custom-button.btn {
background-color: #293241;
color: #FFF;
border: none;
}
.hero {
width: 100vw;
height: 50vh;
}
.hero img {
object-fit: cover;
object-position: top;
height: 100%;
width: 100%;
}
.overlay {
height: 100%;
width: 100%;
opacity: 0.4;
}
您为页面设置一些自定义颜色。这.hero
部分将创建一个框架英雄形象,或者您网站首页上的大型网页横幅,您稍后将添加。此外,custom-button.btn
设置用户用于进入应用程序的按钮的样式。
设置好 CSS 样式后,保存并退出文件。
接下来,重新启动应用程序的 Web 服务器:
然后在浏览器中重新加载该应用程序。将加载一个全新的主页:
停止网络服务器CTRL+C
.
在此步骤中,您将应用程序配置为使用 React 作为其前端。在下一步中,您将创建模型和控制器,使您能够创建、读取、更新和删除配方。
现在您已经为应用程序设置了 React 前端,您将创建一个 Recipe 模型和控制器。菜谱模型将表示包含有关用户菜谱信息的数据库表,而控制器将接收并处理创建、读取、更新或删除菜谱的请求。当用户请求配方时,配方控制器接收该请求并将其传递给配方模型,配方模型从数据库检索请求的数据。然后,模型返回配方数据作为对控制器的响应。最后,该信息显示在浏览器中。
首先创建一个Recipe
模型使用generate model
Rails 提供的子命令,指定模型的名称及其列和数据类型。运行以下命令:
- Rails 生成模型 配方名称:字符串 成分:文本 说明:文本 图片:字符串
前面的命令指示 Rails 创建一个Recipe
模型连同name
类型列string
, an ingredients
and instruction
类型列text
, 和image
类型列string
。本教程已将模型命名为Recipe
,因为 Rails 中的模型使用单数名称,而其对应的数据库表使用复数名称。
运行generate model
命令创建两个文件并打印以下输出:
Output
invoke active_record
create db/migrate/20221017220817_create_recipes.rb
create app/models/recipe.rb
创建的两个文件是:
- A
recipe.rb
保存所有与模型相关的逻辑的文件。
- A
20221017220817_create_recipes.rb
file (the number at the beginning of the file may differ depending on the date when you run the command). This migration file contains the instruction for creating the database structure.
接下来,您将编辑配方模型文件以确保仅将有效数据保存到数据库中。您可以通过向模型添加一些数据库验证来实现此目的。
打开位于以下位置的配方模型app/models/recipe.rb
:
-
nano〜/rails_react_recipe/app/models/recipe.rb
将以下突出显示的代码行添加到文件中:
〜/rails_react_recipe/app/models/recipe.rb
class Recipe < ApplicationRecord
validates :name, presence: true
validates :ingredients, presence: true
validates :instruction, presence: true
end
在此代码中,您添加模型验证,以检查是否存在name
, ingredients
, and instruction
字段。没有这三个字段,菜谱无效,不会保存到数据库中。
保存并关闭文件。
For Rails to create the recipes
table in your database, you have to run a migration, which is a way to make changes to your database programmatically. To ensure that the migration works with the database you set up, you must make changes to the 20221017220817_create_recipes.rb
file.
在编辑器中打开此文件:
-
nano ~/rails_react_recipe/db/migrate/20221017220817_create_recipes.rb
添加突出显示的材料,使您的文件与以下内容匹配:
db/migrate/20221017220817_create_recipes.rb
class CreateRecipes < ActiveRecord::Migration[5.2]
def change
create_table :recipes do |t|
t.string :name, null: false
t.text :ingredients, null: false
t.text :instruction, null: false
t.string :image, default: 'https://raw.githubusercontent.com/do-community/react_rails_recipe/master/app/assets/images/Sammy_Meal.jpg'
t.timestamps
end
end
end
This migration file contains a Ruby class with a change
method and a command to create a table called recipes
along with the columns and their data types. You also update 20221017220817_create_recipes.rb
with a NOT NULL
constraint on the name
, ingredients
, and instruction
columns by adding null: false
, ensuring that these columns have a value before changing the database. Finally, you add a default image URL for your image column; this could be another URL if you want to use a different image.
完成这些更改后,保存并退出文件。您现在已准备好运行迁移并创建表。在您的终端中,运行以下命令:
您可以使用数据库迁移命令来运行迁移文件中的说明。命令成功运行后,您将收到类似于以下内容的输出:
Output
== 20190407161357 CreateRecipes: migrating ====================================
-- create_table(:recipes)
-> 0.0140s
== 20190407161357 CreateRecipes: migrated (0.0141s) ===========================
菜谱模型就位后,您接下来将创建菜谱控制器以添加用于创建、读取和删除菜谱的逻辑。运行以下命令:
- Rails 生成控制器 api/v1/Recipes 索引 create show destroy --skip-template-engine --no-helper
在此命令中,您创建一个Recipes
控制器中的api/v1
目录与index
, create
, show
, and destroy
行动。这index
操作将处理获取所有食谱;这create
行动将负责创造新的食谱;这show
操作将获取单个食谱,并且destroy
操作将保存删除菜谱的逻辑。
您还可以传递一些标志以使控制器更加轻量级,包括:
-
--skip-template-engine
,它指示 Rails 跳过生成 Rails 视图文件,因为 React 会处理您的前端需求。
-
--no-helper
,它指示 Rails 跳过为控制器生成帮助程序文件。
运行该命令还会更新您的路由文件,其中包含每个操作的路由Recipes
控制器。
当命令运行时,它将打印如下输出:
Output
create app/controllers/api/v1/recipes_controller.rb
route namespace :api do
namespace :v1 do
get 'recipes/index'
get 'recipes/create'
get 'recipes/show'
get 'recipes/destroy'
end
end
要使用这些路线,您需要更改您的config/routes.rb
文件。打开routes.rb
在文本编辑器中创建文件:
-
nano〜/rails_react_recipe/config/routes.rb
更新此文件,使其类似于以下代码,更改或添加突出显示的行:
〜/rails_react_recipe/config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
get 'recipes/index'
post 'recipes/create'
get '/show/:id', to: 'recipes#show'
delete '/destroy/:id', to: 'recipes#destroy'
end
end
root 'homepage#index'
get '/*path' => 'homepage#index'
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")
# root "articles#index"
end
在此路由文件中,您可以修改create
and destroy
路线,以便它可以post
and delete
数据。您还可以修改show
and destroy
通过添加一个动作:id
路由的参数。:id
将保存您要读取或删除的菜谱的标识号。
您添加一条包罗万象的路线get '/*path'
这会将与现有路由不匹配的任何其他请求定向到index
的行动homepage
控制器。前端路由将处理与创建、读取或删除菜谱无关的请求。
保存并退出文件。
要评估应用程序中可用的路由列表,请运行以下命令:
运行此命令会显示一长串 URI 模式、动词以及与您的项目匹配的控制器或操作。
接下来,您将添加逻辑以立即获取所有食谱。 Rails 使用活动记录库来处理这样的数据库相关任务。 ActiveRecord 将类连接到关系数据库表,并提供丰富的 API 来使用它们。
要获取所有食谱,您将使用 ActiveRecord 查询食谱表并获取数据库中的所有食谱。
打开recipes_controller.rb
使用以下命令创建文件:
-
nano〜/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
将突出显示的行添加到菜谱控制器:
〜/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
class Api::V1::RecipesController < ApplicationController
def index
recipe = Recipe.all.order(created_at: :desc)
render json: recipe
end
def create
end
def show
end
def destroy
end
end
In your index
动作,你使用ActiveRecord的all
获取数据库中所有食谱的方法。使用order
方法中,您可以按创建日期降序排列它们,这会将最新的食谱放在第一位。最后,您将食谱列表作为 JSON 响应发送render
.
接下来,您将添加创建新食谱的逻辑。与获取所有菜谱一样,您将依靠 ActiveRecord 来验证并保存提供的菜谱详细信息。使用以下突出显示的代码行更新您的配方控制器:
〜/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
class Api::V1::RecipesController < ApplicationController
def index
recipe = Recipe.all.order(created_at: :desc)
render json: recipe
end
def create
recipe = Recipe.create!(recipe_params)
if recipe
render json: recipe
else
render json: recipe.errors
end
end
def show
end
def destroy
end
private
def recipe_params
params.permit(:name, :image, :ingredients, :instruction)
end
end
In the create
动作,你使用ActiveRecord的create
创建新配方的方法。这create
方法可以一次性将提供的所有控制器参数分配给模型。这种方法可以很容易地创建记录,但也带来了恶意使用的可能性。可以通过使用来防止恶意使用参数强Rails 提供的功能。这样,除非允许,否则无法分配参数。你通过一个recipe_params
参数到create
代码中的方法。这recipe_params
is a private
允许控制器参数防止错误或恶意内容进入数据库的方法。在这种情况下,您允许name
, image
, ingredients
, and instruction
参数的有效使用create
method.
您的食谱控制器现在可以读取和创建食谱。剩下的就是读取和删除单个菜谱的逻辑。使用突出显示的代码更新您的食谱控制器:
〜/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
class Api::V1::RecipesController < ApplicationController
before_action :set_recipe, only: %i[show destroy]
def index
recipe = Recipe.all.order(created_at: :desc)
render json: recipe
end
def create
recipe = Recipe.create!(recipe_params)
if recipe
render json: recipe
else
render json: recipe.errors
end
end
def show
render json: @recipe
end
def destroy
@recipe&.destroy
render json: { message: 'Recipe deleted!' }
end
private
def recipe_params
params.permit(:name, :image, :ingredients, :instruction)
end
def set_recipe
@recipe = Recipe.find(params[:id])
end
end
在新的代码行中,您创建一个私有的set_recipe
调用的方法before_action
仅当show
and delete
操作与请求匹配。这set_recipe
方法使用 ActiveRecord 的find
找到食谱的方法id
匹配id
中提供的params
并将其分配给实例变量@recipe
。在里面show
行动,你返回@recipe
对象设置由set_recipe
方法作为 JSON 响应。
In the destroy
动作,您使用 Ruby 的安全导航运算符做了类似的事情&.
,这避免了nil
调用方法时出错。通过此添加,您可以仅删除存在的配方,然后发送消息作为响应。
进行这些更改后recipes_controller.rb
, 保存并关闭文件。
在此步骤中,您为配方创建了模型和控制器。您已经编写了在后端使用菜谱所需的所有逻辑。在下一部分中,您将创建组件来查看您的食谱。
在本部分中,您将创建用于查看菜谱的组件。您将创建两个页面:一个用于查看所有现有食谱,另一个用于查看各个食谱。
您将首先创建一个页面来查看所有食谱。在创建页面之前,您需要使用食谱,因为您的数据库当前为空。 Rails 提供了一种为应用程序创建种子数据的方法。
打开名为的种子文件seeds.rb
用于编辑:
-
nano〜/rails_react_recipe/db/seeds.rb
将种子文件的初始内容替换为以下代码:
〜/rails_react_recipe/db/seeds.rb
9.times do |i|
Recipe.create(
name: "Recipe #{i + 1}",
ingredients: '227g tub clotted cream, 25g butter, 1 tsp cornflour,100g parmesan, grated nutmeg, 250g fresh fettuccine or tagliatelle, snipped chives or chopped parsley to serve (optional)',
instruction: 'In a medium saucepan, stir the clotted cream, butter, and cornflour over a low-ish heat and bring to a low simmer. Turn off the heat and keep warm.'
)
end
在此代码中,您使用一个循环来指示 Rails 创建九个配方,其中包含以下部分:name
, ingredients
, and instruction
。保存并退出文件。
要使用此数据为数据库播种,请在终端中运行以下命令:
运行此命令会将九个食谱添加到您的数据库中。现在您可以获取它们并在前端渲染它们。
查看所有菜谱的组件将向index
行动于RecipesController
获取所有食谱的列表。这些食谱随后将显示在页面上的卡片中。
创建一个Recipes.jsx
文件在app/javascript/components
目录:
-
nano〜/rails_react_recipe/app/javascript/components/Recipes.jsx
文件打开后,导入React
, useState
, useEffect
, Link
, and useNavigate
通过添加以下行来模块:
〜/rails_react_recipe/app/javascript/components/Recipes.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
接下来,添加突出显示的行以创建并导出名为的功能性 React 组件Recipes
:
〜/rails_react_recipe/app/javascript/components/Recipes.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
const Recipes = () => {
const navigate = useNavigate();
const [recipes, setRecipes] = useState([]);
};
export default Recipes;
在 - 的里面Recipe
组件,React Router 的导航 API 将调用使用导航钩。反应的useState钩子将初始化recipes
state,这是一个空数组([]
),和一个setRecipes
更新函数recipes
state.
接下来,在一个useEffect钩子后,您将发出 HTTP 请求来获取所有食谱。为此,请添加突出显示的行:
〜/rails_react_recipe/app/javascript/components/Recipes.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
const Recipes = () => {
const navigate = useNavigate();
const [recipes, setRecipes] = useState([]);
useEffect(() => {
const url = "/api/v1/recipes/index";
fetch(url)
.then((res) => {
if (res.ok) {
return res.json();
}
throw new Error("Network response was not ok.");
})
.then((res) => setRecipes(res))
.catch(() => navigate("/"));
}, []);
};
export default Recipes;
In your useEffect
钩子,您使用 HTTP 调用来获取所有食谱获取API。如果响应成功,应用程序会将食谱数组保存到recipes
状态。如果发生错误,它将把用户重定向到主页。
最后,返回渲染组件时将在浏览器页面上评估并显示的元素的标记。在这种情况下,该组件将呈现一张食谱卡recipes
状态。将突出显示的行添加到Recipes.jsx
:
〜/rails_react_recipe/app/javascript/components/Recipes.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
const Recipes = () => {
const navigate = useNavigate();
const [recipes, setRecipes] = useState([]);
useEffect(() => {
const url = "/api/v1/recipes/index";
fetch(url)
.then((res) => {
if (res.ok) {
return res.json();
}
throw new Error("Network response was not ok.");
})
.then((res) => setRecipes(res))
.catch(() => navigate("/"));
}, []);
const allRecipes = recipes.map((recipe, index) => (
<div key={index} className="col-md-6 col-lg-4">
<div className="card mb-4">
<img
src={recipe.image}
className="card-img-top"
alt={`${recipe.name} image`}
/>
<div className="card-body">
<h5 className="card-title">{recipe.name}</h5>
<Link to={`/recipe/${recipe.id}`} className="btn custom-button">
View Recipe
</Link>
</div>
</div>
</div>
));
const noRecipe = (
<div className="vw-100 vh-50 d-flex align-items-center justify-content-center">
<h4>
No recipes yet. Why not <Link to="/new_recipe">create one</Link>
</h4>
</div>
);
return (
<>
<section className="jumbotron jumbotron-fluid text-center">
<div className="container py-5">
<h1 className="display-4">Recipes for every occasion</h1>
<p className="lead text-muted">
We’ve pulled together our most popular recipes, our latest
additions, and our editor’s picks, so there’s sure to be something
tempting for you to try.
</p>
</div>
</section>
<div className="py-5">
<main className="container">
<div className="text-end mb-3">
<Link to="/recipe" className="btn custom-button">
Create New Recipe
</Link>
</div>
<div className="row">
{recipes.length > 0 ? allRecipes : noRecipe}
</div>
<Link to="/" className="btn btn-link">
Home
</Link>
</main>
</div>
</>
);
};
export default Recipes;
保存并退出Recipes.jsx
.
现在您已经创建了一个组件来显示所有食谱,您将为它创建一个路由。打开前端路由文件app/javascript/routes/index.jsx
:
-
nano应用程序/javascript/routes/index.jsx
将突出显示的行添加到文件中:
〜/rails_react_recipe/app/javascript/routes/index.jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
export default (
<Router>
<Routes>
<Route path="/" exact component={Home} />
<Route path="/recipes" element={<Recipes />} />
</Routes>
</Router>
);
保存并退出文件。
此时,最好验证您的代码是否按预期工作。像之前一样,使用以下命令启动服务器:
然后在浏览器中打开该应用程序。按查看食谱主页上的按钮可访问包含种子食谱的显示页面:
Use CTRL+C
在您的终端中停止服务器并返回到您的提示符。
现在您可以查看应用程序中的所有食谱,是时候创建第二个组件来查看各个食谱了。创建一个Recipe.jsx
文件在app/javascript/components
目录:
-
nano应用程序/javascript/组件/Recipe.jsx
与Recipes
组件,导入React
, useState
, useEffect
, Link
, useNavigate
, and useParam
通过添加以下行来模块:
〜/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
接下来,添加突出显示的行以创建并导出名为的功能性 React 组件Recipe
:
〜/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
};
export default Recipe;
像Recipes
组件,您可以使用以下命令初始化 React Router 导航useNavigate
钩。 Arecipe
状态和一个setRecipe
函数将更新状态useState
钩。此外,您还可以调用useParams
hook,它返回一个对象,其键/值对是 URL 参数。
要查找特定的食谱,您的应用程序需要知道该食谱的id
,这意味着你的Recipe
组件期望一个id
param
在网址中。您可以通过以下方式访问此内容params
保存返回值的对象useParams
hook.
接下来,声明一个useEffect
您将在其中访问的挂钩id
param
来自params
目的。拿到食谱后id
param,您将发出 HTTP 请求来获取配方。将突出显示的行添加到您的文件中:
〜/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
};
export default Recipe;
In the useEffect
钩子,你使用params.id
值来发出 GET HTTP 请求来获取拥有该配方的配方id
然后使用将其保存到组件状态setRecipe
功能。如果菜谱不存在,应用程序会将用户重定向到菜谱页面。
接下来,添加一个addHtmlEntities
函数,它将用于替换字符实体HTML 实体在组件中。这addHtmlEntities
函数将接受一个字符串并用 HTML 实体替换所有转义的左括号和右括号。此功能将帮助您转换配方指令中保存的任何转义字符。添加突出显示的行:
〜/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
};
export default Recipe;
最后,返回标记以通过添加突出显示的行来在页面上以组件状态呈现配方:
〜/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
const ingredientList = () => {
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
return ingredientList;
};
const recipeInstruction = addHtmlEntities(recipe.instruction);
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList()}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`,
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button
type="button"
className="btn btn-danger"
>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
};
export default Recipe;
With an ingredientList
函数中,您可以将逗号分隔的食谱成分拆分为一个数组,并对其进行映射以创建成分列表。如果没有成分,应用程序会显示一条消息:没有可用的成分。您还可以通过将配方指令传递给addHtmlEntities
功能。最后,代码将食谱图像显示为英雄图像,添加一个删除食谱按钮旁边的食谱说明,并添加一个链接回食谱页面的按钮。
Note:使用 React 的dangerouslySetInnerHTML
属性是有风险的,因为它会将您的应用程序暴露给跨站脚本攻击。通过确保使用以下命令替换创建菜谱时输入的特殊字符,可以降低这种风险:stripHtmlEntities
中声明的函数NewRecipe
成分。
保存并退出文件。
要查看Recipe
页面上的组件,您将其添加到您的路由文件中。打开您的路线文件进行编辑:
-
nano应用程序/javascript/routes/index.jsx
将以下突出显示的行添加到文件中:
〜/rails_react_recipe/app/javascript/routes/index.jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";
export default (
<Router>
<Routes>
<Route path="/" exact component={Home} />
<Route path="/recipes" exact component={Recipes} />
<Route path="/recipe/:id" element={<Recipe />} />
</Routes>
</Router>
);
你导入你的Recipe
在此路由文件中添加一个组件并添加一条路由。其路线有一条:id
param
将被替换为id
您要查看的食谱的名称。
保存并关闭文件。
Use the bin/dev
脚本再次启动服务器,然后访问http://localhost:3000
在您的浏览器中。点击查看食谱按钮导航到食谱页面。在食谱页面上,通过单击其访问任何食谱查看食谱按钮。您将看到一个页面,其中填充了数据库中的数据:
您可以使用以下命令停止服务器CTRL+C
.
在此步骤中,您向数据库添加了九个菜谱,并创建了组件来查看这些菜谱(单独查看或作为集合查看)。在下一步中,您将添加一个组件来创建菜谱。
拥有可用的食品食谱应用程序的下一步是创建新食谱的能力。在此步骤中,您将为此功能创建一个组件。该组件将包含一个表单,用于从用户收集所需的食谱详细信息,然后向create
行动于Recipe
控制器保存配方数据。
创建一个NewRecipe.jsx
文件在app/javascript/components
目录:
-
nano应用程序/javascript/components/NewRecipe.jsx
在新文件中,导入React
, useState
, Link
, and useNavigate
您在其他组件中使用的模块:
〜/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
接下来,创建并导出函数NewRecipe
通过添加突出显示的行来组成组件:
〜/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
};
export default NewRecipe;
与之前的组件一样,您可以使用以下命令初始化 React 路由器导航useNavigate
挂钩,然后使用useState
钩子来初始化name
, ingredients
, and instruction
状态,每个状态都有各自的更新功能。这些是您创建有效配方所需的字段。
接下来,创建一个stripHtmlEntities
将转换特殊字符的函数(例如<
)到他们的转义/编码值(如<
), 分别。为此,请将突出显示的行添加到NewRecipe
成分:
〜/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
const stripHtmlEntities = (str) => {
return String(str)
.replace(/\n/g, "<br> <br>")
.replace(/</g, "<")
.replace(/>/g, ">");
};
};
export default NewRecipe;
In the stripHtmlEntities
函数,你替换<
and >
字符及其转义值。这样,您就不会在数据库中存储原始 HTML。
接下来,添加突出显示的行以添加onChange
and onSubmit
函数到NewRecipe
处理表单编辑和提交的组件:
〜/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
const stripHtmlEntities = (str) => {
return String(str)
.replace(/\n/g, "<br> <br>")
.replace(/</g, "<")
.replace(/>/g, ">");
};
const onChange = (event, setFunction) => {
setFunction(event.target.value);
};
const onSubmit = (event) => {
event.preventDefault();
const url = "/api/v1/recipes/create";
if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
return;
const body = {
name,
ingredients,
instruction: stripHtmlEntities(instruction),
};
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "POST",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => navigate(`/recipe/${response.id}`))
.catch((error) => console.log(error.message));
};
};
export default NewRecipe;
The onChange
函数接受用户输入event
和状态设置器函数,然后它随后使用用户输入值更新状态。在里面onSubmit
函数时,您检查所有必需的输入是否为空。然后,您构建一个包含创建新配方所需参数的对象。使用stripHtmlEntities
函数,你替换<
and >
配方指令中的字符及其转义值,并用中断标记替换每个新行字符,从而保留用户输入的文本格式。最后,您发出 POST HTTP 请求来创建新配方,并在成功响应后重定向到其页面。
为了防止跨站请求伪造 (CSRF)攻击时,Rails 将 CSRF 安全令牌附加到 HTML 文档。每当非GET
提出请求。随着token
在前面的代码中,您的应用程序会验证服务器上的令牌,并在安全令牌与预期不匹配时引发异常。在里面onSubmit
函数,应用程序检索CSRF代币通过 Rails 嵌入到 HTML 文档中,然后使用 JSON 字符串发出 HTTP 请求。如果成功创建菜谱,应用程序会将用户重定向到菜谱页面,他们可以在其中查看新创建的菜谱。
最后,返回呈现表单的标记,供用户输入用户希望创建的菜谱的详细信息。添加突出显示的行:
〜/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
const stripHtmlEntities = (str) => {
return String(str)
.replace(/\n/g, "<br> <br>")
.replace(/</g, "<")
.replace(/>/g, ">");
};
const onChange = (event, setFunction) => {
setFunction(event.target.value);
};
const onSubmit = (event) => {
event.preventDefault();
const url = "/api/v1/recipes/create";
if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
return;
const body = {
name,
ingredients,
instruction: stripHtmlEntities(instruction),
};
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "POST",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => navigate(`/recipe/${response.id}`))
.catch((error) => console.log(error.message));
};
return (
<div className="container mt-5">
<div className="row">
<div className="col-sm-12 col-lg-6 offset-lg-3">
<h1 className="font-weight-normal mb-5">
Add a new recipe to our awesome recipe collection.
</h1>
<form onSubmit={onSubmit}>
<div className="form-group">
<label htmlFor="recipeName">Recipe name</label>
<input
type="text"
name="name"
id="recipeName"
className="form-control"
required
onChange={(event) => onChange(event, setName)}
/>
</div>
<div className="form-group">
<label htmlFor="recipeIngredients">Ingredients</label>
<input
type="text"
name="ingredients"
id="recipeIngredients"
className="form-control"
required
onChange={(event) => onChange(event, setIngredients)}
/>
<small id="ingredientsHelp" className="form-text text-muted">
Separate each ingredient with a comma.
</small>
</div>
<label htmlFor="instruction">Preparation Instructions</label>
<textarea
className="form-control"
id="instruction"
name="instruction"
rows="5"
required
onChange={(event) => onChange(event, setInstruction)}
/>
<button type="submit" className="btn custom-button mt-3">
Create Recipe
</button>
<Link to="/recipes" className="btn btn-link mt-3">
Back to recipes
</Link>
</form>
</div>
</div>
</div>
);
};
export default NewRecipe;
返回的标记包括一个包含三个输入字段的表单;各一个recipeName
, recipeIngredients
, and instruction
。每个输入字段都有一个onChange
调用的事件处理程序onChange
功能。一个onSubmit
事件处理程序也附加到提交按钮并调用onSubmit
提交表单数据的函数。
保存并退出文件。
要在浏览器中访问此组件,请使用其路由更新您的路由文件:
-
nano应用程序/javascript/routes/index.jsx
更新您的路线文件以包含这些突出显示的行:
〜/rails_react_recipe/app/javascript/routes/index.jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";
import NewRecipe from "../components/NewRecipe";
export default (
<Router>
<Routes>
<Route path="/" exact component={Home} />
<Route path="/recipes" exact component={Recipes} />
<Route path="/recipe/:id" exact component={Recipe} />
<Route path="/recipe" element={<NewRecipe />} />
</Routes>
</Router>
);
路线就位后,保存并退出文件。
重新启动您的开发服务器并访问http://localhost:3000
在您的浏览器中。导航到食谱页面并单击创建新食谱按钮。您将找到一个带有表单的页面,用于将食谱添加到您的数据库:
输入所需的食谱详细信息,然后单击创建食谱按钮。新创建的菜谱将出现在页面上。准备好后,关闭服务器。
在此步骤中,您向食品食谱应用程序添加了创建食谱的功能。在下一步中,您将添加删除食谱的功能。
在本部分中,您将修改配方组件以包含用于删除配方的选项。当您单击菜谱页面上的删除按钮时,应用程序将发送请求以从数据库中删除菜谱。
首先,打开你的Recipe.jsx
供编辑的文件:
-
nano应用程序/javascript/组件/Recipe.jsx
In the Recipe
组件,添加一个deleteRecipe
具有突出显示的行的函数:
〜/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
const deleteRecipe = () => {
const url = `/api/v1/destroy/${params.id}`;
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "DELETE",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(() => navigate("/recipes"))
.catch((error) => console.log(error.message));
};
const ingredientList = () => {
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
return ingredientList;
};
const recipeInstruction = addHtmlEntities(recipe.instruction);
return (
<div className="">
...
In the deleteRecipe
函数,你得到id
要删除的配方,然后构建您的 URL 并获取 CSRF 令牌。接下来,您制作一个DELETE
请求给Recipes
控制器删除配方。如果成功删除菜谱,应用程序会将用户重定向到菜谱页面。
运行以下代码deleteRecipe
每当单击删除按钮时,都会将其作为单击事件处理程序传递给按钮。添加一个onClick
组件中删除按钮元素的事件:
〜/rails_react_recipe/app/javascript/components/Recipe.jsx
...
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList()}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`,
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button
type="button"
className="btn btn-danger"
onClick={deleteRecipe}
>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
...
在本教程的此时,您已完成Recipe.jsx
文件应与此文件匹配:
〜/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
const deleteRecipe = () => {
const url = `/api/v1/destroy/${params.id}`;
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "DELETE",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(() => navigate("/recipes"))
.catch((error) => console.log(error.message));
};
const ingredientList = () => {
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
return ingredientList;
};
const recipeInstruction = addHtmlEntities(recipe.instruction);
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList()}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`,
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button
type="button"
className="btn btn-danger"
onClick={deleteRecipe}
>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
};
export default Recipe;
保存并退出文件。
重新启动应用服务器并导航至主页。点击查看食谱按钮访问所有现有食谱,然后打开任何特定食谱并单击删除食谱页面上的按钮可删除文章。您将被重定向到菜谱页面,并且删除的菜谱将不再存在。
随着删除按钮的工作,您现在拥有一个功能齐全的食谱应用程序!
在本教程中,您使用 Ruby on Rails 和 React 前端创建了一个食品食谱应用程序,使用 PostgreSQL 作为数据库并使用 Bootstrap 进行样式设置。如果您想继续使用 Ruby on Rails 进行构建,请考虑遵循我们的使用 SSH 隧道保护三层 Rails 应用程序中的通信教程或访问我们的如何使用 Ruby 编码系列来刷新您的 Ruby 技能。要更深入地了解 React,请尝试如何使用 React 显示来自 DigitalOcean API 的数据.