构建 React 应用程序
使用 React 1.x 简介创建具有 React 视图的 Grails 应用程序
作者:Zachery Klein
Grails 版本 3.3.5
1 Grails 培训
Grails 培训 - 由创建并积极维护 Grails 框架的人员开发和提供!
2 入门
在本指南中,你将使用 React 作为视图层创建一个 Grails 应用程序。你将使用 React 简介的 1.0.2 版本(单项目,使用 Webpack 和 Asset Pipeline)。
对初始项目做了一些更改(与使用 React 简介 1.0.2 版本的默认 Grails 项目相比)。提供包含更改内容的补丁文件,见指南库。
2.1 需要的东西
完成本指南,需拥有以下内容
-
手头有些时间
-
一个不错的文本编辑器或 IDE
-
安装有 JDK 1.8 或更高版本,并正确配置了
JAVA_HOME
2.2 如何完成本指南
要开始操作,请执行以下操作
-
下载并解压缩源文件
或
Grails 指南库包含两个文件夹
-
initial
初始项目,通常是一个简单的 Grails 应用程序,包含一些额外的代码,可让你轻松上手。 -
complete
一个完整的示例,它是完成指南介绍的步骤并将这些更改应用到initial
文件夹的结果。
要完成本指南,请转到initial
文件夹
-
在
grails-guides/building-a-react-app/initial
中使用cd
然后按照后续章节中的说明进行操作。
如果你在 grails-guides/building-a-react-app/complete 中使用 cd ,就可以直接转到完成的示例 |
3 编写应用程序
React 概要文件包括一些默认的 React 示例代码。如果你想查看正在操作的示例,可以随时按原样运行该应用程序。
让我们从为该应用程序创建域模型开始。
$ grails create-domain-class demo.Vehicle
$ grails create-domain-class demo.Driver
$ grails create-domain-class demo.Make
$ grails create-domain-class demo.Model
现在让我们在 grails-app/domain/demo/
中编辑我们的域类。我们将添加一些属性和 @Resource
注释。
package demo
import grails.rest.Resource
@Resource(uri = '/vehicle')
class Vehicle {
String name
Make make
Model model
static belongsTo = [driver: Driver]
static constraints = {
}
}
package demo
import grails.rest.Resource
@Resource(uri = '/driver')
class Driver {
String name
static hasMany = [ vehicles: Vehicle ]
static constraints = {
vehicles nullable: true
}
}
package demo
import grails.rest.Resource
@Resource(uri = '/make')
class Make {
String name
static constraints = {
}
}
package demo
import grails.rest.Resource
@Resource(uri = '/model')
class Model {
String name
static constraints = {
}
}
由于我们在域类中添加了 @Resource
注释,Grails 将为每个域类生成 RESTful URL 映射。让我们预装一些数据
package demo
import demo.Driver
import demo.Make
import demo.Model
import demo.Vehicle
class BootStrap {
def init = { servletContext ->
log.info "Loading database..."
def driver1 = new Driver(name: "Susan").save()
def driver2 = new Driver(name: "Pedro").save()
def nissan = new Make(name: "Nissan").save()
def ford = new Make(name: "Ford").save()
def titan = new Model(name: "Titan").save()
def leaf = new Model(name: "Leaf").save()
def windstar = new Model(name: "Windstar").save()
new Vehicle(name: "Pickup", driver: driver1, make: nissan, model: titan).save()
new Vehicle(name: "Economy", driver: driver1, make: nissan, model: leaf).save()
new Vehicle(name: "Minivan", driver: driver2, make: ford, model: windstar).save()
}
def destroy = {
}
}
4 运行应用程序
现在,如果运行我们的应用程序,我们可以试一试 Grails 为我们生成的 RESTful API。使用 Grails 3.2.4 的本地安装程序或提供的一个封装程序来启动应用程序:./gradlew bootRun
或 ./grailsw run-app
$ ./grailsw run-app
现在我们可以使用 cURL 或其他 API 工具来演练该 API。
对 /vehicle
进行 GET
请求来获取车辆列表
$ curl -X "GET" "http://localhost:8080/vehicle"
HTTP/1.1 200
X-Application-Context: application:development
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 06 Jan 2017 19:28:49 GMT
Connection: close
[{"id":1,"driver":{"id":1},"make":{"id":1},"model":{"id":1},"name":"Pickup"},
{"id":2,"driver":{"id":1},"make":{"id":1},"model":{"id":2},"name":"Economy"},
{"id":3,"driver":{"id":2},"make":{"id":2},"model":{"id":3},"name":"Minivan"}]
对 /driver/1
进行 GET
请求来获取特定驾驶员实例
$ curl -X "GET" "http://localhost:8080/driver/1"
HTTP/1.1 200
X-Application-Context: application:development
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 06 Jan 2017 22:10:33 GMT
Connection: close
{"id":1,"name":"Susan","vehicle":[{"id":2},{"id":1}]}
对 /driver
进行 POST
请求来创建新的驾驶员实例
$ curl -X "POST" "http://localhost:8080/driver" \
-H "Content-Type: application/json; charset=utf-8" \
-d '{"name":"Edward"}'
HTTP/1.1 201
X-Application-Context: application:development
Location: http://localhost:8080/driver/3
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 06 Jan 2017 21:55:59 GMT
Connection: close
{"id":3,"name":"Edward"}
对 /vehicle
进行 PUT
请求来更新车辆实例
$ curl -X "PUT" "http://localhost:8080/vehicle/1" \
-H "Content-Type: application/json; charset=utf-8" \
-d '{"name":"Truck","id":1}'
HTTP/1.1 200
X-Application-Context: application:development
Location: http://localhost:8080/vehicle/1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 06 Jan 2017 22:12:31 GMT
Connection: close
{"id":1,"driver":{"id":1},"make":{"id":1},"model":{"id":1},"name":"Truck"}
4.1 自定义 API
默认情况下,Grails 生成的 RESTful URL 仅提供关联对象的 ID。
HTTP/1.1 200
X-Application-Context: application:development
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 06 Jan 2017 23:55:33 GMT
Connection: close
{"id":1,"name":"Pickup","make":{"id":1},"driver":{"id":1}}
这足以满足许多用法,但我们需要尽快为我们的 React 组件获取更多数据。这是 JSON 视图 大显身手的地方。让我们创建一个新的 JSON 视图来渲染我们的车辆列表
$ mkdir grails-app/views/vehicle/
按照惯例,任何 restful 控制器(例如由 @Resource
生成的控制器)对应视图目录中的任何 JSON 视图都将代替默认的 JSON 表示形式使用。现在我们可以通过为车辆创建一个新的 JSON 模板来定制我们每个车辆的 JSON 输出
$ vim grails-app/views/vehicle/_vehicle.gson
编辑文件包括以下内容
import demo.Vehicle
model {
Vehicle vehicle
}
json {
id vehicle.id
name vehicle.name
make name: vehicle.make.name,
id: vehicle.make.id
model name: vehicle.model.name,
id: vehicle.model.id
driver name: vehicle.driver.name,
id: vehicle.driver.id
}
现在当访问我们的 API 时,我们将看到每个 make
、model
和 driver
的name
和 id
已包括在内。
HTTP/1.1 200
X-Application-Context: application:development
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 07 Jan 2017 00:24:18 GMT
Connection: close
{"id":1,"name":"Pickup","make":{"name":"Nissan","id":1},"model":{"name":"Titan","id":1},"driver":{"name":"Susan","id":1}}
5 编写我们的 React 组件
现在我们可以开始创建我们的 React 组件来查看和交互我们的 API 了。我们暂时只处理列出车辆和创建新实例。
5.1 编写 Vehicles 组件
让我们从 Vehicles
组件开始,它将渲染我们的车辆实例的表格。
在 src/main/webapp/app/
下创建名为 Vehicles.js
的 JavaScript 文件。
以下是我们的 Vehicle
组件
import React from 'react';
import {Table} from 'react-bootstrap';
import {array} from 'prop-types';
class Vehicles extends React.Component {
render() {
function renderVehicleRow(vehicle) {
return (<tr key={vehicle.id}>
<td>{vehicle.id}</td>
<td>{vehicle.name}</td>
<td>{vehicle.make.name}</td>
<td>{vehicle.model.name}</td>
<td>{vehicle.driver.name}</td>
</tr>);
}
return <div>
<Table striped bordered condensed hover>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Make</th>
<th>Model</th>
<th>Driver</th>
</tr>
</thead>
<tbody>
{this.props.vehicles.map(renderVehicleRow)}
</tbody>
</Table>
</div>;
}
}
Vehicles.propTypes = {
vehicles: array
};
export default Vehicles;
请注意,在 renderVehicleRow
函数中,我们访问了我们 Vehicle 实例的自定义 JSON 表示形式,如,我们可以使用 vehicle.make.name
。
React 默认包括 React-Bootstrap,所以我们正在使用 Bootstrap Table
组件来简化代码。
5.2 编写 Garage 应用程序
现在,我们需要授权 Vehicles
组件访问来自我们的 API 的数据。我们可以从 Vehicles
组件内部来执行这一操作,但是一个更灵活的选择是创建一个“容器”组件,该组件将从 API 获取数据并通过 props 将其传递给“表示”组件(在本例中为 Vehicles
)。
使用这种分离的方法,我们可在将来扩展该应用程序以发出更多 API 调用(甚至呈现不同的组件,如,一个 Drivers
表),而不必杂乱我们的 Vehicles
组件。我们称我们的“容器”组件为 Garage
。
在 src/main/webapp/app/
下创建名为 Garage.js
的 JavaScript 文件。
以下是我们的 Garage
组件
import React from 'react';
import ReactDOM from 'react-dom';
import Vehicles from './Vehicles';
class Garage extends React.Component {
constructor() {
super();
this.state = {
vehicles: [{"id":1,"name":"Pickup","make":{"name":"Nissan","id":1},"model":{"name":"Titan","id":1},"driver":{"name":"Susan","id":1}}],
}
}
render() {
const {vehicles} = this.state;
return <div>
<Vehicles vehicles={vehicles} />
</div>;
}
}
ReactDOM.render(<Garage />, document.getElementById('garage'));
Garage
组件使用一个 state
对象,该对象可供所有 React 组件使用,但是为可选项。我们不需要在我们的 Vehicles
组件中使用 state
,因为它通过 vehicles
props 接收了所有数据。在编写 React 时,一个好的做法是集中化数个组件(甚至一个组件)中的状态,并将相关数据片段传递给子组件。
请注意,我们正在 state
的 vehicles
集合中硬编码一个 JSON 对象 - 那是因为我们尚未设置 API 调用 - 几个章节后,我们将讨论这一部分。
Garage
组件包含对 ReactDOM.render
的调用,以便将组件呈现在页面上。现在,我们将创建一个新页面,从中加载我们的 React 组件。
5.3 呈现应用程序
React 1.0.2 配置文件依赖于 Webpack 将 React 组件捆绑到对浏览器就绪的 JavaScript 捆绑中,然后可以通过 Grails Asset Pipeline 加载它们。默认应用程序包含一个“index”捆绑,其在索引页上呈现。我们为 Garage
应用程序设置一个新的捆绑。
在 webpack.config.js
中,编辑 entry
部分,并添加一行来加载我们的 Garage.js
文件,如下所示
var path = require('path');
module.exports = {
entry: {
index: './src/main/webapp/index.js', (1)
garage: './src/main/webapp/app/Garage.js' (2)
},
//...
1 | 将通往 Garage.js 的路径作为 garage 入口点添加 |
2 | 别忘记逗号! |
这将导致 Webpack 捆绑两个不同的 React 应用,“index”和“garage”。我们还需要配置 Webpack 为每个 React 应用输出单独的捆绑包,以便我们可以在 Grails 应用程序的不同页面上加载它们。
在 webpack.config.js
中,修改 output
部分,并按如下所示更改 filename
行
//...
output: {
path: path.join(__dirname, 'grails-app/assets/javascripts'),
publicPath: '/assets/',
filename: 'bundle-[name].js'
},
//...
1 | 将 -[name] 添加到 filename 属性 |
现在,当我们启动 Grails 应用(或运行 ./gradlew webpack
)时,Webpack 将生成两个捆绑包,一个是名为 bundle-index.js
的捆绑包,另一个名为 bundle-Garage.js
的捆绑包。我们可以使用 Grails Asset Pipeline 标签在我们的页面上加载这些捆绑包。
由于我们更改了捆绑包的 filename,我们需要快速更新我们的原始 编辑
|
现在,我们终于准备好为我们的 Garage
应用创建一个主页。使用本地 Grails 安装或 ./grailsw
创建一个新的 Grails 控制器
./grailsw create-controller demo.GarageController
确保 GarageController
包含一个 index
动作。
package demo
class GarageController {
def index() { }
}
现在,在 grails-app/views/garage
下创建一个简单的 index.gsp
页面
<!doctype html>
<html>
<head>
<meta name="layout" content="main"/>
<title>Garage</title>
<asset:link rel="icon" href="favicon.ico" type="image/x-ico" />
</head>
<body>
<div id="content" role="main">
<section class="row colset-2-its">
<div id="garage"></div>
<asset:javascript src="bundle-garage.js" />
</section>
</div>
</body>
</html>
重新启动应用程序,并浏览至 http://localhost:8080/garage
。您应该看到我们的新 React 应用加载在页面上,其中包含一行硬编码的数据。
5.4 从 API 中获取数据
现在我们的 Garage
组件已设置完毕,并在我们的页面上呈现 Vehicles
表格,我们终于可以将我们的 API 连接起来,将数据加载到我们的 React 视图中。为此,我们将使用 fetch
API。
编辑 src/main/webapp/app/Garage.js
//...
import 'whatwg-fetch'; (1)
class Garage extends React.Component {
constructor() {
super();
this.state = {
vehicles: [] (2)
}
}
componentDidMount() { (3)
fetch('/vehicle')
.then(r => r.json())
.then(json => this.setState({vehicles: json}))
.catch(error => console.error('Error retrieving vehicles: ' + error));
}
//...
1 | 导入 fetch 库 |
2 | 删除硬编码数据。 |
3 | 从 API 加载数据 |
componentDidMount
是 React 组件生命周期方法之一。它在组件在页面上加载后立即触发。在此方法中,我们使用 fetch
对我们的 /vehicle
终结点发出请求(默认情况下为 GET 请求),解析 JSON 有效负载,并调用 this.setState
使用数据更新我们的 vehicles
集合。
重新启动应用程序(或等待 webpack 重新加载)以查看更改。您现在应该看到 Grails 应用中的 Vehicles 列表显示在 React Vehicles
表格中。
5.5 向 API 发布数据
本指南中的我们的最后一步是创建一个简单的表单来将新的 Vehicle
实例发布到我们的 API。
在 src/main/webapp/app
下创建一个名为 AddVehicleForm.js
的新 JavaScript 文件,内容如下
import React from 'react';
import {array, func} from 'prop-types';
class AddVehicleForm extends React.Component {
constructor(props) {
super(props);
this.state = { (1)
name: '',
make: {id: ''},
model: {id: ''},
driver: {id: ''}};
}
handleSubmit = (event) => { (2)
event.preventDefault();
const {name, make, model, driver} = this.state;
if (!name || !make.id || !model.id || !driver.id) {
console.warn("missing required field!");
return;
}
this.props.onSubmit( {name, make, model, driver} ); (3)
this.setState({ name: '', make: {id: ''}, model: {id: ''}, driver: {id: ''}});
};
handleNameChange = (event) => { (4)
this.setState({ name: event.target.value });
};
handleMakeChange = (event) => { (4)
this.setState({ make: {id: event.target.value} });
};
handleModelChange = (event) => { (4)
this.setState({ model: {id: event.target.value} });
};
handleDriverChange = (event) => { (4)
this.setState({ driver: {id: event.target.value} });
};
render() {
function renderSelectList(item) { (5)
return <option key={item.id} value={item.id}>{item.name}</option>
}
return(
<div>
<h3>Add a Vehicle:</h3>
<form className="form form-inline" onSubmit={this.handleSubmit} >
<label>Name</label>
<input className="form-control" name="name" type="text" value={ this.state.name } onChange={ this.handleNameChange } />
<label>Make</label>
<select className="form-control" name="make" value={this.state.make.id}
onChange={this.handleMakeChange}> {/*<6>*/}
<option value={null}>Select a Make...</option>
{this.props.makes.map(renderSelectList)} {/*<5>*/}
</select>
<label>Model</label>
<select className="form-control" name="model" value={this.state.model.id}
onChange={this.handleModelChange}> {/*<6>*/}
<option value={null}>Select a Model...</option>
{this.props.models.map(renderSelectList)} {/*<5>*/}
</select>
<label>Driver</label>
<select className="form-control" name="driver" value={this.state.driver.id}
onChange={this.handleDriverChange}> {/*<6>*/}
<option value={null}>Select a Driver...</option>
{this.props.drivers.map(renderSelectList)} {/*<5>*/}
</select>
<input className="btn btn-success" type="submit" value="Add to library" />
</form>
</div>
);
}
}
AddVehicleForm.propTypes = {
makes: array,
models: array,
drivers: array,
onSubmit: func
};
export default AddVehicleForm;
1 | 使用填充新 Vehicle 所需的所有属性初始化 state 对象 |
2 | 创建事件处理程序来处理表单提交 |
3 | 将 state 中的属性传递给 onSubmit 回调函数 |
4 | 当接收到用户输入时更新 state 的事件处理程序 |
5 | 使用我们道具中的数组在 select 列表中渲染选项 |
6 | 在用户更改输入值时调用事件处理程序 |
这是一个相当复杂的组件,所以如果您立即没弄明白,也不必担心。关键点在于 AddVehicleForm
组件允许用户设置创建新车辆实例所需 4 个属性:name
、make
、model
和 driver
。它获取名为 onSubmit
的函数道具,当提交表单时该函数被使用。
在 React 中将函数(处理程序)作为道具传递的这种方式是个好习惯。它允许轻松复用组件,因为特定功能可以通过不同的调用方(例如,通过将不同函数作为 onSubmit 道具传递)进行交换。与 state 类似的是,在几个组件中集中您的功能逻辑,并将这些函数作为道具传递给子组件,是使用 React 时一个好习惯。对于此模式更语义化的版本,您可以考虑 Flux 实现(例如 Redux),以将您的 state 和逻辑外部化。 |
由于 make
、model
和 driver
是关联,我们需要允许用户选择 ID,以便 Grails 可以在数据绑定期间进行赋值。AddVehicleForm
需要 3 个道具,预期其中包含这些关联的数组。我们需要提供这些道具才能使用 AddUserForm
,所以我们来编辑 Garage
组件以检索这些列表。
编辑 src/main/webapp/app/Garage.js
//..
import AddVehicleForm from './AddVehicleForm'; (1)
class Garage extends React.Component {
constructor() {
super();
this.state = {
vehicles: [],
makes: [], (2)
models: [],
drivers: []
}
}
componentDidMount() {
fetch('/vehicle')
.then(r => r.json())
.then(json => this.setState({vehicles: json}))
.catch(error => console.error('Error retrieving vehicles: ' + error));
fetch('/make') (3)
.then(r => r.json())
.then(json => this.setState({makes: json}))
.catch(error => console.error('Error retrieving makes: ' + error));
fetch('/model') (3)
.then(r => r.json())
.then(json => this.setState({models: json}))
.catch(error => console.error('Error retrieving models ' + error));
fetch('/driver') (3)
.then(r => r.json())
.then(json => this.setState({drivers: json}))
.catch(error => console.error('Error retrieving drivers: ' + error));
}
render() {
const {vehicles, makes, models, drivers} = this.state; (4)
return <div>
<AddVehicleForm makes={makes} models={models} drivers={drivers}/> (5)
<Vehicles vehicles={vehicles} />
</div>;
}
}
//...
1 | 导入 AddVehicleForm 组件 |
2 | 将 makes 、models 和 drivers 添加到 state |
3 | 从 API 检索数据 |
4 | 使用 ES6 解构语法从 this.state 检索车辆、制造商、型号、驾驶员 |
5 | 将 makes 、models 和 drivers 传递给 AddVehicleForm |
最后一步是实现一个函数,我们通过 onSubmit
道具将其传递给 AddVehicleForm
。此函数需要完成两件事
-
将新车辆详细信息发布到 API,并从 API 检索结果
-
更新
state
,以便我们可以在Vehicles
表格中显示新创建的车辆
让我们来实现此函数。再次编辑 src/main/webapp/app/Garage.js
//..
class Garage extends React.Component {
//...
submitNewVehicle = (vehicle) => { (1)
fetch('/vehicle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(vehicle)
}).then(r => r.json())
.then(json => {
let vehicles = this.state.vehicles;
vehicles.push({id: json.id, name: json.name, make: json.make, model: json.model, driver: json.driver});
this.setState({vehicles});
})
.catch(ex => console.error('Unable to save vehicle', ex));
};
render() {
const {vehicles, makes, models, drivers} = this.state;
return <div>
<AddVehicleForm onSubmit={this.submitNewVehicle} (2)
makes={makes} models={models} drivers={drivers}/>
<Vehicles vehicles={vehicles} />
</div>;
}
}
ReactDOM.render(<Garage />, document.getElementById('garage'));
//...
1 | 创建 submitNewVehicle 函数 |
2 | 将函数作为 onSubmit 道具传递给 AddVehicleForm |
再次,我们使用fetch
API,这次用于向 /vehicle
端点的 POST 请求。调用 JSON.stringify
将从 AddVehicleForm
接收的参数转换为 JSON 字符串,然后可以将其发布到我们的 Grails API。API 将返回新创建的 vehicle 实例,我们随后可以用 this.setState
将其解析并插入到我们的 state
对象中。
重新启动应用,或重新运行 webpack,您应该可以创建新的 Vehicle 实例,并看到它们已添加到表格中。刷新该页面以确认已将新实例持久到数据库中。
6 后续步骤
有很多机会可以扩展此应用程序的范围。以下是您自己可以改进的一些想法
-
创建模态对话框表单以添加新司机、生产商和型号。使用 React-Bootstrap 的
Modal
组件,为您提供一个良好开端。 -
添加对现有 Vehicle 的更新支持。模态对话框可能也适用于此,或可能需要可编辑的表格行
-
当前,生产商和型号是独立的。在生产商和型号之间添加适当的 GORM 关联,并将选择列表更改为仅显示当前选择的生产商的型号。您将需要使用 JavaScript
Array.filter
方法。