Reactフロントエンドを使用してRubyonRailsプロジェクトをセットアップする方法
###序章
Ruby on Rails は、人気のあるサーバー側のWebアプリケーションフレームワークであり、このチュートリアルの執筆時点では、GitHubに42,000を超える星があります。 GitHub 、 Basecamp 、 SoundCloud 、 Airbnb 、など、今日Web上に存在する多くの人気のあるアプリケーションに電力を供給しますX156X]Twitch。 Ruby on Railsは、プログラマーの経験とその周りに築き上げられた情熱的なコミュニティに重点を置いており、最新のWebアプリケーションを構築および保守するために必要なツールを提供します。
React は、フロントエンドのユーザーインターフェイスを作成するために使用されるJavaScriptライブラリです。 Facebookに支えられて、今日Webで使用されている最も人気のあるフロントエンドライブラリの1つです。 Reactは、仮想ドキュメントオブジェクトモデル(DOM)、コンポーネントアーキテクチャ、状態管理などの機能を提供し、フロントエンド開発のプロセスをより組織的かつ効率的にします。
Webのフロントエンドがサーバー側のコードとは別のフレームワークに移行しているため、Railsの優雅さとReactの効率を組み合わせることで、現在のトレンドに基づいた強力で最新のアプリケーションを構築できます。 Railsテンプレートエンジンの代わりにReactを使用してRailsビュー内からコンポーネントをレンダリングすることにより、アプリケーションは、Ruby on Railsの表現力を活用しながら、JavaScriptとフロントエンド開発の最新の進歩の恩恵を受けることができます。
このチュートリアルでは、お気に入りのレシピを保存し、Reactフロントエンドで表示するRubyonRailsアプリケーションを作成します。 終了すると、 Bootstrap でスタイル設定されたReactインターフェースを使用して、レシピを作成、表示、および削除できるようになります。
このアプリケーションのコードを確認したい場合は、 DigitalOcean CommunityGitHubでこのチュートリアルのコンパニオンリポジトリを確認してください。
前提条件
このチュートリアルに従うには、次のものが必要です。
-
Node.jsおよびnpmが開発マシンにインストールされています。 このチュートリアルでは、Node.jsバージョン10.16.0とnpmバージョン6.9.0を使用します。 Node.jsは、ブラウザの外部でコードを実行できるJavaScriptランタイム環境です。 npm と呼ばれるパッケージマネージャーがプリインストールされており、パッケージをインストールおよび更新できます。 これらをmacOSまたはUbuntu18.04にインストールするには、Node.jsをインストールしてmacOSにローカル開発環境を作成する方法またはノードをインストールする方法の「PPAを使用してインストールする」セクションの手順に従います。 Ubuntu18.04の.js。
-
開発マシンにインストールされているYarnパッケージマネージャー。これにより、Reactフレームワークをダウンロードできます。 このチュートリアルはバージョン1.16.0でテストされました。 この依存関係をインストールするには、公式のYarnインストールガイドに従ってください。
-
RubyonRailsフレームワークのインストール。 これを入手するには、 Ubuntu18.04でrbenvを使用してRubyonRailsをインストールする方法またはCentOS7でrbenvを使用してRubyonRailsをインストールする方法に関するガイドに従ってください。 このアプリケーションをmacOSで開発したい場合は、macOSでrbenvを使用してRubyonRailsをインストールする方法に関するこのチュートリアルを参照してください。 このチュートリアルは、Rubyのバージョン2.6.3およびRailsのバージョン5.2.3でテストされているため、インストールプロセス中にこれらのバージョンを指定してください。
-
チュートリアルのステップ1と2に示されているPostgreSQLのインストールUbuntu18.04のRubyonRailsアプリケーションでPostgreSQLを使用する方法または
macOSのRubyonRailsアプリケーションでPostgreSQLを使用する方法[ X226X]。 このチュートリアルに従うには、PostgreSQLバージョン10を使用します。 Linuxの別のディストリビューションまたは別のOSでこのアプリケーションを開発する場合は、公式のPostgreSQLダウンロードページを参照してください。 PostgreSQLの使用方法の詳細については、PostgreSQLのインストールと使用方法のチュートリアルを参照してください。
ステップ1—新しいRailsアプリケーションを作成する
このステップでは、Railsアプリケーションフレームワーク上にレシピアプリケーションを構築します。 最初に、新しいRailsアプリケーションを作成します。このアプリケーションは、ほとんど構成を行わずに、そのままReactで動作するように設定されます。
Railsは、最新のWebアプリケーションを構築するために必要なすべてのものを作成するのに役立つジェネレーターと呼ばれる多数のスクリプトを提供します。 これらのコマンドの完全なリストとその機能を確認するには、ターミナルウィンドウで次のコマンドを実行します。
- rails -h
これにより、オプションの包括的なリストが生成され、アプリケーションのパラメータを設定できるようになります。 リストされているコマンドの1つは new
新しいRailsアプリケーションを作成するコマンド。
次に、を使用して新しいRailsアプリケーションを作成します。 new
発生器。 ターミナルウィンドウで次のコマンドを実行します。
- rails new rails_react_recipe -d=postgresql -T --webpack=react --skip-coffee
上記のコマンドは、名前の付いたディレクトリに新しいRailsアプリケーションを作成します rails_react_recipe
、必要なRubyとJavaScriptの依存関係をインストールし、Webpackを構成します。 これに関連付けられているフラグを見ていきましょう new
ジェネレータコマンド:
- The
-d
flagは、優先データベースエンジン(この場合はPostgreSQL)を指定します。 - The
-T
このチュートリアルの目的でテストを作成することはないため、flagはRailsにテストファイルの生成をスキップするように指示します。 Railsが提供するものとは異なるRubyテストツールを使用する場合にも、このコマンドをお勧めします。 - The
--webpack
Railsにwebpackbundler を使用してJavaScriptを事前構成するように指示します。この場合は、特にReactアプリケーション用です。 - The
--skip-coffee
RailsにCoffeeScriptを設定しないように要求します。これはこのチュートリアルでは必要ありません。
コマンドの実行が完了したら、に移動します rails_react_recipe
アプリのルートディレクトリであるディレクトリ:
- cd rails_react_recipe
次に、ディレクトリの内容を一覧表示します。
- ls
このルートディレクトリには、Railsアプリケーションの構造を構成する自動生成されたファイルとフォルダがいくつかあります。 package.json
Reactアプリケーションの依存関係を含むファイル。
これで、新しいRailsアプリケーションが正常に作成されたので、次のステップでそれをデータベースに接続する準備が整いました。
ステップ2—データベースのセットアップ
新しいRailsアプリケーションを実行する前に、まずそれをデータベースに接続する必要があります。 このステップでは、新しく作成したRailsアプリケーションをPostgreSQLデータベースに接続して、必要なときにレシピデータを保存およびフェッチできるようにします。
The database.yml
で見つかったファイル config/database.yml
さまざまな開発環境のデータベース名などのデータベースの詳細が含まれています。 Railsは、アンダースコア((_
)に続いて、アプリの名前に環境名を付けます。 環境データベース名はいつでも好きな名前に変更できます。
注:この時点で、変更できます config/database.yml
Railsがデータベースの作成に使用するPostgreSQLの役割を設定します。 前提条件Rubyon RailsアプリケーションでPostgreSQLを使用する方法に従い、パスワードで保護されたロールを作成した場合は、ステップ4のの手順に従うことができます。 macOSまたはUbuntu18.04。
前述のように、RailsはWebアプリケーションの開発を容易にするための多くのコマンドを提供します。 これには、データベースを操作するためのコマンドが含まれます。 create
, drop
、 と reset
. アプリケーションのデータベースを作成するには、ターミナルウィンドウで次のコマンドを実行します。
- rails db:create
このコマンドは、 development
と test
データベース、次の出力を生成します。
OutputCreated database 'rails_react_recipe_development'
Created database 'rails_react_recipe_test'
アプリケーションがデータベースに接続されたので、ターミナルウィンドウで次のコマンドを実行してアプリケーションを起動します。
- rails s --binding=127.0.0.1
The s
また server
コマンドはPumaを起動します。これは、デフォルトでRailsとともに配布されるWebサーバーであり、 --binding=127.0.0.1
サーバーを localhost
.
このコマンドを実行すると、コマンドプロンプトが消え、次の出力が表示されます。
Output=> Booting Puma
=> Rails 5.2.3 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.1 (ruby 2.6.3-p62), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:3000
Use Ctrl-C to stop
アプリケーションを表示するには、ブラウザウィンドウを開き、次の場所に移動します。 http://localhost:3000
. Railsのデフォルトのウェルカムページが表示されます。
これは、Railsアプリケーションが適切に設定されていることを意味します。
Webサーバーをいつでも停止するには、を押します。 CTRL+C
サーバーが実行されているターミナルウィンドウで。 さあ、今これを実行してください。 プーマからさようならのメッセージが届きます。
Output^C- Gracefully stopping, waiting for requests to finish
=== puma shutdown: 2019-07-31 14:21:24 -0400 ===
- Goodbye!
Exiting
その後、プロンプトが再度表示されます。
これで、食品レシピアプリケーションのデータベースが正常に設定されました。 次のステップでは、Reactフロントエンドをまとめるのに必要なすべての追加のJavaScript依存関係をインストールします。
ステップ3—フロントエンドの依存関係をインストールする
このステップでは、フードレシピアプリケーションのフロントエンドに必要なJavaScriptの依存関係をインストールします。 それらが含まれます:
- Reactルーター、Reactアプリケーションでナビゲーションを処理します。
- Bootstrap 、フロントエンドコンポーネントのスタイリング用。
- jQueryおよびPopper、Bootstrapを操作するため。
ターミナルウィンドウで次のコマンドを実行して、Yarnパッケージマネージャーでこれらのパッケージをインストールします。
- yarn add react-router-dom bootstrap jquery popper.js
このコマンドは、Yarnを使用して指定されたパッケージをインストールし、それらをに追加します。 package.json
ファイル。 これを確認するには、 package.json
プロジェクトのルートディレクトリにあるファイル:
- nano package.json
インストールされているパッケージが dependencies
鍵:
{
"name": "rails_react_recipe",
"private": true,
"dependencies": {
"@babel/preset-react": "^7.0.0",
"@rails/webpacker": "^4.0.7",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"bootstrap": "^4.3.1",
"jquery": "^3.4.1",
"popper.js": "^1.15.0",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.1"
},
"devDependencies": {
"webpack-dev-server": "^3.7.2"
}
}
アプリケーションにいくつかのフロントエンド依存関係をインストールしました。 次に、フードレシピアプリケーションのホームページを設定します。
ステップ4—ホームページの設定
必要なすべての依存関係をインストールしたら、このステップでアプリケーションのホームページを作成します。 ホームページは、ユーザーが最初にアプリケーションにアクセスしたときのランディングページとして機能します。
Railsは、アプリケーションのModel-View-Controllerアーキテクチャパターンに従います。 MVCパターンでは、コントローラーの目的は、特定の要求を受信し、それらを適切なモデルまたはビューに渡すことです。 現在、ルートURLがブラウザにロードされると、アプリケーションはRailsのウェルカムページを表示します。 これを変更するには、コントローラーを作成してホームページを表示し、ルートに一致させます。
Railsは controller
コントローラを作成するためのジェネレータ。 The controller
ジェネレーターは、一致するアクションとともにコントローラー名を受け取ります。 詳細については、Railsの公式ドキュメントをご覧ください。
このチュートリアルでは、コントローラーを呼び出します Homepage
. ターミナルウィンドウで次のコマンドを実行して、 index
アクション。
- rails g controller Homepage index
注: Linuxでは、エラーが発生した場合 FATAL: Listen error: unable to monitor directories for changes.
、これは、マシンが変更を監視できるファイル数のシステム制限によるものです。 次のコマンドを実行して修正します。
- echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
これにより、監視できるディレクトリの数が恒久的に増加します Listen
に 524288
. 同じコマンドを実行して置き換えることで、これを再度変更できます 524288
ご希望の番号で。
このコマンドを実行すると、次のファイルが生成されます。
- A
homepage_controller.rb
ホームページ関連のすべてのリクエストを受信するためのファイル。 このファイルには、index
コマンドで指定したアクション。 - A
homepage.js
に関連するJavaScriptの動作を追加するためのファイルHomepage
コントローラ。 - A
homepage.scss
に関連するスタイルを追加するためのファイルHomepage
コントローラ。 - A
homepage_helper.rb
に関連するヘルパーメソッドを追加するためのファイルHomepage
コントローラ。 - アン
index.html.erb
ホームページに関連するものをレンダリングするためのビューページであるファイル。
Railsコマンドを実行して作成されたこれらの新しいページとは別に、Railsは次の場所にあるルートファイルも更新します。 config/routes.rb
. それは追加します get
ルートルートとして変更するホームページのルート。
Railsのルートルートは、ユーザーがアプリケーションのルートURLにアクセスしたときに表示されるものを指定します。 この場合、ユーザーにホームページを表示してもらいます。 にあるルートファイルを開きます config/routes.rb
お気に入りのエディターで:
- nano config/routes.rb
このファイル内で、 get 'homepage/index'
と root 'homepage#index'
ファイルが次のようになるようにします。
Rails.application.routes.draw do
root 'homepage#index'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
この変更により、Railsは、アプリケーションのルートへのリクエストを index
のアクション Homepage
コントローラは、次に、にあるものをすべてレンダリングします index.html.erb
にあるファイル app/views/homepage/index.html.erb
ブラウザに。
これが機能していることを確認するには、アプリケーションを起動します。
- rails s --binding=127.0.0.1
ブラウザでアプリケーションを開くと、アプリケーションの新しいランディングページが表示されます。
アプリケーションが機能していることを確認したら、を押します CTRL+C
サーバーを停止します。
次に、 ~/rails_react_recipe/app/views/homepage/index.html.erb
ファイルを作成し、ファイル内のコードを削除してから、ファイルを空として保存します。 これを行うことにより、あなたはの内容を確認します index.html.erb
フロントエンドのReactレンダリングに干渉しないでください。
アプリケーションのホームページを設定したので、次のセクションに移動して、Reactを使用するようにアプリケーションのフロントエンドを構成します。
ステップ5—RailsフロントエンドとしてのReactの構成
このステップでは、テンプレートエンジンではなく、アプリケーションのフロントエンドでReactを使用するようにRailsを構成します。 これにより、Reactレンダリングを利用して、より視覚的に魅力的なホームページを作成できます。
Railsは、 Webpacker gem を使用して、すべてのJavaScriptコードをpacksにバンドルします。 これらは、packsディレクトリにあります。 app/javascript/packs
. これらのパックは、Railsビューでリンクできます。 javascript_pack_tag
ヘルパー、およびパックにインポートされたスタイルシートをリンクすることができます stylesheet_pack_tag
ヘルパー。 React環境へのエントリポイントを作成するには、これらのパックの1つをアプリケーションレイアウトに追加します。
まず、名前を変更します ~/rails_react_recipe/app/javascript/packs/hello_react.jsx
にファイルする ~/rails_react_recipe/app/javascript/packs/Index.jsx
.
- mv ~/rails_react_recipe/app/javascript/packs/hello_react.jsx ~/rails_react_recipe/app/javascript/packs/Index.jsx
ファイルの名前を変更した後、 application.html.erb
、アプリケーションレイアウトファイル:
- nano ~/rails_react_recipe/app/views/layouts/application.html.erb
アプリケーションレイアウトファイルのheadタグの最後に、次の強調表示されたコード行を追加します。
<!DOCTYPE html>
<html>
<head>
<title>RailsReactRecipe</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<%= javascript_pack_tag 'Index' %>
</head>
<body>
<%= yield %>
</body>
</html>
アプリケーションのヘッダーにJavaScriptパックを追加すると、すべてのJavaScriptコードが利用可能になり、 Index.jsx
アプリを実行するたびにページ上のファイル。 JavaScriptパックに加えて、 meta
viewport
アプリケーションのページのサイズとスケーリングを制御するタグ。
ファイルを保存して終了します。
エントリファイルがページにロードされたので、ホームページのReactコンポーネントを作成します。 作成することから始めます components
のディレクトリ app/javascript
ディレクトリ:
- mkdir ~/rails_react_recipe/app/javascript/components
The components
ディレクトリには、アプリケーション内の他のReactコンポーネントとともに、ホームページのコンポーネントが格納されます。 ホームページには、すべてのレシピを表示するためのテキストと召喚状のボタンが含まれます。
エディターで、 Home.jsx
のファイル components
ディレクトリ:
- nano ~/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
ReactRouterのコンポーネント。 The Link
コンポーネントは、あるページから別のページに移動するためのハイパーリンクを作成します。 次に、Bootstrapクラスでスタイル設定された、ホームページ用のマークアップ言語を含む機能コンポーネントを作成してエクスポートしました。
あなたと Home
コンポーネントが配置されたら、ReactRouterを使用してルーティングを設定します。 作成する routes
のディレクトリ app/javascript
ディレクトリ:
- mkdir ~/rails_react_recipe/app/javascript/routes
The routes
ディレクトリには、対応するコンポーネントを含むいくつかのルートが含まれます。 指定されたルートがロードされるたびに、対応するコンポーネントがブラウザにレンダリングされます。
の中に routes
ディレクトリ、作成 Index.jsx
ファイル:
- nano ~/rails_react_recipe/app/javascript/routes/Index.jsx
次のコードを追加します。
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "../components/Home";
export default (
<Router>
<Switch>
<Route path="/" exact component={Home} />
</Switch>
</Router>
);
これで Index.jsx
ルートファイル、いくつかのモジュールをインポートしました: React
Reactを使用できるようにするモジュール、および BrowserRouter
, Route
、 と Switch
React Routerのモジュール。これらを組み合わせることで、あるルートから別のルートに移動できます。 最後に、 Home
コンポーネント。リクエストがルートと一致するたびにレンダリングされます(/
)ルート。 アプリケーションにさらにページを追加する場合は常に、このファイルでルートを宣言し、そのページにレンダリングするコンポーネントと一致させるだけです。
ファイルを保存して終了します。
これで、ReactRouterを使用してルーティングを正常に設定できました。 Reactが利用可能なルートを認識して使用するには、アプリケーションへのエントリポイントでルートが利用可能である必要があります。 これを実現するには、Reactがエントリファイルでレンダリングするコンポーネントでルートをレンダリングします。
作成する App.jsx
のファイル app/javascript/components
ディレクトリ:
- nano ~/rails_react_recipe/app/javascript/components/App.jsx
次のコードをに追加します App.jsx
ファイル:
import React from "react";
import Routes from "../routes/Index";
export default props => <>{Routes}</>;
の中に App.jsx
ファイル、Reactと作成したルートファイルをインポートしました。 次に、フラグメント内のルートをレンダリングするコンポーネントをエクスポートしました。 このコンポーネントは、アプリケーションのエントリポイントでレンダリングされるため、アプリケーションがロードされるたびにルートが使用可能になります。
今、あなたはあなたを持っています App.jsx
セットアップしたら、エントリファイルにレンダリングします。 エントリを開く Index.jsx
ファイル:
- nano ~/rails_react_recipe/app/javascript/packs/Index.jsx
そこでのコードを次のコードに置き換えます。
import React from "react";
import { render } from "react-dom";
import 'bootstrap/dist/css/bootstrap.min.css';
import $ from 'jquery';
import Popper from 'popper.js';
import 'bootstrap/dist/js/bootstrap.bundle.min';
import App from "../components/App";
document.addEventListener("DOMContentLoaded", () => {
render(
<App />,
document.body.appendChild(document.createElement("div"))
);
});
このコードスニペットでは、React、ReactDOM、Bootstrap、jQuery、Popper.jsからのレンダリングメソッド、および App
成分。 ReactDOMのrenderメソッドを使用して、 App
のコンポーネント div
ページの本文に追加された要素。 アプリケーションがロードされるたびに、Reactはコンテンツをレンダリングします App
内部のコンポーネント div
ページ上の要素。
ファイルを保存して終了します。
最後に、いくつかのCSSスタイルをホームページに追加します。
あなたの application.css
あなたの中で ~/rails_react_recipe/app/assets/stylesheets
ディレクトリ:
- nano ~/rails_react_recipe/app/assets/stylesheets/application.css
次に、内容を置き換えます application.css
次のコードでファイルします。
.bg_primary-color {
background-color: #FFFFFF;
}
.primary-color {
background-color: #FFFFFF;
}
.bg_secondary-color {
background-color: #293241;
}
.secondary-color {
color: #293241;
}
.custom-button.btn {
background-color: #293241;
color: #FFF;
border: none;
}
.custom-button.btn:hover {
color: #FFF !important;
border: none;
}
.hero {
width: 100vw;
height: 50vh;
}
.hero img {
object-fit: cover;
object-position: top;
height: 100%;
width: 100%;
}
.overlay {
height: 100%;
width: 100%;
opacity: 0.4;
}
これにより、ヒーロー画像のフレームワーク、または後で追加するWebサイトのフロントページに大きなWebバナーが作成されます。 さらに、これにより、ユーザーがアプリケーションに入るときに使用するボタンのスタイルが設定されます。
CSSスタイルを配置したら、ファイルを保存して終了します。 次に、アプリケーションのWebサーバーを再起動してから、ブラウザーにアプリケーションを再ロードします。 新しいホームページが表示されます。
このステップでは、フロントエンドとしてReactを使用するようにアプリケーションを構成しました。 次のセクションでは、レシピの作成、読み取り、更新、および削除を可能にするモデルとコントローラーを作成します。
ステップ6—レシピコントローラーとモデルの作成
アプリケーションのReactフロントエンドを設定したので、このステップでは、レシピモデルとコントローラーを作成します。 レシピモデルは、ユーザーのレシピに関する情報を保持するデータベーステーブルを表し、コントローラーはレシピの作成、読み取り、更新、または削除の要求を受信して処理します。 ユーザーがレシピを要求すると、レシピコントローラーはこの要求を受信し、それをレシピモデルに渡します。レシピモデルは、要求されたデータをデータベースから取得します。 次に、モデルはレシピデータをコントローラーへの応答として返します。 最後に、この情報がブラウザに表示されます。
を使用してレシピモデルを作成することから始めます generate model
Railsによって提供されるサブコマンドであり、モデルの名前とその列およびデータ型を指定します。 ターミナルウィンドウで次のコマンドを実行して、 Recipe
モデル:
- rails generate model Recipe name:string ingredients:text instruction:text image:string
上記のコマンドは、Railsに作成するように指示します Recipe
と一緒にモデル name
タイプの列 string
、 ingredients
と instruction
タイプの列 text
、 と image
タイプの列 string
. このチュートリアルでは、モデルに名前を付けています Recipe
、慣例により、Railsのモデルは単数形の名前を使用し、対応するデータベーステーブルは複数形の名前を使用するためです。
の実行 generate model
コマンドは2つのファイルを作成します:
- A
recipe.rb
モデルに関連するすべてのロジックを保持するファイル。 - A
20190407161357_create_recipes.rb
ファイル(ファイルの先頭の番号は、コマンドを実行した日付によって異なる場合があります)。 これは、データベース構造を作成するための命令を含む移行ファイルです。
次に、レシピモデルファイルを編集して、有効なデータのみがデータベースに保存されるようにします。 これは、モデルにデータベース検証を追加することで実現できます。 次の場所にあるレシピモデルを開きます app/models/recipe.rb
:
- nano ~/rails_react_recipe/app/models/recipe.rb
次の強調表示されたコード行をファイルに追加します。
class Recipe < ApplicationRecord
validates :name, presence: true
validates :ingredients, presence: true
validates :instruction, presence: true
end
このコードでは、の存在をチェックするモデル検証を追加しました name
, ingredients
、 と instruction
分野。 これらの3つのフィールドが存在しない場合、レシピは無効であり、データベースに保存されません。
ファイルを保存して終了します。
Railsが作成する recipes
データベース内のテーブルでは、 migration を実行する必要があります。これは、Railsではプログラムでデータベースに変更を加える方法です。 セットアップしたデータベースで移行が機能することを確認するには、に変更を加える必要があります。 20190407161357_create_recipes.rb
ファイル。
このファイルをエディターで開きます。
- nano ~/rails_react_recipe/db/migrate/20190407161357_create_recipes.rb
次の強調表示された行を追加して、ファイルが次のようになるようにします。
class CreateRecipes < ActiveRecord::Migration[5.2]
def change
create_table :recipes do |t|
t.string :name, null: false
t.text :ingredients, null: false
t.text :instruction, null: false
t.string :image, default: 'https://raw.githubusercontent.com/do-community/react_rails_recipe/master/app/assets/images/Sammy_Meal.jpg'
t.timestamps
end
end
end
この移行ファイルには、Rubyクラスが含まれています。 change
メソッド、およびと呼ばれるテーブルを作成するコマンド recipes
列とそのデータ型とともに。 また、更新しました 20190407161357_create_recipes.rb
とともに NOT NULL
の制約 name
, ingredients
、 と instruction
追加することによる列 null: false
、データベースを変更する前に、これらの列に値があることを確認してください。 最後に、画像列にデフォルトの画像URLを追加しました。 別の画像を使用する場合は、これを別のURLにすることができます。
これらの変更を加えて、ファイルを保存して終了します。 これで、移行を実行して実際にテーブルを作成する準備が整いました。 ターミナルウィンドウで、次のコマンドを実行します。
- rails db:migrate
ここでは、データベース移行コマンドを使用しました。このコマンドは、移行ファイルの命令を実行します。 コマンドが正常に実行されると、次のような出力が表示されます。
Output== 20190407161357 CreateRecipes: migrating ====================================
-- create_table(:recipes)
-> 0.0140s
== 20190407161357 CreateRecipes: migrated (0.0141s) ===========================
レシピモデルを配置したら、レシピコントローラーを作成し、レシピを作成、読み取り、削除するためのロジックを追加します。 ターミナルウィンドウで、次のコマンドを実行します。
- rails generate controller api/v1/Recipes index create show destroy -j=false -y=false --skip-template-engine --no-helper
このコマンドでは、 Recipes
のコントローラー api/v1
ディレクトリと index
, create
, show
、 と destroy
アクション。 The index
アクションは、すべてのレシピのフェッチを処理します。 create
アクションは、新しいレシピの作成を担当します。 show
アクションは単一のレシピをフェッチし、 destroy
アクションは、レシピを削除するためのロジックを保持します。
また、コントローラーをより軽量にするために、次のようないくつかのフラグを渡しました。
-j=false
これは、関連するJavaScriptファイルの生成をスキップするようにRailsに指示します。-y=false
これは、関連するスタイルシートファイルの生成をスキップするようにRailsに指示します。--skip-template-engine
、Reactがフロントエンドのニーズを処理しているため、Railsビューファイルの生成をスキップするようにRailsに指示します。--no-helper
、これは、コントローラーのヘルパーファイルの生成をスキップするようにRailsに指示します。
コマンドを実行すると、ルートファイルも更新され、 Recipes
コントローラ。 これらのルートを使用するには、 config/routes.rb
ファイル。
テキストエディタでルートファイルを開きます。
- nano ~/rails_react_recipe/config/routes.rb
開いたら、次のコードのように更新し、強調表示された行を変更または追加します。
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
get 'recipes/index'
post 'recipes/create'
get '/show/:id', to: 'recipes#show'
delete '/destroy/:id', to: 'recipes#destroy'
end
end
root 'homepage#index'
get '/*path' => 'homepage#index'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
このルートファイルでは、のHTTP動詞を変更しました create
と destroy
それができるようにルート post
と delete
データ。 のルートも変更しました show
と destroy
追加することによるアクション :id
ルートへのパラメータ。 :id
読み取りまたは削除するレシピの識別番号を保持します。
また、キャッチオールルートを追加しました get '/*path'
これにより、既存のルートと一致しない他のリクエストが index
のアクション homepage
コントローラ。 このように、フロントエンドのルーティングは、レシピの作成、読み取り、または削除に関連しないリクエストを処理します。
ファイルを保存して終了します。
アプリケーションで使用可能なルートのリストを表示するには、ターミナルウィンドウで次のコマンドを実行します。
- rails routes
このコマンドを実行すると、プロジェクトのURIパターン、動詞、および一致するコントローラーまたはアクションのリストが表示されます。
次に、すべてのレシピを一度に取得するためのロジックを追加します。 RailsはActiveRecordライブラリを使用して、このようなデータベース関連のタスクを処理します。 ActiveRecordは、クラスをリレーショナルデータベーステーブルに接続し、それらを操作するための豊富なAPIを提供します。
すべてのレシピを取得するには、ActiveRecordを使用してレシピテーブルをクエリし、データベースに存在するすべてのレシピをフェッチします。
を開きます recipes_controller.rb
次のコマンドでファイルします。
- nano ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
次の強調表示されたコード行をレシピコントローラーに追加します。
class Api::V1::RecipesController < ApplicationController
def index
recipe = Recipe.all.order(created_at: :desc)
render json: recipe
end
def create
end
def show
end
def destroy
end
end
あなたの中で index
アクション、を使用して all
ActiveRecordが提供するメソッドを使用すると、データベース内のすべてのレシピを取得できます。 を使用して order
メソッドでは、作成日から降順で並べ替えます。 このようにして、最初に最新のレシピを入手します。 最後に、レシピのリストをJSON応答として送信します。 render
.
次に、新しいレシピを作成するためのロジックを追加します。 すべてのレシピをフェッチする場合と同様に、提供されたレシピの詳細を検証して保存するためにActiveRecordに依存します。 次の強調表示されたコード行でレシピコントローラーを更新します。
class Api::V1::RecipesController < ApplicationController
def index
recipe = Recipe.all.order(created_at: :desc)
render json: recipe
end
def create
recipe = Recipe.create!(recipe_params)
if recipe
render json: recipe
else
render json: recipe.errors
end
end
def show
end
def destroy
end
private
def recipe_params
params.permit(:name, :image, :ingredients, :instruction)
end
end
の中に create
アクション、あなたはActiveRecordのを使用します create
新しいレシピを作成する方法。 The create
メソッドには、モデルに提供されたすべてのコントローラーパラメーターを一度に割り当てる機能があります。 これにより、レコードの作成が容易になりますが、悪用される可能性もあります。 これは、強力なパラメーターとして知られるRailsが提供する機能を使用することで防ぐことができます。 このように、ホワイトリストに登録されていない限り、パラメータを割り当てることはできません。 あなたのコードでは、あなたは recipe_params
のパラメータ create
方法。 The recipe_params
は private
間違ったコンテンツや悪意のあるコンテンツがデータベースに侵入するのを防ぐために、コントローラーパラメーターをホワイトリストに登録した方法。 この場合、あなたは許可しています name
, image
, ingredients
、 と instruction
を有効に使用するためのパラメータ create
方法。
これで、レシピコントローラーがレシピを読み取って作成できるようになりました。 残っているのは、単一のレシピを読み取って削除するためのロジックです。 次のコードでレシピコントローラーを更新します。
class Api::V1::RecipesController < ApplicationController
def index
recipe = Recipe.all.order(created_at: :desc)
render json: recipe
end
def create
recipe = Recipe.create!(recipe_params)
if recipe
render json: recipe
else
render json: recipe.errors
end
end
def show
if recipe
render json: recipe
else
render json: recipe.errors
end
end
def destroy
recipe&.destroy
render json: { message: 'Recipe deleted!' }
end
private
def recipe_params
params.permit(:name, :image, :ingredients, :instruction)
end
def recipe
@recipe ||= Recipe.find(params[:id])
end
end
新しいコード行で、プライベートを作成しました recipe
方法。 The recipe
メソッドはActiveRecordを使用します find
そのレシピを見つける方法 id
一致します id
で提供される params
それをインスタンス変数に割り当てます @recipe
. の中に show
アクション、レシピがによって返されるかどうかを確認しました recipe
メソッドを使用してJSON応答として送信するか、そうでない場合はエラーを送信しました。
の中に destroy
アクション、Rubyの安全なナビゲーション演算子を使用して同様のことをしました &.
、回避します nil
メソッドを呼び出すときのエラー。 これにより、レシピが存在する場合にのみレシピを削除してから、応答としてメッセージを送信できます。
これで、これらの変更が完了しました。 recipes_controller.rb
、ファイルを保存してテキストエディタを終了します。
このステップでは、レシピのモデルとコントローラーを作成しました。 バックエンドでレシピを操作するために必要なすべてのロジックを作成しました。 次のセクションでは、レシピを表示するためのコンポーネントを作成します。
ステップ7—レシピの表示
このセクションでは、レシピを表示するためのコンポーネントを作成します。 最初に、既存のすべてのレシピを表示できるページを作成し、次に別のページを作成して個々のレシピを表示します。
まず、すべてのレシピを表示するページを作成します。 ただし、データベースは現在空であるため、これを行う前に、使用するレシピが必要です。 Railsは、アプリケーションのシードデータを作成する機会を提供します。
シードファイルを開きます seeds.rb
編集するには:
- nano ~/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に9つのレシピを作成するように指示しています。 name
, ingredients
、 と instruction
. ファイルを保存して終了します。
このデータをデータベースにシードするには、ターミナルウィンドウで次のコマンドを実行します。
- rails db:seed
このコマンドを実行すると、データベースに9つのレシピが追加されます。 これで、それらをフェッチしてフロントエンドでレンダリングできます。
すべてのレシピを表示するコンポーネントは、 index
でのアクション RecipesController
すべてのレシピのリストを取得します。 これらのレシピは、ページのカードに表示されます。
作成する Recipes.jsx
のファイル app/javascript/components
ディレクトリ:
- nano ~/rails_react_recipe/app/javascript/components/Recipes.jsx
ファイルが開いたら、次の行を追加して、ReactモジュールとLinkモジュールをファイルにインポートします。
import React from "react";
import { Link } from "react-router-dom";
次に、 Recipes
を拡張するクラス React.Component
クラス。 次の強調表示されたコードを追加して、拡張するReactコンポーネントを作成します React.Component
:
import React from "react";
import { Link } from "react-router-dom";
class Recipes extends React.Component {
constructor(props) {
super(props);
this.state = {
recipes: []
};
}
}
export default Recipes;
コンストラクター内で、レシピの状態を保持する state オブジェクトを初期化しています。これは、初期化時に空の配列です([]
).
次に、 componentDidMount
Recipeクラスのメソッド。 componentDidMount メソッドは、コンポーネントがマウントされた直後に呼び出されるReactライフサイクルメソッドです。 このライフサイクルメソッドでは、すべてのレシピをフェッチするための呼び出しを行います。 これを行うには、次の行を追加します。
import React from "react";
import { Link } from "react-router-dom";
class Recipes extends React.Component {
constructor(props) {
super(props);
this.state = {
recipes: []
};
}
componentDidMount() {
const url = "/api/v1/recipes/index";
fetch(url)
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(response => this.setState({ recipes: response }))
.catch(() => this.props.history.push("/"));
}
}
export default Recipes;
あなたの中で componentDidMount
メソッドでは、 FetchAPIを使用してすべてのレシピをフェッチするHTTP呼び出しを行いました。 応答が成功すると、アプリケーションはレシピの配列をレシピ状態に保存します。 エラーが発生した場合は、ユーザーをホームページにリダイレクトします。
最後に、 render
のメソッド Recipe
クラス。 render メソッドは、コンポーネントがレンダリングされるときに評価され、ブラウザーページに表示されるReact要素を保持します。 この場合、 render
メソッドは、コンポーネントの状態からレシピのカードをレンダリングします。 次の強調表示された行をに追加します Recipes.jsx
:
import React from "react";
import { Link } from "react-router-dom";
class Recipes extends React.Component {
constructor(props) {
super(props);
this.state = {
recipes: []
};
}
componentDidMount() {
const url = "/api/v1/recipes/index";
fetch(url)
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(response => this.setState({ recipes: response }))
.catch(() => this.props.history.push("/"));
}
render() {
const { recipes } = this.state;
const allRecipes = recipes.map((recipe, index) => (
<div key={index} className="col-md-6 col-lg-4">
<div className="card mb-4">
<img
src={recipe.image}
className="card-img-top"
alt={`${recipe.name} image`}
/>
<div className="card-body">
<h5 className="card-title">{recipe.name}</h5>
<Link to={`/recipe/${recipe.id}`} className="btn custom-button">
View Recipe
</Link>
</div>
</div>
</div>
));
const noRecipe = (
<div className="vw-100 vh-50 d-flex align-items-center justify-content-center">
<h4>
No recipes yet. Why not <Link to="/new_recipe">create one</Link>
</h4>
</div>
);
return (
<>
<section className="jumbotron jumbotron-fluid text-center">
<div className="container py-5">
<h1 className="display-4">Recipes for every occasion</h1>
<p className="lead text-muted">
We’ve pulled together our most popular recipes, our latest
additions, and our editor’s picks, so there’s sure to be something
tempting for you to try.
</p>
</div>
</section>
<div className="py-5">
<main className="container">
<div className="text-right mb-3">
<Link to="/recipe" className="btn custom-button">
Create New Recipe
</Link>
</div>
<div className="row">
{recipes.length > 0 ? allRecipes : noRecipe}
</div>
<Link to="/" className="btn btn-link">
Home
</Link>
</main>
</div>
</>
);
}
}
export default Recipes;
保存して終了 Recipes.jsx
.
すべてのレシピを表示するコンポーネントを作成したので、次のステップはそのルートを作成することです。 にあるフロントエンドルートファイルを開きます app/javascript/routes/Index.jsx
:
- nano app/javascript/routes/Index.jsx
次の強調表示された行をファイルに追加します。
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
export default (
<Router>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/recipes" exact component={Recipes} />
</Switch>
</Router>
);
ファイルを保存して終了します。
この時点で、コードが正しく機能していることを確認することをお勧めします。 以前と同じように、次のコマンドを使用してサーバーを起動します。
- rails s --binding=127.0.0.1
先に進み、ブラウザでアプリを開きます。 ホームページのレシピの表示ボタンをクリックすると、シードレシピが表示されます。
使用する CTRL+C
ターミナルウィンドウでサーバーを停止し、プロンプトを元に戻します。
アプリケーションに存在するすべてのレシピを表示できるようになったので、次に、個々のレシピを表示するための2番目のコンポーネントを作成します。 作成する Recipe.jsx
のファイル app/javascript/components
ディレクトリ:
- nano app/javascript/components/Recipe.jsx
と同じように Recipes
コンポーネントの場合、次の行を追加してReactモジュールとLinkモジュールをインポートします。
import React from "react";
import { Link } from "react-router-dom";
次に、 Recipe
拡張するクラス React.Component
強調表示されたコード行を追加してクラスを作成します。
import React from "react";
import { Link } from "react-router-dom";
class Recipe extends React.Component {
constructor(props) {
super(props);
this.state = { recipe: { ingredients: "" } };
this.addHtmlEntities = this.addHtmlEntities.bind(this);
}
}
export default Recipe;
あなたのように Recipes
コンポーネント、コンストラクターで、レシピの状態を保持する状態オブジェクトを初期化しました。 あなたはまた、 addHtmlEntities
方法 this
そのため、コンポーネント内でアクセスできます。 The addHtmlEntities
メソッドは、コンポーネント内の文字エンティティをHTMLエンティティに置き換えるために使用されます。
特定のレシピを見つけるために、アプリケーションには id
レシピの。 これはあなたの Recipe
コンポーネントは、 id
param
. これには、 props
コンポーネントに渡されます。
次に、 componentDidMount
アクセスする方法 id
param
から match
の鍵 props
物体。 あなたが取得したら id
、次に、レシピをフェッチするためのHTTPリクエストを作成します。 次の強調表示された行をファイルに追加します。
import React from "react";
import { Link } from "react-router-dom";
class Recipe extends React.Component {
constructor(props) {
super(props);
this.state = { recipe: { ingredients: "" } };
this.addHtmlEntities = this.addHtmlEntities.bind(this);
}
componentDidMount() {
const {
match: {
params: { id }
}
} = this.props;
const url = `/api/v1/show/${id}`;
fetch(url)
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(response => this.setState({ recipe: response }))
.catch(() => this.props.history.push("/recipes"));
}
}
export default Recipe;
の中に componentDidMount
メソッド、オブジェクトの破壊を使用すると、 id
param
から props
オブジェクト、次にFetch APIを使用して、HTTPリクエストを作成し、 id
を使用してコンポーネントの状態に保存します setState
方法。 レシピが存在しない場合、アプリはユーザーをレシピページにリダイレクトします。
次に、 addHtmlEntities
メソッド。文字列を受け取り、エスケープされたすべての開き角かっこと閉じ角かっこをHTMLエンティティに置き換えます。 これは、レシピ命令に保存されたエスケープ文字を変換するのに役立ちます。
import React from "react";
import { Link } from "react-router-dom";
class Recipe extends React.Component {
constructor(props) {
super(props);
this.state = { recipe: { ingredients: "" } };
this.addHtmlEntities = this.addHtmlEntities.bind(this);
}
componentDidMount() {
const {
match: {
params: { id }
}
} = this.props;
const url = `/api/v1/show/${id}`;
fetch(url)
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(response => this.setState({ recipe: response }))
.catch(() => this.props.history.push("/recipes"));
}
addHtmlEntities(str) {
return String(str)
.replace(/</g, "<")
.replace(/>/g, ">");
}
}
export default Recipe;
最後に、 render
状態からレシピを取得し、それをページにレンダリングするメソッド。 これを行うには、次の強調表示された行を追加します。
import React from "react";
import { Link } from "react-router-dom";
class Recipe extends React.Component {
constructor(props) {
super(props);
this.state = { recipe: { ingredients: "" } };
this.addHtmlEntities = this.addHtmlEntities.bind(this);
}
componentDidMount() {
const {
match: {
params: { id }
}
} = this.props;
const url = `/api/v1/show/${id}`;
fetch(url)
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(response => this.setState({ recipe: response }))
.catch(() => this.props.history.push("/recipes"));
}
addHtmlEntities(str) {
return String(str)
.replace(/</g, "<")
.replace(/>/g, ">");
}
render() {
const { recipe } = this.state;
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
const recipeInstruction = this.addHtmlEntities(recipe.instruction);
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button type="button" className="btn btn-danger">
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
}
}
export default Recipe;
これで render
この方法では、コンマで区切られた材料を配列に分割してその上にマッピングし、材料のリストを作成します。 材料がない場合、アプリは材料がありませんというメッセージを表示します。 また、レシピ画像をヒーロー画像として表示し、レシピ指示の横にレシピ削除ボタンを追加し、レシピページにリンクするボタンを追加します。
ファイルを保存して終了します。
表示するには Recipe
ページ上のコンポーネントをルートファイルに追加します。 ルートファイルを開いて編集します。
- nano app/javascript/routes/Index.jsx
次に、次の強調表示された行をファイルに追加します。
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";
export default (
<Router>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/recipes" exact component={Recipes} />
<Route path="/recipe/:id" exact component={Recipe} />
</Switch>
</Router>
);
このルートファイルでは、 Recipe
コンポーネントとそのルートを追加しました。 そのルートには :id
param
に置き換えられます id
あなたが見たいレシピの。
使用 rails s
サーバーを再起動するコマンドを実行してから、 http://localhost:3000
ブラウザで。 レシピの表示ボタンをクリックして、レシピページに移動します。 レシピページで、レシピの表示ボタンをクリックしてレシピを表示します。 データベースからのデータが入力されたページが表示されます。
このセクションでは、データベースに9つのレシピを追加し、これらのレシピを個別におよびコレクションとして表示するためのコンポーネントを作成しました。 次のセクションでは、レシピを作成するためのコンポーネントを追加します。
ステップ8—レシピの作成
使用可能な食品レシピアプリケーションを作成するための次のステップは、新しいレシピを作成する機能です。 このステップでは、レシピを作成するためのコンポーネントを作成します。 このコンポーネントには、ユーザーから必要なレシピの詳細を収集するためのフォームが含まれ、 create
でのアクション Recipe
レシピデータを保存するコントローラー。
作成する NewRecipe.jsx
のファイル app/javascript/components
ディレクトリ:
- nano app/javascript/components/NewRecipe.jsx
新しいファイルで、これまでに他のコンポーネントで使用したReactモジュールとLinkモジュールをインポートします。
import React from "react";
import { Link } from "react-router-dom";
次に、 NewRecipe
拡張するクラス React.Component
クラス。 次の強調表示されたコードを追加して、拡張するReactコンポーネントを作成します react.Component
:
import React from "react";
import { Link } from "react-router-dom";
class NewRecipe extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "",
ingredients: "",
instruction: ""
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
}
}
export default NewRecipe;
の中に NewRecipe
コンポーネントのコンストラクター、状態オブジェクトを空で初期化しました name
, ingredients
、 と instruction
田畑。 これらは、有効なレシピを作成するために必要なフィールドです。 また、3つの方法があります。 onChange
, onSubmit
、 と stripHtmlEntities
、あなたがバインドした this
. これらのメソッドは、状態の更新、フォームの送信、および特殊文字の変換( <
)エスケープ/エンコードされた値( <
)、 それぞれ。
次に、を作成します stripHtmlEntities
強調表示された行をに追加することにより、メソッド自体 NewRecipe
成分:
class NewRecipe extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "",
ingredients: "",
instruction: ""
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
}
stripHtmlEntities(str) {
return String(str)
.replace(/</g, "<")
.replace(/>/g, ">");
}
}
export default NewRecipe;
の中に stripHtmlEntities
メソッド、あなたは置き換えています <
と >
エスケープされた値を持つ文字。 このようにして、生のHTMLをデータベースに保存しません。
次に、 onChange
と onSubmit
へのメソッド NewRecipe
フォームの編集と送信を処理するコンポーネント:
class NewRecipe extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "",
ingredients: "",
instruction: ""
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
}
stripHtmlEntities(str) {
return String(str)
.replace(/</g, "<")
.replace(/>/g, ">");
}
onChange(event) {
this.setState({ [event.target.name]: event.target.value });
}
onSubmit(event) {
event.preventDefault();
const url = "/api/v1/recipes/create";
const { name, ingredients, instruction } = this.state;
if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
return;
const body = {
name,
ingredients,
instruction: instruction.replace(/\n/g, "<br> <br>")
};
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "POST",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json"
},
body: JSON.stringify(body)
})
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(response => this.props.history.push(`/recipe/${response.id}`))
.catch(error => console.log(error.message));
}
}
export default NewRecipe;
の中に onChange
この方法では、ES6 計算されたプロパティ名を使用して、すべてのユーザー入力の値を、州内の対応するキーに設定しました。 の中に onSubmit
メソッドでは、必要な入力がどれも空でないことを確認しました。 次に、新しいレシピを作成するためにレシピコントローラーに必要なパラメーターを含むオブジェクトを作成します。 正規表現を使用して、命令内のすべての改行文字をブレークタグに置き換え、ユーザーが入力したテキスト形式を保持できるようにします。
クロスサイトリクエストフォージェリ(CSRF)攻撃から保護するために、RailsはCSRFセキュリティトークンをHTMLドキュメントに添付します。 このトークンは、GET
リクエストが行われます。 とともに token
上記のコードで定数である場合、アプリケーションはサーバー上のトークンを検証し、セキュリティトークンが期待されるものと一致しない場合は例外をスローします。 の中に onSubmit
メソッドでは、アプリケーションはRailsによってHTMLドキュメントに埋め込まれた CSRFトークンを取得し、JSON文字列を使用してHTTPリクエストを作成します。 レシピが正常に作成されると、アプリケーションはユーザーをレシピページにリダイレクトし、そこで新しく作成されたレシピを表示できます。
最後に、 render
ユーザーが作成したいレシピの詳細を入力するためのフォームをユーザーがレンダリングするメソッド:
class NewRecipe extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "",
ingredients: "",
instruction: ""
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
}
stripHtmlEntities(str) {
return String(str)
.replace(/</g, "<")
.replace(/>/g, ">");
}
onChange(event) {
this.setState({ [event.target.name]: event.target.value });
}
onSubmit(event) {
event.preventDefault();
const url = "/api/v1/recipes/create";
const { name, ingredients, instruction } = this.state;
if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
return;
const body = {
name,
ingredients,
instruction: instruction.replace(/\n/g, "<br> <br>")
};
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "POST",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json"
},
body: JSON.stringify(body)
})
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(response => this.props.history.push(`/recipe/${response.id}`))
.catch(error => console.log(error.message));
}
render() {
return (
<div className="container mt-5">
<div className="row">
<div className="col-sm-12 col-lg-6 offset-lg-3">
<h1 className="font-weight-normal mb-5">
Add a new recipe to our awesome recipe collection.
</h1>
<form onSubmit={this.onSubmit}>
<div className="form-group">
<label htmlFor="recipeName">Recipe name</label>
<input
type="text"
name="name"
id="recipeName"
className="form-control"
required
onChange={this.onChange}
/>
</div>
<div className="form-group">
<label htmlFor="recipeIngredients">Ingredients</label>
<input
type="text"
name="ingredients"
id="recipeIngredients"
className="form-control"
required
onChange={this.onChange}
/>
<small id="ingredientsHelp" className="form-text text-muted">
Separate each ingredient with a comma.
</small>
</div>
<label htmlFor="instruction">Preparation Instructions</label>
<textarea
className="form-control"
id="instruction"
name="instruction"
rows="5"
required
onChange={this.onChange}
/>
<button type="submit" className="btn custom-button mt-3">
Create Recipe
</button>
<Link to="/recipes" className="btn btn-link mt-3">
Back to recipes
</Link>
</form>
</div>
</div>
</div>
);
}
}
export default NewRecipe;
renderメソッドには、3つの入力フィールドを含むフォームがあります。 1つは recipeName
, recipeIngredients
、 と instruction
. 各入力フィールドには onChange
を呼び出すイベントハンドラー onChange
方法。 また、 onSubmit
を呼び出す送信ボタンのイベントハンドラー onSubmit
次にフォームデータを送信するメソッド。
ファイルを保存して終了します。
ブラウザでこのコンポーネントにアクセスするには、ルートファイルをそのルートで更新します。
- nano app/javascript/routes/Index.jsx
ルートファイルを更新して、次の強調表示された行を含めます。
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";
import NewRecipe from "../components/NewRecipe";
export default (
<Router>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/recipes" exact component={Recipes} />
<Route path="/recipe/:id" exact component={Recipe} />
<Route path="/recipe" exact component={NewRecipe} />
</Switch>
</Router>
);
ルートを設定したら、ファイルを保存して終了します。 開発サーバーを再起動して、 http://localhost:3000
ブラウザで。 レシピページに移動し、新しいレシピの作成ボタンをクリックします。 データベースにレシピを追加するためのフォームを含むページがあります。
必要なレシピの詳細を入力し、レシピの作成ボタンをクリックします。 ページに新しく作成されたレシピが表示されます。
このステップでは、レシピを作成する機能を追加することで、食品レシピアプリケーションに命を吹き込みました。 次のステップでは、レシピを削除する機能を追加します。
ステップ9—レシピを削除する
このセクションでは、レシピを削除できるようにレシピコンポーネントを変更します。
レシピページの削除ボタンをクリックすると、アプリケーションはデータベースからレシピを削除するリクエストを送信します。 これを行うには、 Recipe.jsx
ファイル:
- nano app/javascript/components/Recipe.jsx
のコンストラクターで Recipe
コンポーネント、バインド this
に deleteRecipe
方法:
class Recipe extends React.Component {
constructor(props) {
super(props);
this.state = { recipe: { ingredients: "" } };
this.addHtmlEntities = this.addHtmlEntities.bind(this);
this.deleteRecipe = this.deleteRecipe.bind(this);
}
...
次に、 deleteRecipe
レシピコンポーネントへのメソッド:
class Recipe extends React.Component {
constructor(props) {
super(props);
this.state = { recipe: { ingredients: "" } };
this.addHtmlEntities = this.addHtmlEntities.bind(this);
this.deleteRecipe = this.deleteRecipe.bind(this);
}
componentDidMount() {
const {
match: {
params: { id }
}
} = this.props;
const url = `/api/v1/show/${id}`;
fetch(url)
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(response => this.setState({ recipe: response }))
.catch(() => this.props.history.push("/recipes"));
}
addHtmlEntities(str) {
return String(str)
.replace(/</g, "<")
.replace(/>/g, ">");
}
deleteRecipe() {
const {
match: {
params: { id }
}
} = this.props;
const url = `/api/v1/destroy/${id}`;
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "DELETE",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json"
}
})
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(() => this.props.history.push("/recipes"))
.catch(error => console.log(error.message));
}
render() {
const { recipe } = this.state;
let ingredientList = "No ingredients available";
...
の中に deleteRecipe
メソッド、あなたは id
削除するレシピのURLを作成し、CSRFトークンを取得します。 次に、あなたは DELETE
にリクエスト Recipes
レシピを削除するコントローラー。 レシピが正常に削除されると、アプリケーションはユーザーをレシピページにリダイレクトします。
でコードを実行するには deleteRecipe
削除ボタンがクリックされるたびにメソッドを実行し、クリックイベントハンドラーとしてボタンに渡します。 追加します onClick
の削除ボタンへのイベント render
方法:
...
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button type="button" className="btn btn-danger" onClick={this.deleteRecipe}>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
...
チュートリアルのこの時点で、 Recipe.jsx
ファイルは次のようになります。
import React from "react";
import { Link } from "react-router-dom";
class Recipe extends React.Component {
constructor(props) {
super(props);
this.state = { recipe: { ingredients: "" } };
this.addHtmlEntities = this.addHtmlEntities.bind(this);
this.deleteRecipe = this.deleteRecipe.bind(this);
}
addHtmlEntities(str) {
return String(str)
.replace(/</g, "<")
.replace(/>/g, ">");
}
componentDidMount() {
const {
match: {
params: { id }
}
} = this.props;
const url = `/api/v1/show/${id}`;
fetch(url)
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(response => this.setState({ recipe: response }))
.catch(() => this.props.history.push("/recipes"));
}
deleteRecipe() {
const {
match: {
params: { id }
}
} = this.props;
const url = `/api/v1/destroy/${id}`;
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "DELETE",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json"
}
})
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(() => this.props.history.push("/recipes"))
.catch(error => console.log(error.message));
}
render() {
const { recipe } = this.state;
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
const recipeInstruction = this.addHtmlEntities(recipe.instruction);
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button type="button" className="btn btn-danger" onClick={this.deleteRecipe}>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
}
}
export default Recipe;
ファイルを保存して終了します。
アプリケーションサーバーを再起動し、ホームページに移動します。 レシピの表示ボタンをクリックして既存のすべてのレシピを表示し、個々のレシピを表示し、ページのレシピの削除ボタンをクリックして記事を削除します。 レシピページにリダイレクトされ、削除されたレシピは存在しなくなります。
削除ボタンが機能することで、完全に機能するレシピアプリケーションが完成しました。
結論
このチュートリアルでは、データベースとしてPostgreSQLを使用し、スタイリングにBootstrapを使用して、RubyonRailsとReactフロントエンドを使用して食品レシピアプリケーションを作成しました。 より多くのRubyonRailsコンテンツを実行したい場合は、 SSHトンネルを使用した3層Railsアプリケーションでの通信の保護チュートリアルを参照するか、コーディング方法に進んでください。 RubyシリーズでRubyスキルをリフレッシュします。 Reactをさらに深く掘り下げるには、Reactを使用してDigitalOceanAPIからデータを表示する方法の記事を試してください。