Reactフロントエンドを使用してRubyonRailsプロジェクトをセットアップする方法

提供:Dev Guides
移動先:案内検索

著者は、 Electronic Frontier Foundation を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。

###序章

Ruby on Rails は、人気のあるサーバー側のWebアプリケーションフレームワークであり、このチュートリアルの執筆時点では、GitHubに42,000を超える星があります。 GitHubBasecampSoundCloudAirbnb 、など、今日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アプリケーションの開発を容易にするための多くのコマンドを提供します。 これには、createdropresetなどのデータベースを操作するためのコマンドが含まれます。 アプリケーションのデータベースを作成するには、ターミナルウィンドウで次のコマンドを実行します。

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モジュールと、BrowserRouterRoute、および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

上記のコマンドは、タイプstringname列、ingredientsおよびinstruction列とともにRecipeモデルを作成するようにRailsに指示します。タイプtext、およびタイプstringimage列。 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

このコードでは、nameingredients、および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を追加して、nameingredients、および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

このコマンドでは、indexcreateshow、および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メソッドを有効に使用するために、nameimageingredients、および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と一致するレシピを検索し、それをに割り当てます。インスタンス変数@recipeshowアクションで、レシピが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

このコードでは、ループを使用して、nameingredients、およびinstructionで9つのレシピを作成するようにRailsに指示しています。 ファイルを保存して終了します。

このデータをデータベースにシードするには、ターミナルウィンドウで次のコマンドを実行します。

rails db:seed

このコマンドを実行すると、データベースに9つのレシピが追加されます。 これで、それらをフェッチしてフロントエンドでレンダリングできます。

すべてのレシピを表示するコンポーネントは、RecipesControllerindexアクションに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コンポーネントがidparamを予期していることを意味します。 これには、コンポーネントに渡されたpropsを介してアクセスできます。

次に、propsオブジェクトのmatchキーからidparamにアクセスする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(/&lt;/g, "<")
      .replace(/&gt;/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(/&lt;/g, "<")
      .replace(/&gt;/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コンポーネントをインポートし、そのルートを追加しました。 そのルートには:idparamがあり、表示するレシピの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コンポーネントのコンストラクターで、状態オブジェクトを空のnameingredients、およびinstructionフィールドで初期化しました。 これらは、有効なレシピを作成するために必要なフィールドです。 また、3つの方法があります。 onChangeonSubmit、およびstripHtmlEntitiesは、thisにバインドしました。 これらのメソッドは、状態の更新、フォームの送信、および特殊文字(<など)のエスケープ/エンコードされた値(&lt;など)への変換をそれぞれ処理します。

次に、強調表示された行を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, "&lt;")
      .replace(/>/g, "&gt;");
  }

}

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, "&lt;")
      .replace(/>/g, "&gt;");
  }

  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, "&lt;")
      .replace(/>/g, "&gt;");
  }

  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つの入力フィールドを含むフォームがあります。 recipeNamerecipeIngredients、および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コンポーネントのコンストラクターで、thisdeleteRecipeメソッドにバインドします。

〜/ 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(/&lt;/g, "<")
      .replace(/&gt;/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(/&lt;/g, "<")
      .replace(/&gt;/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からデータを表示する方法の記事を試してください。