Reactフロントエンドを使用してRubyonRailsプロジェクトをセットアップする方法
著者は、 Electronic Frontier Foundation を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。
###序章
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つは、新しいRailsアプリケーションを作成するnew
コマンドです。
次に、new
ジェネレーターを使用して新しいRailsアプリケーションを作成します。 ターミナルウィンドウで次のコマンドを実行します。
rails new rails_react_recipe -d=postgresql -T --webpack=react --skip-coffee
上記のコマンドは、rails_react_recipe
という名前のディレクトリに新しいRailsアプリケーションを作成し、必要なRubyとJavaScriptの依存関係をインストールし、Webpackを構成します。 このnew
ジェネレータコマンドに関連付けられているフラグを見ていきましょう。
-d
フラグは、優先データベースエンジン(この場合はPostgreSQL)を指定します。-T
フラグは、このチュートリアルの目的でテストを作成しないため、Railsにテストファイルの生成をスキップするように指示します。 Railsが提供するものとは異なるRubyテストツールを使用する場合にも、このコマンドをお勧めします。--webpack
は、 webpack bundler を使用してJavaScriptを事前構成するように、この場合は特にReactアプリケーション用にRailsに指示します。--skip-coffee
は、このチュートリアルでは不要なCoffeeScriptをセットアップしないようにRailsに要求します。
コマンドの実行が完了したら、アプリのルートディレクトリであるrails_react_recipe
ディレクトリに移動します。
cd rails_react_recipe
次に、ディレクトリの内容を一覧表示します。
ls
このルートディレクトリには、Reactアプリケーションの依存関係を含むpackage.json
ファイルなど、Railsアプリケーションの構造を構成する多数の自動生成ファイルとフォルダーがあります。
これで、新しいRailsアプリケーションが正常に作成されたので、次のステップでそれをデータベースに接続する準備が整いました。
ステップ2—データベースのセットアップ
新しいRailsアプリケーションを実行する前に、まずそれをデータベースに接続する必要があります。 このステップでは、新しく作成したRailsアプリケーションをPostgreSQLデータベースに接続して、必要なときにレシピデータを保存およびフェッチできるようにします。
config/database.yml
にある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
s
またはserver
コマンドは、デフォルトでRailsで配布されるWebサーバーである Puma を起動し、--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
キーの下にインストールされているパッケージが表示されます。
〜/ rails_react_recipe / package.json
{ "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
ジェネレーターを提供します。 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
を希望の番号に置き換えることで、これを再度変更できます。
このコマンドを実行すると、次のファイルが生成されます。
- ホームページ関連のすべてのリクエストを受信するための
homepage_controller.rb
ファイル。 このファイルには、コマンドで指定したindex
アクションが含まれています。 Homepage
コントローラーに関連するJavaScriptの動作を追加するためのhomepage.js
ファイル。Homepage
コントローラーに関連するスタイルを追加するためのhomepage.scss
ファイル。Homepage
コントローラーに関連するヘルパーメソッドを追加するためのhomepage_helper.rb
ファイル。- ホームページに関連するものをレンダリングするためのビューページである
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_react_recipe / config / routers.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
この変更により、Railsは、アプリケーションのルートへのリクエストをHomepage
コントローラーのindex
アクションにマップするように指示されます。これにより、index.html.erb
ファイルにあるものがすべてレンダリングされます。 X201X]をブラウザに表示します。
これが機能していることを確認するには、アプリケーションを起動します。
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
にあります。 これらのパックは、javascript_pack_tag
ヘルパーを使用してRailsビューでリンクでき、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タグの最後に、次の強調表示されたコード行を追加します。
〜/ rails_react_recipe / app / views / layouts / application.html.erb
<!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コンポーネントを作成します。 app/javascript
ディレクトリにcomponents
ディレクトリを作成することから始めます。
mkdir ~/rails_react_recipe/app/javascript/components
components
ディレクトリには、アプリケーション内の他のReactコンポーネントとともに、ホームページのコンポーネントが格納されます。 ホームページには、すべてのレシピを表示するためのテキストと召喚状のボタンが含まれます。
エディタで、components
ディレクトリにHome.jsx
ファイルを作成します。
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
コンポーネントをReactRouterからインポートしました。 Link
コンポーネントは、あるページから別のページに移動するためのハイパーリンクを作成します。 次に、Bootstrapクラスでスタイル設定された、ホームページ用のマークアップ言語を含む機能コンポーネントを作成してエクスポートしました。
Home
コンポーネントを配置したら、Reactルーターを使用してルーティングを設定します。 app/javascript
ディレクトリにroutes
ディレクトリを作成します。
mkdir ~/rails_react_recipe/app/javascript/routes
routes
ディレクトリには、対応するコンポーネントを含むいくつかのルートが含まれます。 指定されたルートがロードされるたびに、対応するコンポーネントがブラウザにレンダリングされます。
routes
ディレクトリに、Index.jsx
ファイルを作成します。
nano ~/rails_react_recipe/app/javascript/routes/Index.jsx
次のコードを追加します。
〜/ rails_react_recipe / app / javascript / routers / 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
、およびReactRouterのSwitch
モジュール。これらを組み合わせることで、あるルートから別のルートに移動できます。 最後に、Home
コンポーネントをインポートしました。これは、リクエストがルート(/
)ルートに一致するたびにレンダリングされます。 アプリケーションにさらにページを追加する場合は常に、このファイルでルートを宣言し、そのページにレンダリングするコンポーネントと一致させるだけです。
ファイルを保存して終了します。
これで、ReactRouterを使用してルーティングを正常に設定できました。 Reactが利用可能なルートを認識して使用するには、アプリケーションへのエントリポイントでルートが利用可能である必要があります。 これを実現するには、Reactがエントリファイルでレンダリングするコンポーネントでルートをレンダリングします。
app/javascript/components
ディレクトリにApp.jsx
ファイルを作成します。
nano ~/rails_react_recipe/app/javascript/components/App.jsx
次のコードをApp.jsx
ファイルに追加します。
〜/ rails_react_recipe / app / javascript / components / 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
そこでのコードを次のコードに置き換えます。
〜/ 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メソッドを使用して、ページの本文に追加されたdiv
要素でApp
コンポーネントをレンダリングしました。 アプリケーションがロードされるたびに、Reactはページのdiv
要素内のApp
コンポーネントのコンテンツをレンダリングします。
ファイルを保存して終了します。
最後に、いくつかのCSSスタイルをホームページに追加します。
~/rails_react_recipe/app/assets/stylesheets
ディレクトリでapplication.css
を開きます。
nano ~/rails_react_recipe/app/assets/stylesheets/application.css
次に、application.css
ファイルの内容を次のコードに置き換えます。
〜/ rails_react_recipe / app / Assets / stylesheets / 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フロントエンドを設定したので、このステップでは、レシピモデルとコントローラーを作成します。 レシピモデルは、ユーザーのレシピに関する情報を保持するデータベーステーブルを表し、コントローラーはレシピの作成、読み取り、更新、または削除の要求を受信して処理します。 ユーザーがレシピを要求すると、レシピコントローラーはこの要求を受信し、それをレシピモデルに渡します。レシピモデルは、要求されたデータをデータベースから取得します。 次に、モデルはレシピデータをコントローラーへの応答として返します。 最後に、この情報がブラウザに表示されます。
Railsが提供するgenerate model
サブコマンドを使用し、モデルの名前とその列およびデータ型を指定して、レシピモデルを作成することから始めます。 ターミナルウィンドウで次のコマンドを実行して、Recipe
モデルを作成します。
rails generate model Recipe name:string ingredients:text instruction:text image:string
上記のコマンドは、タイプstring
のname
列、ingredients
およびinstruction
列とともにRecipe
モデルを作成するようにRailsに指示します。タイプtext
、およびタイプstring
のimage
列。 Railsのモデルは慣例により単数形の名前を使用し、対応するデータベーステーブルは複数形の名前を使用するため、このチュートリアルではモデルにRecipe
という名前を付けました。
generate model
コマンドを実行すると、次の2つのファイルが作成されます。
- モデルに関連するすべてのロジックを保持する
recipe.rb
ファイル。 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
次の強調表示された行を追加して、ファイルが次のようになるようにします。
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
この移行ファイルには、change
メソッドを持つRubyクラスと、recipes
というテーブルを列とそのデータ型とともに作成するコマンドが含まれています。 また、null: false
を追加して、name
、ingredients
、およびinstruction
列のNOT NULL
制約で20190407161357_create_recipes.rb
を更新しました。 、データベースを変更する前に、これらの列に値があることを確認してください。 最後に、画像列にデフォルトの画像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
このコマンドでは、index
、create
、show
、およびdestroy
アクション。 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_react_recipe / config / routers.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
このルートファイルでは、create
およびdestroy
ルートのHTTP動詞を変更して、post
およびdelete
データを使用できるようにしました。 また、ルートに:id
パラメーターを追加して、show
およびdestroy
アクションのルートを変更しました。 :id
は、読み取りまたは削除するレシピの識別番号を保持します。
また、既存のルートと一致しない他のリクエストをhomepage
コントローラーのindex
アクションに転送する、get '/*path'
を使用したキャッチオールルートを追加しました。 このように、フロントエンドのルーティングは、レシピの作成、読み取り、または削除に関連しないリクエストを処理します。
ファイルを保存して終了します。
アプリケーションで使用可能なルートのリストを表示するには、ターミナルウィンドウで次のコマンドを実行します。
rails routes
このコマンドを実行すると、プロジェクトのURIパターン、動詞、および一致するコントローラーまたはアクションのリストが表示されます。
次に、すべてのレシピを一度に取得するためのロジックを追加します。 RailsはActiveRecordライブラリを使用して、このようなデータベース関連のタスクを処理します。 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
index
アクションでは、ActiveRecordが提供するall
メソッドを使用して、データベース内のすべてのレシピを取得します。 order
メソッドを使用して、作成日から降順に並べ替えます。 このようにして、最初に最新のレシピを入手します。 最後に、レシピのリストをrender
を使用してJSON応答として送信します。
次に、新しいレシピを作成するためのロジックを追加します。 すべてのレシピをフェッチする場合と同様に、提供されたレシピの詳細を検証して保存するために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
create
アクションでは、ActiveRecordのcreate
メソッドを使用して新しいレシピを作成します。 create
メソッドには、モデルに提供されたすべてのコントローラーパラメーターを一度に割り当てる機能があります。 これにより、レコードの作成が容易になりますが、悪用される可能性もあります。 これは、強力なパラメーターとして知られるRailsが提供する機能を使用することで防ぐことができます。 このように、ホワイトリストに登録されていない限り、パラメータを割り当てることはできません。 コードで、recipe_params
パラメーターをcreate
メソッドに渡しました。 recipe_params
は、private
メソッドであり、コントローラーパラメーターをホワイトリストに登録して、誤ったコンテンツや悪意のあるコンテンツがデータベースに侵入するのを防ぎます。 この場合、create
メソッドを有効に使用するために、name
、image
、ingredients
、およびinstruction
パラメーターを許可しています。
これで、レシピコントローラーがレシピを読み取って作成できるようになりました。 残っているのは、単一のレシピを読み取って削除するためのロジックです。 次のコードでレシピコントローラーを更新します。
〜/ 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 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
メソッドを作成しました。 recipe
メソッドは、ActiveRecordのfind
メソッドを使用して、id
がparams
で提供されるid
と一致するレシピを検索し、それをに割り当てます。インスタンス変数@recipe
。 show
アクションで、レシピがrecipe
メソッドによって返されるかどうかを確認し、JSON応答として送信するか、そうでない場合はエラーを送信しました。
destroy
アクションでは、Rubyの安全なナビゲーション演算子&.
を使用して同様のことを行いました。これにより、メソッドを呼び出すときのnil
エラーが回避されます。 これにより、レシピが存在する場合にのみレシピを削除してから、応答としてメッセージを送信できます。
recipes_controller.rb
へのこれらの変更が完了したので、ファイルを保存してテキストエディタを終了します。
このステップでは、レシピのモデルとコントローラーを作成しました。 バックエンドでレシピを操作するために必要なすべてのロジックを作成しました。 次のセクションでは、レシピを表示するためのコンポーネントを作成します。
ステップ7—レシピの表示
このセクションでは、レシピを表示するためのコンポーネントを作成します。 最初に、既存のすべてのレシピを表示できるページを作成し、次に別のページを作成して個々のレシピを表示します。
まず、すべてのレシピを表示するページを作成します。 ただし、データベースは現在空であるため、これを行う前に、使用するレシピが必要です。 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
このコードでは、ループを使用して、name
、ingredients
、およびinstruction
で9つのレシピを作成するようにRailsに指示しています。 ファイルを保存して終了します。
このデータをデータベースにシードするには、ターミナルウィンドウで次のコマンドを実行します。
rails db:seed
このコマンドを実行すると、データベースに9つのレシピが追加されます。 これで、それらをフェッチしてフロントエンドでレンダリングできます。
すべてのレシピを表示するコンポーネントは、RecipesController
のindex
アクションにHTTPリクエストを送信して、すべてのレシピのリストを取得します。 これらのレシピは、ページのカードに表示されます。
Recipes.jsx
ファイルをapp/javascript/components
ディレクトリに作成します。
nano ~/rails_react_recipe/app/javascript/components/Recipes.jsx
ファイルが開いたら、次の行を追加して、ReactモジュールとLinkモジュールをファイルにインポートします。
〜/ rails_react_recipe / app / javascript / components / Recipes.jsx
import React from "react"; import { Link } from "react-router-dom";
次に、React.Component
クラスを拡張するRecipes
クラスを作成します。 次の強調表示されたコードを追加して、React.Component
を拡張するReactコンポーネントを作成します。
〜/ rails_react_recipe / app / javascript / components / Recipes.jsx
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 オブジェクトを初期化しています。これは、初期化時に空の配列([]
)になります。
次に、RecipeクラスにcomponentDidMount
メソッドを追加します。 componentDidMount メソッドは、コンポーネントがマウントされた直後に呼び出されるReactライフサイクルメソッドです。 このライフサイクルメソッドでは、すべてのレシピをフェッチするための呼び出しを行います。 これを行うには、次の行を追加します。
〜/ rails_react_recipe / app / javascript / components / 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("/")); } } export default Recipes;
componentDidMount
メソッドで、 FetchAPIを使用してすべてのレシピをフェッチするためのHTTP呼び出しを行いました。 応答が成功すると、アプリケーションはレシピの配列をレシピ状態に保存します。 エラーが発生した場合は、ユーザーをホームページにリダイレクトします。
最後に、Recipe
クラスにrender
メソッドを追加します。 render メソッドは、コンポーネントがレンダリングされるときに評価され、ブラウザーページに表示されるReact要素を保持します。 この場合、render
メソッドは、コンポーネントの状態からレシピのカードをレンダリングします。 次の強調表示された行をRecipes.jsx
に追加します。
〜/ rails_react_recipe / app / javascript / components / 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
次の強調表示された行をファイルに追加します。
〜/ rails_react_recipe / app / javascript / routers / 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モジュールをインポートします。
〜/ rails_react_recipe / app / javascript / components / Recipe.jsx
import React from "react"; import { Link } from "react-router-dom";
次に、強調表示されたコード行を追加して、React.Component
クラスを拡張するRecipe
クラスを作成します。
〜/ rails_react_recipe / app / javascript / components / 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); } } export default Recipe;
Recipes
コンポーネントと同様に、コンストラクターで、レシピの状態を保持する状態オブジェクトを初期化しました。 また、addHtmlEntities
メソッドをthis
にバインドして、コンポーネント内でアクセスできるようにしました。 addHtmlEntities
メソッドは、コンポーネント内の文字エンティティをHTMLエンティティに置き換えるために使用されます。
特定のレシピを見つけるには、アプリケーションにレシピのid
が必要です。 これは、Recipe
コンポーネントがid
param
を予期していることを意味します。 これには、コンポーネントに渡されたprops
を介してアクセスできます。
次に、props
オブジェクトのmatch
キーからid
param
にアクセスするcomponentDidMount
メソッドを追加します。 id
を取得したら、レシピを取得するためのHTTPリクエストを作成します。 次の強調表示された行をファイルに追加します。
〜/ rails_react_recipe / app / javascript / components / 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); } 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
メソッドでは、オブジェクトの破棄を使用して、props
オブジェクトからid
param
を取得し、次にFetchAPIを使用します。 、id
を所有するレシピをフェッチし、setState
メソッドを使用してコンポーネントの状態に保存するために、HTTPリクエストを作成します。 レシピが存在しない場合、アプリはユーザーをレシピページにリダイレクトします。
次に、addHtmlEntities
メソッドを追加します。このメソッドは、文字列を受け取り、エスケープされたすべての開き括弧と閉じ括弧をHTMLエンティティに置き換えます。 これは、レシピ命令に保存されたエスケープ文字を変換するのに役立ちます。
〜/ rails_react_recipe / app / javascript / components / 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); } 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
メソッドを追加します。 これを行うには、次の強調表示された行を追加します。
〜/ rails_react_recipe / app / javascript / components / 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); } 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
次に、次の強調表示された行をファイルに追加します。
〜/ rails_react_recipe / app / javascript / routers / 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—レシピの作成
使用可能な食品レシピアプリケーションを作成するための次のステップは、新しいレシピを作成する機能です。 このステップでは、レシピを作成するためのコンポーネントを作成します。 このコンポーネントには、ユーザーから必要なレシピの詳細を収集するためのフォームが含まれ、Recipe
コントローラーのcreate
アクションにレシピデータを保存するように要求します。
NewRecipe.jsx
ファイルをapp/javascript/components
ディレクトリに作成します。
nano app/javascript/components/NewRecipe.jsx
新しいファイルで、これまでに他のコンポーネントで使用したReactモジュールとLinkモジュールをインポートします。
〜/ rails_react_recipe / app / javascript / components / NewRecipe.jsx
import React from "react"; import { Link } from "react-router-dom";
次に、React.Component
クラスを拡張するNewRecipe
クラスを作成します。 次の強調表示されたコードを追加して、react.Component
を拡張するReactコンポーネントを作成します。
〜/ rails_react_recipe / app / javascript / components / NewRecipe.jsx
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
にバインドしました。 これらのメソッドは、状態の更新、フォームの送信、および特殊文字(<
など)のエスケープ/エンコードされた値(<
など)への変換をそれぞれ処理します。
次に、強調表示された行をNewRecipe
コンポーネントに追加して、stripHtmlEntities
メソッド自体を作成します。
〜/ rails_react_recipe / app / javascript / components / NewRecipe.jsx
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
コンポーネントに追加して、フォームの編集と送信を処理します。
〜/ rails_react_recipe / app / javascript / components / NewRecipe.jsx
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
メソッドを追加します。
〜/ rails_react_recipe / app / javascript / components / NewRecipe.jsx
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つの入力フィールドを含むフォームがあります。 recipeName
、recipeIngredients
、およびinstruction
用に1つ。 各入力フィールドには、onChange
メソッドを呼び出すonChange
イベントハンドラーがあります。 また、送信ボタンにはonSubmit
イベントハンドラーがあり、onSubmit
メソッドを呼び出して、フォームデータを送信します。
ファイルを保存して終了します。
ブラウザでこのコンポーネントにアクセスするには、ルートファイルをそのルートで更新します。
nano app/javascript/routes/Index.jsx
ルートファイルを更新して、次の強調表示された行を含めます。
〜/ rails_react_recipe / app / javascript / routers / 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
メソッドにバインドします。
〜/ rails_react_recipe / app / javascript / components / Recipe.jsx
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
メソッドをレシピコンポーネントに追加します。
〜/ rails_react_recipe / app / javascript / components / Recipe.jsx
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
メソッドの削除ボタンに追加します。
〜/ 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={this.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 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からデータを表示する方法の記事を試してください。