このチュートリアルでは、Spring Data REST とその強力なバックエンド機能を使用するアプリのコレクションと、React の洗練された機能を組み合わせて、わかりやすい UI を構築します。

  • Spring Data REST (英語) は、ハイパーメディアを使用したリポジトリをすばやく構築する方法を提供します。

  • React (英語) は、JavaScript での効率的で高速で使いやすいビューに対する Facebook のソリューションです。

パート 1 — 基本的な機能

Spring コミュニティへようこそ。

このセクションでは、最低限の Spring Data REST アプリケーションを迅速に起動して実行する方法を示します。次に、Facebook の React.js ツールセットを使用して、その上にシンプルな UI を構築する方法を示します。

ステップ 0 — 環境のセットアップ

このリポジトリからコード (GitHub) を自由に入手して、フォローしてください。

自分でやりたい場合は、https://start.spring.io (英語) にアクセスして、次の依存関係を選択してください。

  • Rest リポジトリ

  • Thymeleaf

  • JPA

  • H2

このデモでは、Java 8、Maven プロジェクト、および Spring Boot の最新の安定版リリースを使用しています。また、ES6 (英語) でコーディングされた React.js も使用します。これにより、クリーンで空のプロジェクトが作成されます。そこから、このセクションに明示的に示されているさまざまなファイルを追加したり、前述のリポジトリから借用したりできます。

はじめに ...

最初はデータがありました。そしてそれは良かった。しかしその後、人々はさまざまな手段でデータにアクセスしたいと考えました。何年もの間、人々は多くの MVC コントローラーを組み合わせ、多くは Spring の強力な REST サポートを使用していました。しかし、繰り返し行うには多くの時間がかかります。

Spring Data REST は、いくつかの仮定が行われた場合にこの問題がどれほど簡単になるかを示しています。

  • 開発者は、リポジトリモデルをサポートする Spring Data プロジェクトを使用します。

  • システムは、HTTP 動詞、標準化されたメディアタイプ、IANA 承認済みのリンク名 (英語) など、広く受け入れられている業界標準プロトコルを使用します。

ドメインを宣言する

ドメインオブジェクトは、Spring Data REST ベースのアプリケーションの基盤を形成します。このセクションでは、会社の従業員を追跡するアプリケーションを作成します。次のように、データ型を作成して開始します。

例 1: src/main/java/com/greglturnquist/payroll/Employee.java
@Entity (1)
public class Employee {

	private @Id @GeneratedValue Long id; (2)
	private String firstName;
	private String lastName;
	private String description;

	private Employee() {}

	public Employee(String firstName, String lastName, String description) {
		this.firstName = firstName;
		this.lastName = lastName;
		this.description = description;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;
		Employee employee = (Employee) o;
		return Objects.equals(id, employee.id) &&
			Objects.equals(firstName, employee.firstName) &&
			Objects.equals(lastName, employee.lastName) &&
			Objects.equals(description, employee.description);
	}

	@Override
	public int hashCode() {

		return Objects.hash(id, firstName, lastName, description);
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	@Override
	public String toString() {
		return "Employee{" +
			"id=" + id +
			", firstName='" + firstName + '\'' +
			", lastName='" + lastName + '\'' +
			", description='" + description + '\'' +
			'}';
	}
}
1@Entity は、リレーショナルテーブルに格納するためのクラス全体を示す JPA アノテーションです。
2@Id および @GeneratedValue は、主キーを記録するための JPA アノテーションであり、必要に応じて自動的に生成されます。

このエンティティは、従業員情報を追跡するために使用されます。この場合、名前と職務説明です。

Spring Data REST は JPA に限定されません。このチュートリアルでは表示されませんが、多くの NoSQL データストアをサポートしています。詳細については、REST で Neo4j データアクセスREST で JPA データアクセス、および REST で MongoDB データアクセスを参照してください。

リポジトリの定義

Spring Data REST アプリケーションのもう 1 つの重要な部分は、次のような対応するリポジトリ定義です。

例 2: src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
public interface EmployeeRepository extends CrudRepository<Employee, Long> { (1)

}
1 リポジトリは Spring Data Commons の CrudRepository を継承し、ドメインオブジェクトのタイプとそのプライマリキーをプラグインします。

必要なのはそれだけです!実際、最上位で表示されている場合は、インターフェースにアノテーションを付ける必要さえありません。IDE を使用して CrudRepository を開くと、事前定義されたメソッドのコレクションが見つかります。

必要に応じて、独自のリポジトリを定義できます。Spring Data REST も同様にサポートしています。

デモのプリロード

このアプリケーションを使用するには、次のようにデータを事前にロードする必要があります。

例 3: src/main/java/com/greglturnquist/payroll/DatabaseLoader.java
@Component (1)
public class DatabaseLoader implements CommandLineRunner { (2)

	private final EmployeeRepository repository;

	@Autowired (3)
	public DatabaseLoader(EmployeeRepository repository) {
		this.repository = repository;
	}

	@Override
	public void run(String... strings) throws Exception { (4)
		this.repository.save(new Employee("Frodo", "Baggins", "ring bearer"));
	}
}
1 このクラスには Spring の @Component アノテーションが付いているため、@SpringBootApplication によって自動的に取得されます。
2Spring Boot の CommandLineRunner を実装するため、すべての Bean が作成および登録された後に実行されます。
3 コンストラクター注入とオートワイヤーを使用して、Spring Data の自動作成された EmployeeRepository を取得します。
4run() メソッドはコマンドライン引数で呼び出され、データをロードします。

Spring Data の最大かつ最も強力な機能の 1 つは、JPA クエリを作成できることです。これにより、開発時間が短縮されるだけでなく、バグやエラーのリスクも軽減されます。Spring Data は、リポジトリクラス内のメソッドの名前を調べ、保存、削除、検索など、必要な操作を見つけ出します。

これが、空のインターフェースを作成し、既に構築されている保存、検索、および削除操作を継承する方法です。

ルート URI の調整

デフォルトでは、Spring Data REST は / でリンクのルートコレクションをホストします。そのパスで Web UI をホストするため、次のようにルート URI を変更する必要があります。

例 4: src/main/resources/application.properties
spring.data.rest.base-path=/api

バックエンドの起動

完全に機能する REST API を作成するために必要な最後のステップは、次のように Spring Boot を使用して public static void main メソッドを記述することです。

例 5: src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java
@SpringBootApplication
public class ReactAndSpringDataRestApplication {

	public static void main(String[] args) {
		SpringApplication.run(ReactAndSpringDataRestApplication.class, args);
	}
}

前のクラスと Maven ビルドファイルが https://start.spring.io (英語) から生成されたと仮定すると、IDE 内でその main() メソッドを実行するか、コマンドラインで ./mvnw spring-boot:run を入力することで起動できます。(Windows ユーザーの場合は mvnw.bat)。

Spring Boot の最新情報がなく、その仕組みについては、ジョシュロングの紹介プレゼンテーション (英語) のいずれかを参照してください。それをやった?押して!

REST サービスのツアー

アプリケーションが実行されている状態で、cURL (英語) (または他の好み (英語) のツール)を使用して、コマンドラインで確認できます。次のコマンド(出力とともに表示)は、アプリケーション内のリンクを一覧表示します。

$ curl localhost:8080/api
{
  "_links" : {
    "employees" : {
      "href" : "http://localhost:8080/api/employees"
    },
    "profile" : {
      "href" : "http://localhost:8080/api/profile"
    }
  }
}

ルートノードに ping を実行すると、HAL 形式の JSON ドキュメント (英語) にラップされたリンクのコレクションが返されます。

  • _links は利用可能なリンクのコレクションです。

  • employees は、EmployeeRepository インターフェースによって定義された従業員オブジェクトの集約ルートを指します。

  • profile は IANA 標準の関係であり、サービス全体に関する検出可能なメタデータを指します。これについては後のセクションで説明します。

employees リンクをナビゲートすることにより、このサービスをさらに掘り下げることができます。次のコマンド(出力とともに表示)はこれを行います。

$ curl localhost:8080/api/employees
{
  "_embedded" : {
    "employees" : [ {
      "firstName" : "Frodo",
      "lastName" : "Baggins",
      "description" : "ring bearer",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/employees/1"
        }
      }
    } ]
  }
}

この段階では、従業員のコレクション全体を表示しています。

前に事前にロードしたデータとともに、self リンクを持つ _links 属性が含まれています。これは、その特定の従業員の正規リンクです。正規とは何ですか? 「文脈の自由」を意味します。例: /api/orders/1/processor を介して同じユーザーを取得でき、従業員は特定のオーダーの処理に関連付けられています。ここでは、他のエンティティとの関連はありません。

リンクは REST の重要な側面です。これらは、関連アイテムにナビゲートするためのパワーを提供します。これにより、変更が発生するたびに書き直さなくても、他の関係者が API をナビゲートできるようになります。クライアントの更新は、クライアントがリソースへのパスをハードコードする場合の一般的な問題です。リソースを再構築すると、コードに大きな混乱が生じる可能性があります。リンクが使用され、ナビゲーションルートが維持される場合、そのような調整を行うことは簡単かつ柔軟になります。

必要に応じて、その従業員を表示することもできます。次のコマンド(出力とともに表示)はこれを行います。

$ curl localhost:8080/api/employees/1
{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "description" : "ring bearer",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/api/employees/1"
    }
  }
}

ドメインオブジェクトしかないため、_embedded ラッパーは必要ないことを除いて、ここではほとんど変更はありません。

それはすべてうまくいきますが、おそらく新しいエントリを作成するのに苦労しています。次のコマンド(出力とともに表示)はこれを行います。

$ curl -X POST localhost:8080/api/employees -d "{\"firstName\": \"Bilbo\", \"lastName\": \"Baggins\", \"description\": \"burglar\"}" -H "Content-Type:application/json"
{
  "firstName" : "Bilbo",
  "lastName" : "Baggins",
  "description" : "burglar",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/api/employees/2"
    }
  }
}

この関連ガイドに示すように、PUTPATCH および DELETE も使用できます。ただし、今のところは、洗練された UI の構築に進みます。

カスタム UI コントローラーのセットアップ

Spring Boot を使用すると、カスタム Web ページを簡単に立ち上げることができます。まず、次のように Spring MVC コントローラーが必要です。

例 6: src/main/java/com/greglturnquist/payroll/HomeController.java
@Controller (1)
public class HomeController {

	@RequestMapping(value = "/") (2)
	public String index() {
		return "index"; (3)
	}

}
1@Controller は、このクラスを Spring MVC コントローラーとしてマークします。
2@RequestMapping は、/ ルートをサポートするために index() メソッドにフラグを立てます。
3 テンプレートの名前として index を返します。Spring Boot の自動構成ビューリゾルバーは、src/main/resources/templates/index.html にマッピングします。

HTML テンプレートの定義

Thymeleaf を使用していますが、その機能の多くは実際には使用しません。開始するには、次のようにインデックスページが必要です。

例 7: src/main/resources/templates/index.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head lang="en">
    <meta charset="UTF-8"/>
    <title>ReactJS + Spring Data REST</title>
    <link rel="stylesheet" href="/main.css" />
</head>
<body>

    <div id="react"></div>

    <script src="built/bundle.js"></script>

</body>
</html>

このテンプレートの重要な部分は、中央の <div id="react"></div> コンポーネントです。ここで、レンダリングされた出力をプラグインするように React に指示します。

また、その bundle.js ファイルがどこから来たのか疑問に思うかもしれません。構築方法は次のセクションで説明します。

このチュートリアルでは main.css を示していませんが、上にリンクされていることがわかります。CSS に関しては、Spring Boot は src/main/resources/static で見つかったものを自動的に提供します。そこに独自の main.css ファイルを配置します。私たちの焦点は CSS ではなく React と Spring Data REST であるため、チュートリアルには示されていません。

JavaScript モジュールのロード

このセクションには、JavaScript ビットを地面から取り出すための最低限の情報が含まれています。JavaScripts コマンドラインツールはすべてインストールできますが、インストールする必要はありません。少なくともまだインストールしていません。代わりに、pom.xml ビルドファイルに次を追加するだけです。

例 8: JavaScript ビットの作成に使用される frontend-maven-plugin 
<plugin>
	<groupId>com.github.eirslett</groupId>
	<artifactId>frontend-maven-plugin</artifactId>
</plugin>

この小さなプラグインは複数の手順を実行します。

  • install-node-and-npm コマンドは、node.js とそのパッケージ管理ツール npm を target フォルダーにインストールします。(これにより、バイナリがソース管理下に置かれなくなり、clean でクリーンアウトできるようになります)。

  • npm コマンドは、指定された引数(install)で npm バイナリを実行します。これにより、package.json で定義されたモジュールがインストールされます。

  • webpack コマンドは webpack バイナリを実行し、webpack.config.js に基づいてすべての JavaScript コードをコンパイルします。

これらの手順は順番に実行され、本質的に node.js をインストールし、JavaScript モジュールをダウンロードし、JS ビットを構築します。

どのモジュールがインストールされていますか? JavaScript 開発者は通常、npm を使用して、次のような package.json ファイルを作成します。

例 9: package.json
{
  "name": "spring-data-rest-and-reactjs",
  "version": "0.1.0",
  "description": "Demo of ReactJS + Spring Data REST",
  "repository": {
    "type": "git",
    "url": "[email protected] (英語)  :spring-guides/tut-react-and-spring-data-rest.git"
  },
  "keywords": [
    "rest",
    "hateoas",
    "spring",
    "data",
    "react"
  ],
  "author": "Greg L. Turnquist",
  "license": "Apache-2.0",
  "bugs": {
    "url": "https://github.com/spring-guides/tut-react-and-spring-data-rest/issues"
  },
  "homepage": "https://github.com/spring-guides/tut-react-and-spring-data-rest",
  "dependencies": {
    "react": "^16.5.2",
    "react-dom": "^16.5.2",
    "rest": "^1.3.1"
  },
  "scripts": {
    "watch": "webpack --watch -d --output ./target/classes/static/built/bundle.js"
  },
  "devDependencies": {
    "@babel/core": "^7.1.0",
    "@babel/preset-env": "^7.1.0",
    "@babel/preset-react": "^7.0.0",
    "babel-loader": "^8.0.2",
    "webpack": "^4.19.1",
    "webpack-cli": "^3.1.0"
  }
}

主な依存関係は次のとおりです。

  • react.js: このチュートリアルで使用されるツールキット

  • rest.js: REST 呼び出しを行うために使用される CujoJS ツールキット

  • webpack: JavaScript コンポーネントを単一のロード可能なバンドルにコンパイルするために使用されるツールキット

  • バベル : ES6 を使用して JavaScript コードを記述し、ES5 にコンパイルしてブラウザーで実行するには

後で使用する JavaScript コードをビルドするには、次のように webpack (英語) のビルドファイルを定義する必要があります。

例 10: webpack.config.js
var path = require('path');

module.exports = {
    entry: './src/main/js/app.js',
    devtool: 'sourcemaps',
    cache: true,
    mode: 'development',
    output: {
        path: __dirname,
        filename: './src/main/resources/static/built/bundle.js'
    },
    module: {
        rules: [
            {
                test: path.join(__dirname, '.'),
                exclude: /(node_modules)/,
                use: [{
                    loader: 'babel-loader',
                    options: {
                        presets: ["@babel/preset-env", "@babel/preset-react"]
                    }
                }]
            }
        ]
    }
};

この webpack 構成ファイル:

  • entry point を ./src/main/js/app.js として定義します。本質的に、app.js (まもなく作成するモジュール)は、JavaScript アプリケーションのことわざである public static void main() です。webpack は、最終バンドルがブラウザーによってロードされたときにを起動するを知るために、これを知っている必要があります。

  • sourcemaps を作成して、ブラウザーで JS コードをデバッグしているときに、元のソースコードにリンクバックできるようにします。

  • すべての JavaScript ビットを ./src/main/resources/static/built/bundle.js にコンパイルします。これは、Spring Boot uber JAR と同等の JavaScript です。すべてのカスタムコードと require() 呼び出しによってプルされたモジュールは、このファイルに詰め込まれます。

  • ES6 React コードを標準ブラウザで実行できる形式にコンパイルするために、es2015 と react の両方のプリセットを使用して、Babel エンジンに接続します。

これらの各 JavaScript ツールの動作方法の詳細については、対応するリファレンスドキュメントを参照してください。

JavaScript の変更を自動的に確認したいですか? npm run-script watch を実行して、webpack を監視モードにします。ソースを編集すると、bundle.js が再生成されます。

これらすべての準備が整ったら、React ビットに焦点を合わせることができます。React ビットは、DOM のロード後に取得されます。次のように、パーツに分割されます。

webpack を使用して物を組み立てているため、先に進み、必要なモジュールをフェッチします。

例 11: src/main/js/app.js
const React = require('react'); (1)
const ReactDOM = require('react-dom'); (2)
const client = require('./client'); (3)
1React は、このアプリのビルドに使用される Facebook のメインライブラリの 1 つです。
2ReactDOM は、React の DOM およびサーバーレンダラーへのエントリポイントとして機能するパッケージです。汎用の React パッケージと組み合わせることを目的としています。
3client は、rest.js を構成して、HAL、URI テンプレートなどのサポートを含めるカスタムコードです。また、デフォルトの Accept リクエストヘッダーを application/hal+json に設定します。ここでコードを読むことができます (GitHub)
REST 呼び出しに使用するものは重要ではないため、client のコードは表示されません。ソースを自由に確認してください。しかし、ポイントは、Restangular(または好きなもの)をプラグインすることができ、概念はまだ適用されます。

React に飛び込む

React は、コンポーネントの定義に基づいています。多くの場合、1 つのコンポーネントは、親子関連で別のコンポーネントの複数のインスタンスを保持できます。この概念は、複数のレイヤーに拡張できます。

まず最初に、すべてのコンポーネントにトップレベルのコンテナーを用意しておくと非常に便利です。(これは、このシリーズ全体でコードを展開するにつれて明らかになります)現在、従業員リストのみがあります。ただし、後で他の関連コンポーネントが必要になる場合があるため、次から始めてください。

例 12: src/main/js/app.js - アプリコンポーネント
class App extends React.Component { (1)

	constructor(props) {
		super(props);
		this.state = {employees: []};
	}

	componentDidMount() { (2)
		client({method: 'GET', path: '/api/employees'}).done(response => {
			this.setState({employees: response.entity._embedded.employees});
		});
	}

	render() { (3)
		return (
			<EmployeeList employees={this.state.employees}/>
		)
	}
}
1class App extends React.Component{ …​ } は、React コンポーネントを作成するメソッドです。
2componentDidMount は、React が DOM のコンポーネントをレンダリングした後に呼び出される API です。
3render は、画面上にコンポーネントを「描画」する API です。
React では、大文字はコンポーネントの命名規則です。

App コンポーネントでは、Spring Data REST バックエンドから従業員の配列が取得され、このコンポーネントの state データに保存されます。

React コンポーネントには、stateproperties の 2 種類のデータがあります。

State は、コンポーネントがそれ自体を処理することが期待されるデータです。また、変動して変化する可能性のあるデータです。状態を読み取るには、this.state を使用します。更新するには、this.setState() を使用します。this.setState() が呼び出されるたびに、React は状態を更新し、前の状態と新しい状態の間の差分を計算し、ページ上の DOM に一連の変更を挿入します。これにより、UI が迅速かつ効率的に更新されます。

一般的な規則は、コンストラクターですべての属性を空にして状態を初期化することです。次に、componentDidMount を使用して属性を設定することにより、サーバーからデータを検索します。それ以降は、ユーザーアクションまたはその他のイベントによって更新を実行できます。

Properties には、コンポーネントに渡されるデータが含まれます。プロパティは変更されませんが、代わりに固定値です。これらを設定するには、すぐにわかるように、新しいコンポーネントを作成するときに属性に割り当てます。

JavaScript は、他の言語のようにデータ構造をロックダウンしません。値を割り当ててプロパティを破壊しようとすることはできますが、これは React の差動エンジンでは機能しないため、避ける必要があります。

このコードでは、関数は rest.js の約束に準拠 (英語) インスタンスである client を介してデータをロードします。/api/employees からの取得が完了すると、done() 内の関数を呼び出し、その HAL ドキュメント(response.entity._embedded.employees)に基づいて状態を設定します。に curl /api/employees の構造を見て、それがこの構造にどのようにマッピングされるかを参照してください。

状態が更新されると、フレームワークによって render() 関数が呼び出されます。従業員の状態データは、<EmployeeList /> React コンポーネントの作成に入力パラメーターとして含まれています。

次のリストは、EmployeeList の定義を示しています。

例 13: src/main/js/app.js - EmployeeList コンポーネント
class EmployeeList extends React.Component{
	render() {
		const employees = this.props.employees.map(employee =>
			<Employee key={employee._links.self.href} employee={employee}/>
		);
		return (
			<table>
				<tbody>
					<tr>
						<th>First Name</th>
						<th>Last Name</th>
						<th>Description</th>
					</tr>
					{employees}
				</tbody>
			</table>
		)
	}
}

JavaScript のマップ機能を使用して、this.props.employees は従業員レコードの配列から <Element /> React コンポーネントの配列に変換されます(後ほど説明します)。

次のリストを考慮してください。

<Employee key={employee._links.self.href} data={employee} />

上記のリストは、keydata の 2 つのプロパティを持つ新しい React コンポーネント(大文字形式に注意)を作成します。これらには、employee._links.self.href および employee から値が提供されます。

Spring Data REST を使用する場合は常に、self リンクが特定のリソースのキーになります。React は子ノードの一意の識別子を必要とし、_links.self.href は完璧です。

最後に、次のように、マッピングを使用して構築された employees の配列をラップした HTML テーブルを返します。

<table>
    <tr>
        <th>First Name</th>
        <th>Last Name</th>
        <th>Description</th>
    </tr>
    {employees}
</table>

状態、プロパティ、および HTML のこのシンプルなレイアウトは、React を使用して、シンプルでわかりやすいコンポーネントを宣言的に作成する方法を示しています。

このコードには HTML JavaScript の両方が含まれているか判定します。はい、これは JSX (英語) です。使用する必要はありません。React は純粋な JavaScript を使用して作成できますが、JSX 構文は非常に簡潔です。Babel.js の迅速な作業のおかげで、トランスパイラーは JSX と ES6 の両方のサポートを一度に提供します。

JSX には、ES6 (英語) の断片も含まれています。このコードで使用されているものは、アロー関数 (英語) です。独自のスコープ this でネストされた function() を作成することを避け、self 変数 (英語) を必要としないようにします。

ロジックを構造に混ぜることが心配ですか? React の API は、状態とプロパティを組み合わせた優れた宣言型構造を推奨します。React は、関連のない JavaScript と HTML の束を混合する代わりに、関連する状態とプロパティの小さなビットを備えた単純なコンポーネントを構築することを推奨します。単一のコンポーネントを見て、設計を理解できます。その後、組み合わせてより大きな構造を簡単に作成できます。

次に、<Employee /> が何であるかを次のように実際に定義する必要があります。

例 14: src/main/js/app.js - 従業員コンポーネント
class Employee extends React.Component{
	render() {
		return (
			<tr>
				<td>{this.props.employee.firstName}</td>
				<td>{this.props.employee.lastName}</td>
				<td>{this.props.employee.description}</td>
			</tr>
		)
	}
}

このコンポーネントは非常に単純です。従業員の 3 つのプロパティを囲む 1 つの HTML テーブル行があります。プロパティ自体は this.props.employee です。JavaScript オブジェクトを渡すと、サーバーから取得したデータを簡単に渡すことができることに注意してください。

このコンポーネントは状態を管理せず、ユーザー入力も処理しないため、他に行うことはありません。これは、上記の <EmployeeList /> に詰め込むように誘惑するかもしれません。それをしません!アプリを小さなコンポーネントに分割し、それぞれが 1 つのジョブを実行することで、将来機能を構築しやすくなります。

最後のステップは、次のように全体をレンダリングすることです。

例 15: src/main/js/app.js- レンダリングコード
ReactDOM.render(
	<App />,
	document.getElementById('react')
)

React.render() は 2 つの引数を受け入れます : 定義した React コンポーネントと、それを注入する DOM ノード。HTML ページで以前に <div id="react"></div> アイテムをどのように見たか覚えていますか?ここでピックアップしてプラグインします。

これらすべてを準備したら、アプリケーション(./mvnw spring-boot:run)を再実行して http://localhost:8080 にアクセスしてください。次のイメージは、更新されたアプリケーションを示しています。

basic 1

システムによってロードされた最初の従業員を見ることができます。

cURL を使用して新しいエントリを作成したことを覚えていますか?次のコマンドを使用して再度実行します。

curl -X POST localhost:8080/api/employees -d "{\"firstName\": \"Bilbo\", \"lastName\": \"Baggins\", \"description\": \"burglar\"}" -H "Content-Type:application/json"

ブラウザをリフレッシュすると、新しいエントリが表示されるはずです。

basic 2

これで、Web サイトに両方のリストが表示されます。

レビュー

本セクション :

  • ドメインオブジェクトと対応するリポジトリを定義しました。

  • Spring Data REST に本格的なハイパーメディアコントロールを使用してエクスポートさせます。

  • 親子関連で 2 つの単純な React コンポーネントを作成しました。

  • サーバーデータを取得し、単純な静的 HTML 構造としてレンダリングしました。

課題 ?

  • Web ページは動的ではありませんでした。新しいレコードを取得するには、ブラウザをリフレッシュする必要がありました。

  • Web ページは、ハイパーメディアコントロールまたはメタデータを使用しませんでした。代わりに、/api/employees からデータをフェッチするためにハードコードされました。

  • 読み取り専用です。cURL を使用してレコードを変更できますが、Web ページにはインタラクティブ機能はありません。

次のセクションでこれらの欠点に対処します。

パート 2 - ハイパーメディアコントロール

前のセクションでは、Spring Data REST を使用して従業員データを保存するバックエンド給与サービスを作成する方法を見つけました。欠落していた主な機能は、ハイパーメディアコントロールとリンクによるナビゲーションを使用することでした。代わりに、データを見つけるためにパスをハードコードしました。

このリポジトリからコード (GitHub) を自由に入手して、フォローしてください。このセクションは、前のセクションのアプリケーションに基づいており、追加のものが追加されています。

はじめに、データがありました ... そして REST がありました

HTTP ベースのインターフェースを REST API と呼んでいる人々の数にイライラしています。今日の例は、SocialSite REST API です。それが RPC です。RPC を叫びます ... ハイパーテキストが制約であるという概念で REST アーキテクチャスタイルを明確にするために何をする必要がありますか?つまり、アプリケーション状態のエンジン(および API)がハイパーテキストによって駆動されていない場合、RESTful にすることも REST API にすることもできません。限目。修正が必要な壊れたマニュアルがどこかにありますか?
— Roy T. フィールディング
https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven

それでは、ハイパーメディアコントロール(つまり、ハイパーテキスト)とは正確に何であり、どのように使用できますか?調べるために、一歩後退して、REST のコアミッションを見ていきます。

REST の概念は、Web を成功に導いたアイデアを借用し、API に適用することでした。Web の広大なサイズ、動的な性質、およびクライアント(つまりブラウザ)の更新頻度が低いにもかかわらず、Web は驚くべき成功を収めています。Roy Fielding は、いくつかの制約と機能を使用して、API の生成と消費を同様に拡張できるかどうかを確認しようとしました。

制約の 1 つは、動詞の数を制限することです。REST の場合、主なものは GET、POST、PUT、DELETE、および PATCH です。他にもありますが、ここでは取り上げません。

  • GET: システムを変更せずにリソースの状態を取得する

  • POST: 場所を指定せに新しいリソースを作成する

  • PUT: 既存のリソースを置き換え、既に存在する他のもの(存在する場合)を上書きする

  • DELETE: 既存のリソースを削除する

  • PATCH: 既存のリソースを変更する (新しいリソースを作成するよりも部分的に)

これらは、よく知られた仕様を持つ標準化された HTTP 動詞です。既に作成された HTTP 操作を選択して使用することにより、新しい言語を発明したり、業界を教育したりする必要がなくなります。

REST のもう 1 つの制約は、メディアタイプを使用してデータの形式を定義することです。誰もが情報交換のために独自のダイアレクトを書く代わりに、いくつかのメディアタイプを開発するのが賢明でしょう。受け入れられる最も人気のあるものの 1 つは、メディアタイプ application/hal+json の HAL です。Spring Data REST のデフォルトのメディアタイプです。重要な点は、REST には単一のメディアタイプが集中化されていないことです。代わりに、人々はメディアの種類を開発し、接続して試してみることができます。さまざまなニーズが利用可能になると、業界は柔軟に移動できます。

REST の主要な機能は、関連リソースへのリンクを含めることです。例:オーダーを表示している場合、RESTful API には、関連する顧客へのリンク、アイテムのカタログへのリンク、およびおそらくオーダー元のストアへのリンクが含まれます。このセクションでは、ページングを紹介し、ナビゲーションページングリンクの使用方法も確認します。

バックエンドからページングをオンにする

フロントエンドハイパーメディアコントロールの使用を開始するには、追加のコントロールをオンにする必要があります。Spring Data REST はページングのサポートを提供します。これを使用するには、リポジトリ定義を次のように微調整します。

例 16: src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {

}

インターフェースは PagingAndSortingRepository を継承し、ページサイズを設定するための追加オプションを追加し、ナビゲーションリンクを追加してページ間を移動します。バックエンドの残りは同じです(物事を面白くするためにいくつかの追加の事前ロードされたデータ (GitHub) を除いて)。

アプリケーション(./mvnw spring-boot:run)を再起動し、動作を確認します。次に、次のコマンド(出力とともに表示)を実行して、ページングの動作を確認します。

$ curl "localhost:8080/api/employees?size=2"
{
  "_links" : {
    "first" : {
      "href" : "http://localhost:8080/api/employees?page=0&size=2"
    },
    "self" : {
      "href" : "http://localhost:8080/api/employees"
    },
    "next" : {
      "href" : "http://localhost:8080/api/employees?page=1&size=2"
    },
    "last" : {
      "href" : "http://localhost:8080/api/employees?page=2&size=2"
    }
  },
  "_embedded" : {
    "employees" : [ {
      "firstName" : "Frodo",
      "lastName" : "Baggins",
      "description" : "ring bearer",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/employees/1"
        }
      }
    }, {
      "firstName" : "Bilbo",
      "lastName" : "Baggins",
      "description" : "burglar",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/employees/2"
        }
      }
    } ]
  },
  "page" : {
    "size" : 2,
    "totalElements" : 6,
    "totalPages" : 3,
    "number" : 0
  }
}

デフォルトのページサイズは 20 ですが、それほど多くのデータはありません。そのため、動作を確認するために、?size=2 を設定します。予想どおり、2 人の従業員のみがリストされています。さらに、firstnext および last リンクもあります。self リンクもあります。これは、ページパラメーターを含むコンテキストがありません。

next リンクに移動すると、prev リンクも表示されます。次のコマンド(出力とともに表示)はこれを行います。

$ curl "http://localhost:8080/api/employees?page=1&size=2"
{
  "_links" : {
    "first" : {
      "href" : "http://localhost:8080/api/employees?page=0&size=2"
    },
    "prev" : {
      "href" : "http://localhost:8080/api/employees?page=0&size=2"
    },
    "self" : {
      "href" : "http://localhost:8080/api/employees"
    },
    "next" : {
      "href" : "http://localhost:8080/api/employees?page=2&size=2"
    },
    "last" : {
      "href" : "http://localhost:8080/api/employees?page=2&size=2"
    }
  },
...
URL クエリパラメーターで & を使用する場合、コマンドラインはそれが改行であると見なします。この問題を回避するには、URL 全体を引用符で囲みます。

それはきれいに見えますが、それを利用するためにフロントエンドを更新するとさらに良くなります。

関連によるナビゲート

Spring Data REST がすぐに提供するハイパーメディアコントロールの使用を開始するために、バックエンドでこれ以上の変更は必要ありません。フロントエンドでの作業に切り替えることができます。(それは Spring Data REST の美しさの一部です:乱雑なコントローラーの更新はありません!)

このアプリケーションは「Spring Data REST 固有」ではないことを指摘することが重要です。代わりに、HAL (英語) URI テンプレート (英語) 、およびその他の標準を使用します。それが rest.js の使用が簡単な理由です。そのライブラリには HAL サポートが付属しています。

前のセクションでは、パスを /api/employees にハードコードしました。代わりに、ハードコードする必要がある唯一のパスは、次のようにルートです

...
var root = '/api';
...

便利な小さな follow() 関数 (GitHub) を使用して、次のようにルートから開始して目的の場所に移動できます。

Unresolved directive in hypermedia/README.adoc - include::src/main/js/app.js[tag=follow-1]

前のセクションでは、ロードは componentDidMount() 内で直接行われました。このセクションでは、ページサイズが更新されたときに従業員のリスト全体を再読み込みできるようにします。そのために、次のように loadFromServer() に物を移動しました。

Unresolved directive in hypermedia/README.adoc - include::src/main/js/app.js[tag=follow-2]

loadFromServer は、前のセクションと非常によく似ています。ただし、follow() を使用します。

  • follow() 関数の最初の引数は、REST 呼び出しを行うために使用される client オブジェクトです。

  • 2 番目の引数は、開始するルート URI です。

  • 3 番目の引数は、ナビゲートする関連の配列です。各文字列またはオブジェクトを指定できます。

関連の配列は ["employees"] のように単純にすることができます。つまり、最初の呼び出しが行われたとき、employees という名前の関連(または rel)を _links で調べます。href を見つけてナビゲートします。配列に別の関連がある場合は、プロセスを繰り返します。

rel だけでは不十分な場合があります。このコードの断片では、?size=<pageSize> のクエリパラメーターもプラグインします。後で見るように、提供できる他のオプションがあります。

JSON スキーマメタデータの取得

サイズベースのクエリで employees に移動すると、employeeCollection が利用可能になります。前のセクションで、そのデータを <EmployeeList /> 内に表示しました。このセクションでは、/api/profile/employees/ で見つかった JSON スキーマメタデータ (英語) を取得するために別の呼び出しを実行しています。

次の curl コマンドを実行することにより、自分でデータを表示できます(出力とともに表示)。

$ curl http://localhost:8080/api/profile/employees -H "Accept:application/schema+json"
{
  "title" : "Employee",
  "properties" : {
    "firstName" : {
      "title" : "First name",
      "readOnly" : false,
      "type" : "string"
    },
    "lastName" : {
      "title" : "Last name",
      "readOnly" : false,
      "type" : "string"
    },
    "description" : {
      "title" : "Description",
      "readOnly" : false,
      "type" : "string"
    }
  },
  "definitions" : { },
  "type" : "object",
  "$schema" : "https://json-schema.org/draft-04/schema#"
}
/profile/employees のメタデータのデフォルト形式は ALPS (英語) です。ただし、この場合、コンテンツネゴシエーションを使用して JSON スキーマを取得しています。

この情報を `<App />` コンポーネントの状態でキャプチャーすることにより、後で入力フォームを構築するときにそれを有効に活用できます。

新しいレコードを作成する

このメタデータを装備して、UI にいくつかの追加コントロールを追加できるようになりました。次のように、新しい React コンポーネント <CreateDialog /> を作成することから開始できます。

Unresolved directive in hypermedia/README.adoc - include::src/main/js/app.js[tag=create-dialog]

この新しいコンポーネントには、handleSubmit() 機能と予想される render() 機能の両方があります。

これらの関数を逆順で掘り下げ、まず render() 関数を調べます。

レンダリング

コードは、attributes プロパティにある JSON スキーマデータをマッピングし、それを <p><input></p> 要素の配列に変換します。

  • key は、React が複数の子ノードを区別するために再び必要です。

  • これは、単純なテキストベースの入力フィールドです。

  • placeholder を使用すると、フィールドがどちらであるかをユーザーに表示できます。

  • name 属性を持つことに慣れているかもしれませんが、必須ではありません。React では、ref は特定の DOM ノードを取得するためのメカニズムです(すぐにわかるように)。

これは、サーバーからデータをロードすることにより駆動されるコンポーネントの動的な性質を表しています。

このコンポーネントのトップレベル <div> の中には、アンカータグと別の <div> があります。アンカータグは、ダイアログを開くためのボタンです。そして、ネストされた <div> は隠されたダイアログそのものです。この例では、純粋な HTML5 と CSS3 を使用しています。JavaScript はまったくありません!ダイアログの表示と非表示に使用される CSS コードを確認できます (GitHub) 。ここでは詳しく説明しません。

<div id="createEmployee"> の内部には、入力フィールドの動的リストが挿入され、その後に Create ボタンが挿入されるフォームがあります。そのボタンには onClick={this.handleSubmit} イベントハンドラーがあります。これは、イベントハンドラーを登録する React の方法です。

React は、すべての DOM 要素にイベントハンドラーを作成しません。代わりに、はるかに高性能で洗練された (英語) ソリューションがあります。そのインフラストラクチャを管理する必要はなく、代わりに機能コードの作成に集中できます。

ユーザー入力の処理

handleSubmit() 関数は、最初にイベントが階層をさらに上にバブルするのを停止します。次に、React.findDOMNode(this.refs[attribute]) を使用して、同じ JSON スキーマ属性プロパティを使用して各 <input> を検索します。

this.refs は、名前で特定の React コンポーネントに手を伸ばして取得する方法です。仮想 DOM コンポーネントのみを取得していることに注意してください。実際の DOM 要素を取得するには、React.findDOMNode() を使用する必要があります。

すべての入力を反復処理して newEmployee オブジェクトを作成した後、新しい従業員レコードの onCreate() へのコールバックを呼び出します。この関数は App.onCreate 内にあり、この React コンポーネントに別のプロパティとして提供されました。そのトップレベル関数がどのように動作するかを参照してください。

Unresolved directive in hypermedia/README.adoc - include::src/main/js/app.js[tag=create]

ここでも、follow() 関数を使用して、POST 操作が実行される employees リソースに移動します。この場合、パラメーターを適用する必要はなかったため、rel インスタンスの文字列ベースの配列は問題ありません。この状況では、POST 呼び出しが返されます。これにより、次の then() 句で POST の結果の処理を処理できます。

通常、新しいレコードはデータセットの最後に追加されます。特定のページを見ているため、新しい従業員レコードが現在のページにないことを期待するのは論理的です。これを処理するには、同じページサイズが適用されたデータの新しいバッチをフェッチする必要があります。その約束は done() 内の最終節に対して返されます。

ユーザーはおそらく新しく作成された従業員を見たいと思うため、ハイパーメディアコントロールを使用して last エントリに移動できます。

約束ベースの API を初めて使用しますか? 約束 (英語) は、非同期操作を開始し、タスクが完了したときに応答する関数を登録する方法です。約束は、「コールバック地獄」を回避するために一緒に連鎖されるように設計されています。次のフローを参照してください。

when.promise(async_func_call())
	.then(function(results) {
		/* process the outcome of async_func_call */
	})
	.then(function(more_results) {
		/* process the previous then() return value */
	})
	.done(function(yet_more) {
		/* process the previous then() and wrap things up */
	});

詳細については、promise に関するこのチュートリアル (英語) を参照してください。

promise で覚えておくべき秘密は、then() 関数値であろうと別の promise であろうと、何かを返す必要があるということです。done() 関数は何も返しません。また、チェーンは何も返しません。まだ気付いていない場合は、client (rest.js からの rest のインスタンス)と follow 関数が promise を返します。

データのページング

バックエンドでページングを設定し、新しい従業員を作成するときに既にそれを利用し始めています。

前のセクションでは、ページコントロールを使用して last ページにジャンプしました。UI に動的に適用し、ユーザーが必要に応じてナビゲートできるようにすることは非常に便利です。利用可能なナビゲーションリンクに基づいてコントロールを動的に調整することは素晴らしいことです。

まず、使用した onNavigate() 関数を確認しましょう。

Unresolved directive in hypermedia/README.adoc - include::src/main/js/app.js[tag=navigate]

これは、App.onNavigate 内の上部で定義されます。繰り返しますが、これは最上位コンポーネントの UI の状態を管理できるようにするためです。onNavigate() を <EmployeeList /> React コンポーネントに渡した後、次のハンドラーがコーディングされ、一部のボタンのクリックを処理します。

Unresolved directive in hypermedia/README.adoc - include::src/main/js/app.js[tag=handle-nav]

これらの各関数はデフォルトイベントをインターセプトし、バブリングを防ぎます。次に、適切なハイパーメディアリンクを使用して onNavigate() 関数を呼び出します。

これで、EmployeeList.render のハイパーメディアリンクに表示されるリンクに基づいて、条件付きでコントロールを表示できます。

Unresolved directive in hypermedia/README.adoc - include::src/main/js/app.js[tag=employee-list-render]

前のセクションと同様に、this.props.employees を <Element /> コンポーネントの配列に変換します。次に、navLinks の配列を HTML ボタンの配列として構築します。

React は XML に基づいているため、< を <button> 要素内に配置することはできません。代わりに、エンコードされたバージョンを使用する必要があります。

次に、返された HTML の下部に {navLinks} が挿入されているのを確認できます。

既存のレコードを削除する

エントリの削除ははるかに簡単です。必要なのは、HAL ベースのレコードを取得し、DELETE を self リンクに適用することだけです。

Unresolved directive in hypermedia/README.adoc - include::src/main/js/app.js[tag=employee]

Employee コンポーネントのこの更新されたバージョンでは、行の最後に追加のエントリ(削除ボタン)が表示されます。クリックすると、this.handleDelete を呼び出すように登録されます。handleDelete() 関数は、コンテキスト的に重要な this.props.employee レコードを提供しながら、渡されたコールバックを呼び出すことができます。

これも、1 つの場所で最上位コンポーネントの状態を管理するのが最も簡単であることを示しています。これは常にそうであるとは限りません。ただし、多くの場合、状態を 1 か所で管理すると、物事をまっすぐに簡単に保つことが容易になります。コンポーネント固有の詳細(this.props.onDelete(this.props.employee))でコールバックを呼び出すことにより、コンポーネント間の動作を非常に簡単に調整できます。

onDelete() 関数を App.onDelete の先頭にトレースすると、その動作を確認できます。

Unresolved directive in hypermedia/README.adoc - include::src/main/js/app.js[tag=delete]

ページベースの UI でレコードを削除した後に適用する動作は少し注意が必要です。この場合、サーバーからデータ全体をリロードし、同じページサイズを適用します。次に、最初のページが表示されます。

最後のページの最後のレコードを削除する場合、最初のページにジャンプします。

ページサイズの調整

ハイパーメディアが実際にどのように輝くかを確認する 1 つの方法は、ページサイズを更新することです。Spring Data REST は、ページサイズに基づいてナビゲーションリンクを流動的に更新します。

ElementList.render の上部には <input ref="pageSize" defaultValue={this.props.pageSize} onInput={this.handleInput}/> という HTML 要素があります。

  • ref="pageSize" を使用すると、this.refs.pageSize を使用してその要素を簡単に取得できます。

  • defaultValue は、状態の pageSize で初期化します。

  • 以下に示すように、onInput はハンドラーを登録します。

    Unresolved directive in hypermedia/README.adoc - include::src/main/js/app.js[tag=handle-page-size-updates]

イベントの泡立ちを停止します。次に、<input> の ref 属性を使用して、すべて React の findDOMNode() ヘルパー関数を介して DOM ノードを見つけ、その値を抽出します。入力が実際に数字であるかどうかは、数字の文字列であるかどうかを確認してテストします。その場合、コールバックを呼び出して、新しいページサイズを App React コンポーネントに送信します。そうでない場合は、入力した文字が入力から取り除かれます。

App は updatePageSize() を取得するとどうなるでしょうか?見てみな:

Unresolved directive in hypermedia/README.adoc - include::src/main/js/app.js[tag=update-page-size]

新しいページサイズの値により、すべてのナビゲーションリンクが変更されるため、データを再フェッチして最初から開始することをお勧めします。

すべてを一緒に入れて

これらすべての素晴らしい追加により、次のイメージに示すように、UI が大幅に改善されました。

hypermedia 1

ページサイズの設定は上部に、削除ボタンは各行に、ナビゲーションボタンは下部に表示されます。ナビゲーションボタンは、ハイパーメディアコントロールの強力な機能を示しています。

次のイメージでは、HTML 入力プレースホルダーにメタデータがプラグインされた CreateDialog を見ることができます。

hypermedia 2

これは、ドメイン駆動型メタデータ(JSON スキーマ)と組み合わせたハイパーメディアを使用する力を実際に示しています。Web ページは、どのフィールドがどのフィールドであるかを知る必要はありません。代わりに、ユーザーそれをて、それを使用する方法を知ることができますEmployee ドメインオブジェクトに別のフィールドを追加した場合、このポップアップは自動的にそれを表示します。

レビュー

本セクション :

  • Spring Data REST のページング機能を有効にしました。

  • ハードコードされた URI パスを捨て、関連名または「rels」と組み合わせたルート URI の使用を開始しました。

  • ページベースのハイパーメディアコントロールを動的に使用するように UI を更新しました。

  • 従業員を作成および削除し、必要に応じて UI を更新する機能を追加しました。

  • ページサイズを変更し、UI が柔軟に応答できるようにしました。

課題 ?

Web ページを動的にしました。ただし、別のブラウザタブを開いて、同じアプリをポイントします。1 つのタブを変更しても、他のタブは何も更新されません。

次のセクションでその課題に対処します。

<stdin> の未解決のディレクティブ -include::conditional/README.adoc [leveloffset = + 1] <stdin> の未解決のディレクティブ -include::events/README.adoc [leveloffset = + 1] <stdin> の未解決のディレクティブ -include::security/README.adoc [leveloffset = + 1]

新しいガイドを作成したり、既存のガイドに貢献したいですか? 投稿ガイドラインを参照してください (GitHub)

すべてのガイドは、コード用の ASLv2 ライセンス、およびドキュメント用の Attribution、NoDerivatives クリエイティブコモンズライセンス (英語) でリリースされています。