作者选择了 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前端显示它们。
如果你想看看这个应用程序的代码,请参阅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 macOS或 Ubuntu 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
命令s
或server
会启动 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 应用程序。
要随时停止 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 依赖性,其中包括:
- React Router,用于处理React应用程序中的导航。
- Bootstrap,用于对前端组件进行定制。
在您的终端窗口中运行以下命令以使用 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
在浏览器中打开应用程序,您将看到您的应用程序的新定位页面:
一旦您确认您的应用程序正在工作,请按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
,Route
和Switch
模块,这些模块一起帮助我们从一个路由导航到另一个路由。最后,你导入了你的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服务器,然后在你的浏览器中重新加载应用程序。
在此步骤中,您将配置您的应用程序以使用 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 We’ve pulled together our most popular recipes, our latest
59 additions, and our editor’s picks, so there’s 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
继续,并在您的浏览器中打开应用程序. 点击主页上的 ** 查看食谱**按钮,您将看到您的种子食谱的显示:
在您的终端窗口中使用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
。这意味着你的食谱
组件预计一个id
的param
。
接下来,添加一个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(/</g, "<")
36 .replace(/>/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(/</g, "<")
36 .replace(/>/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**
按钮查看任何食谱。 您将收到来自您的数据库的数据填充的页面:
在本节中,您已将九个食谱添加到您的数据库中,并创建了组件以单独查看这些食谱和作为一组合。
第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
,onSubmit
和stripHtmlEntities
,你将这些字段绑定到this
。这些方法将处理更新状态,形成提交,并将特殊字符(如<
)转换为它们的逃出/编码值(如<
),分别。
接下来,通过将突出的行添加到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, "<")
19 .replace(/>/g, ">");
20 }
21
22}
23
24export default NewRecipe;
在stripHtmlEntities
方法中,你正在用它们的逃脱值代替<
和>
字符,这样你就不会在你的数据库中存储原始HTML。
接下来,将onChange
和onSubmit
方法添加到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, "<")
19 .replace(/>/g, ">");
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, "<")
19 .replace(/>/g, ">");
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
,recipeIngredients
和instruction
。每一个输入字段都有一个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
。 导航到食谱页面,然后点击创建新食谱
按钮。 您将找到一个页面以添加食谱到您的数据库的表格:
输入所需的食谱细节并点击创建食谱
按钮;您将在页面上看到新创建的食谱。
在此步骤中,您通过添加创建食谱的能力,为您的食谱应用带来了生命,在下一步,您将添加删除食谱的功能。
步骤 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(/</g, "<")
32 .replace(/>/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(/</g, "<")
17 .replace(/>/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技能。