使用 Grails 构建 Vue.js 应用程序
了解如何在应用程序中添加 Vue.js 前端
作者:Ben Rhine、Zachary Klein
Grails 版本 3.3.3
1 培训
Grails 培训 - 由创建并积极维护 Grails 框架的团队开发和提供!。
2 开始使用
在本指南中,我们将使用 Vue 配置文件 构建一个具有 Vue.js 应用程序作为前端的 Grails 应用程序。示例项目将是 Garage
应用程序,如 React 和 Vaadin 指南中所示。你可以参考这些指南,将 Vue.js 版本进行比较。
2.1 所需内容
要完成本指南,你需要以下内容
-
时间
-
一款出色的文本编辑器或 IDE
-
安装了 JDK 1.8 或更高版本,并且正确配置了
JAVA_HOME
2.2 如何完成指南
若要开始,请执行以下操作
-
下载并解压源代码
或
-
克隆 Git 存储库
git clone https://github.com/grails-guides/building-a-vue-app.git
Grails 指南存储库包含两个文件夹
-
initial
初始项目。通常是一个简单的 Grails 应用程序,带有一些额外代码,以便你能抢先上手。 -
complete
一个完成的示例。这是完成指南呈现的步骤,并将这些更改应用于initial
文件夹的结果。
若要完成指南,请转到 initial
文件夹
-
cd
到grails-guides/building-a-vue-app/initial
并在后续章节中按照说明进行操作。
如果你 cd 到 grails-guides/building-a-vue-app/complete ,则可以直接转到已完成的示例 |
3 运行应用程序
Vue 配置文件生成多项目架构,其中包含 server
和 client
子项目。server
项目是使用 rest-api
配置文件的 Grails 应用程序,而 client
是使用 Vue-CLI 的 webpack 模板生成的 Vue 应用程序。为了运行整个项目,你需要分别启动 server
和 client
应用程序。
转到 initial
目录
$ cd initial/
为了启动 Grails 应用程序,请运行以下命令
$ ./gradlew server:bootRun
Grails 应用程序将在 http://localhost:8080
提供
为了启动 Vue.js 应用程序,请在同一目录中打开一个第二个终端会话,并运行以下命令
$ ./gradlew client:start
Vue.js 应用程序将在 http://localhost:3000
提供。浏览到该 URL,你应该会看到默认的“Welcome”页面。
4 构建服务器
创建以下四个领域类。
$ grails create-domain-class demo.Vehicle
$ grails create-domain-class demo.Driver
$ grails create-domain-class demo.Make
$ grails create-domain-class demo.Model
以以下方式编辑领域类
package demo
import grails.rest.Resource
@Resource(uri = '/vehicle')
class Vehicle {
String name
Make make
Model model
static belongsTo = [driver: Driver]
}
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
}
package demo
import grails.rest.Resource
@Resource(uri = '/model')
class Model {
String name
}
由于我们向领域类添加了 @Resource
注释,因此 Grails 将为它们中的每一个生成 RESTful URL 映射。让我们借助 GORM 数据服务来预加载一些数据。
package demo
import grails.gorm.services.Service
@Service(Make)
interface MakeDataService {
Make save(String name)
}
package demo
import grails.gorm.services.Service
@Service(Model)
interface ModelDataService {
Model save(String name)
}
package demo
import grails.gorm.services.Service
@Service(Driver)
interface DriverDataService {
Driver save(String name)
}
package demo
import grails.gorm.services.Service
@Service(Vehicle)
interface VehicleDataService {
Vehicle save(String name, Driver driver, Make make, Model model)
}
package demo
import groovy.transform.CompileStatic
@CompileStatic
class BootStrap {
DriverDataService driverDataService
MakeDataService makeDataService
ModelDataService modelDataService
VehicleDataService vehicleDataService
def init = { servletContext ->
log.info "Loading database..."
Driver driver1 = driverDataService.save("Susan")
Driver driver2 = driverDataService.save("Pedro")
Make nissan = makeDataService.save("Nissan")
Make ford = makeDataService.save("Ford")
Model titan = modelDataService.save("Titan")
Model leaf = modelDataService.save("Leaf")
Model windstar = modelDataService.save("Windstar")
vehicleDataService.save("Pickup", driver1, nissan, titan)
vehicleDataService.save("Economy", driver1, nissan, leaf)
vehicleDataService.save("Minivan", driver2, ford, windstar)
}
def destroy = {
}
}
重新启动 server
项目以在默认数据源中加载测试数据。
如果你希望使用 Grails 封装程序 ./grailsw run-app (而非 Gradle 封装程序)来运行 server 应用程序,则请确保启动应用程序时你处于 server 目录中。 |
4.1 测试 API
当 Grails 应用程序正在运行时,我们可以使用 cURL 或另一个 API 工具,来试验 Grails 为我们生成的 RESTful 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"}]
发出一个 GET
请求到 /driver/1
,可以获取一个特定的司机实例
$ 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}]}
发出一个 POST
请求到 /driver
,可以创建一个新的司机实例
$ 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"}
发出一个 PUT
请求到 /vehicle
,可以更新一个汽车实例
$ 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.2 自定义 API
默认情况下,由 Grails 生成的 RESTful URL 仅提供关联的对象的 ID。
$ 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 23:55:33 GMT
Connection: close
{"id":1,"name":"Pickup","make":{"id":1},"driver":{"id":1}}
这是许多 REST API 的标准,但我们马上需要从这个端点获取更多数据来使用 Vue 应用程序。对于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 构建客户端
在这一点上,我们的 server
项目已经完成,我们可以开始构建我们的 Vue.js client
应用程序了。让我们概述一下 Vue 配置文件和 Vue-CLI 提供的项目结构。
5.1 默认 Vue.js 应用程序布局
Vue-CLI 项目包含运行、测试和构建 Vue.js 项目所需的所有配置。它遵循 Vue.js 社区推荐的约定。
build
目录包含 webpack 配置文件,包括 dev、prod 和 test 环境的特定于环境的配置。
config
目录包含非构建相关的配置,包括如上所述的特定于环境的配置。我们要关注的一个配置属性是 SERVER_URL
,它指向 Grails 的 URL server
应用程序,默认设置为 http://localhost:8080
。如有需要,你可以编辑它来指向另一个服务器。
虽然它们具有类似的功能,但请记住,Vue/webpack 环境设置与 Grails 环境没有直接关系 - 例如,如果你用特定的环境运行/构建你的 Grails 项目,那么它将不会自动影响 client 项目中的环境。 |
static
目录包含不应由 webpack 处理的静态资源 - 你将不会在本指南中使用它。
test
目录包含 Vue.js 应用程序的单元和集成(端到端)测试。
src
目录包含我们的 Vue.js 项目的实际源代码。它包含组件、资产(例如 CSS 文件)和默认的 Vue-Router 配置的子目录。这是 Vue.js 应用程序的典型项目结构,但是你可以在src
目录内应用适合你需要的任何目录结构。在本文档的余下部分,我们将在该目录中花费大部分时间。
5.2 Vue 组件
单文件组件
我们将创建多个 Vue 组件来使用/与我们的 API 交互。在本指南中,我们将使用单文件组件。单文件组件允许我们在一个文件中封装模板(HTML)、样式和组件的 Vue 实例(它处理组件的数据和行为)。这些文件的扩展名为.vue
。
单文件组件需要一些附加处理才能在浏览器中呈现。Vue 配置文件提供的client
项目已配置为正确编译单文件组件。
组件已导出,可以导入到其他组件中。他们还可以接受props、触发和响应事件,并且包含内部数据(就像所有 Vue 组件一样)。参考Vue.js 文档以了解有关单文件组件的更多信息。
5.3 UI 头部组件
我们的第一个组件将是头部栏。在client/src/components
下新建一个名为AppHeader.vue
的文件,并按如下所示编辑
<template id="app-header-template">
<div class="container">
<div class="jumbotron">
<img src="../assets/logo.png">
<h1>Welcome to the Garage</h1>
</div>
</div>
</template>
<script>
export default {
name: 'app-header'
}
</script>
<style>
</style>
<template>
包含将由组件呈现的 HTML 模板。在上面的示例中,我们正在呈现一个<div>
标签来表示我们 UI 的主标题,包括一个横幅图片和<h1>
标签。
每个<template> 都必须只包含一个根级别元素。 |
在<script>
标签中,我们以模块的形式导出了一个 JavaScript 对象。该对象将用作 Vue 组件的实例定义,并用于为组件提供数据和行为。在这种情况下,我们的组件完全是表示性的,所以在这个对象中我们没有太多内容。我们将在本指南的后面看到更多有关此对象中可用功能的示例。
单文件组件的最后一部分是<style>
标签。在这里,你可以指定组件特定的 CSS 规则。这些规则将“限定”到组件的模板,并且不会影响任何其他 HTML 元素。
在client/src/components/form
下创建一个名为VehicleFormHeader.vue
的新文件(如果需要,创建一个form
目录),并按如下所示编辑
<template id="add-vehicle-header-template">
<div id="headerRow" class="row">
<div class="col">
<h3>Add a Vehicle:</h3>
</div>
<div class="col"></div>
<div class="col"></div>
</div>
</template>
<script>
export default {
name: 'vehicle-form-header'
}
</script>
<style>
</style>
默认情况下,当你在模板中使用组件时,元素名称将是组件的名称,用连字符连接。例如,AppHeader 将变为<app-header> 。 |
5.4 选择和表格组件
选择组件
当创建新车辆时,我们需要一个通用的 <select>
,允许用户从可用的 Make
、Model
和 Driver
记录中进行选择。
在 client/src/components/form
下创建文件 FieldSelect.vue
,并按照如下所示编辑内容
<template id="driverSelect-template" xmlns="http://www.w3.org/1999/xhtml">
<div class="form-group"> (4)
<select class="form-control" v-model="selected" @change="updateValue()"> (7)
<option disabled :value="null">Select a {{field}}</option> (2)
(1)
<option v-for="value in values" :value="value.id" :key="value.id">
{{ value.name }}
</option>
</select>
</div>
</template>
<script>
export default {
(1)
props: ['values', 'field'], (2)
data: function () { (2)
return {
selected: null (3)
}
},
methods: { (5)
updateValue: function () { (6)
this.$emit('input', this.selected)
}
}
}
</script>
<style>
/* Add custom rules here */
</style>
1 | 使用 values 声明一个 prop,该 prop 将显示我们选择的对象列表,并将作为 HTML 属性传递到组件中。例如,<field-select values="[obj1,obj2,obj3]"/> 。 |
2 | 第二个 prop 被命名为 field ,它将作为正在选择字段的可读名称(它用作默认的“无选择”选项)。 |
3 | data() 函数返回一个对象,该对象将成为组件的初始数据(或状态)。在这种情况下,我们 data 中只有一个变量,即 selected ,它将存储选择列表的当前值。 |
4 | v-model 指令在元素的“value”和 data 中的变量之间设置双向绑定。当值发生更改时,模型变量(selected )将被更新,反之亦然。 |
5 | methods 是一个包含任意 JavaScript 函数的对象,可以从模板或组件中的其他方法中进行调用。 |
6 | updateValue 方法发出一个事件,这样父组件就能对此组件中的更改做出响应。在这种情况下,我们发出 selected 的值,它将是列表中用户选择的选项。 |
7 | 我们使用 updateValue 方法作为 <select> 元素的 onChange 事件的事件处理程序,使用 @change 属性(还支持其他事件,诸如 @click 、@focus 等)。 |
单向与双向数据绑定
Vue.js 支持单向和双向数据绑定,而此组件展示了这两种方法。当在模板表达式中使用了数据变量( 但是,如果一个元素使用了 这种灵活性意味着你既可以使用单向数据绑定,也可以使用双向数据绑定来开发 Vue.js,并在适当的时候结合使用。通常情况下,单向数据绑定会生成更简单、更可预测的代码。但是,双向数据绑定很方便,并且可以简化创建对应于组件数据的具有多个字段的表单。Vue.js 让开发者自己做出选择。 |
表组件
下一对组件将用于在我们的用户界面中展示车辆表。它们是展示组件,因此不需要任何方法或事件处理。
在 `client/src/components/table/` 下创建一个名为 `TableRow.vue` 的新文件,并添加以下内容
<template id="tablerow-template" xmlns="http://www.w3.org/1999/xhtml">
<tr> <!-- 1 -->
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.make.name }}</td>
<td>{{ item.model.name }}</td>
<td>{{ item.driver.name }}</td>
</tr>
</template>
<script>
export default {
props: ['item'] (1)
}
</script>
<!-- Per Component Custom CSS Rules -->
<style>
/* Add custom rules here */
</style>
1 | 此组件接受一个 `item` 的单个属性,该属性保存要在模板中呈现的记录。 |
在 `client/src/components/table/` 下创建一个名为 `VehicleTable.vue` 的新文件,并添加以下内容
<template id="fulltable-template" xmlns="http://www.w3.org/1999/xhtml">
<table class="table">
<thead class="thead-inverse">
<tr>
<th>ID</th>
<th>Name</th>
<th>Make</th>
<th>Model</th>
<th>Driver</th>
</tr>
</thead> (1)
<table-row v-for="vehicle in vehicles"
:item="vehicle" :key="vehicle.id"></table-row> (2)
</table>
</template>
<script>
import TableRow from './TableRow.vue' (3)
export default {
props: ['vehicles'],
components: { (3)
TableRow
}
}
</script>
<style>
/* Add custom rules here */
</style>
1 | `v-for` 指令允许我们遍历数组,类似于 GSP `<g:each>` 标记或 Angular 中的 `ng-for` 指令。 |
2 | 同样,我们使用 `:item` 语法将 `vehicle` 对象绑定到 `<table-row>` 组件的 `item` 属性。请注意,我们还绑定到 `:key` 属性 - 类似于 React,使用 `v-for` 迭代元素要求每个元素都有一个唯一的 `key`,在我们的例子中是 `vehicle.id`。 |
3 | 为了使用我们的 `<table-row>` 组件,我们在 `<script>` 标记的顶部导入它,然后在实例定义的 `components` 对象中指定它。 |
请注意,`components` 对象不是一个数组,而是一个对象。当键和值同名时,我们会使用一些 ES6 速记来组合它们。换句话说,上面的 `components` 对象和以下代码完全相同
当然,这假设你希望为该组件使用与该组件的 `.vue` 文件(在我们示例中是 `TableRow.vue`)相同的名称。如果不是,你可以使用此对象在模板中有效地“重命名”该组件 请注意,`components` 对象不是一个数组,而是一个对象。当键和值同名时,我们会使用一些 ES6 速记来组合它们。换句话说,上面的 `components` 对象和以下代码完全相同
|
5.5 表单组件
在把所有内容连接在一起之前,我们使用的最后一个组件将是一个采用我们在 API 中预先填入的司机、制造商和型号来创建新车辆的表单。与我们迄今为止创建的组件相比,这是一个稍复杂的组件,但它建立在我们已经看到的相同特性之上。
在 `client/src/components/form` 下创建一个名为 `VehicleForm.vue` 的新文件,并按以下内容编辑
<template id="add-vehicle-template" xmlns="http://www.w3.org/1999/xhtml">
<div>
<vehicle-form-header/> (1)
<div id="inputRow" class="row">
<div class="col-sm-3">
<div class="input-group">
<input type="text" class="form-control" placeholder="Enter a name..." v-model="vehicle.name"> (2)
</div>
</div>
<div class="col-sm-7">
<div class="row">
<div class="col-sm-4">
<field-select v-model="vehicle.make" :field="'Make'" :values="makes"></field-select> (3)
</div>
<div class="col-sm-4">
<field-select v-model="vehicle.model" :field="'Model'" :values="models"></field-select> (4)
</div>
<div class="col-sm-4">
<field-select v-model="vehicle.driver" :field="'Driver'" :values="drivers"></field-select>
</div>
</div>
</div>
<div class="col-sm-2">
<div class="btn-group" role="group" aria-label="Add new vehicle">
<button type="button" class="btn btn-success" @click="submit()">Add to Garage</button> (5)
</div>
</div>
</div>
</div>
</template>
<script>
import VehicleFormHeader from './VehicleFormHeader'
import FieldSelect from './FieldSelect'
export default {
props: ['vehicle', 'makes', 'models', 'drivers'], (6)
model: {
prop: 'vehicle', (4)
event: 'change'
},
components: {
VehicleFormHeader,
FieldSelect
},
methods: {
submit () { (5)
this.$emit('submit')
}
}
}
</script>
<style>
/* Add custom rules here */
</style>
1 | 这是我们之前创建的 `VehicleFormHeader` 组件。 |
2 | 同样,我们使用 `v-model` 指令将输入值绑定到 `data` 中的变量。 |
3 | 这是我们之前创建的 FieldSelect 组件 - 请注意我们正在使用 v-model 指令进行双向绑定(允许组件更新我们的数据),以及使用单向数据绑定传递一系列的 :values 。 |
4 | 由于我们以通用方式编写 FieldSelect 组件,所以我们可以将其重用于表单中的每个选择列表。 |
5 | 请注意,我们实际上并没有在此组件中进行 POST 调用来创建车辆 - 该任务将委派给父组件,方法是发出一个 submit 事件(如在 submit() 方法中完成的那样)。 |
6 | vehicle prop 表示正在从此表单中的字段创建的“新”车辆对象。makes 、models 和 drivers prop 将用于填充选择组件的记录列表。 |
下一步
在这一点上,我们拥有构建表单和在表中显示车辆所需的所有组件。我们仍然需要实现 API 集成,然后将所有这些部分组合到一个正常运行的应用程序中。
5.6 车辆显示
在 client/src/components/
中创建一个名为 Garage.vue
的新文件,并对其进行如下编辑
<template>
<div id="garage">
<app-header></app-header>
<vehicle-form v-model="vehicle"
:makes="makes"
:models="models"
:drivers="drivers"
@submit="submitNewVehicle()">
</vehicle-form>
<vehicle-table :vehicles="vehicles"></vehicle-table>
</div>
</template>
<script>
import AppHeader from './AppHeader'
import VehicleForm from './form/VehicleForm'
import VehicleTable from './table/VehicleTable'
export default {
components: {
AppHeader,
VehicleForm,
VehicleTable
},
data: function () {
return {
vehicles: [],
vehicle: {name: '', make: null, model: null, driver: null},
models: [],
makes: [],
drivers: [],
serverURL: process.env.SERVER_URL
}
},
created () {
this.fetchData()
},
methods: {
fetchData: async function () {
try {
await Promise.all([
this.fetchVehicles(),
this.fetchModels(),
this.fetchModels(),
this.fetchMakes(),
this.fetchDrivers()
])
} catch (error) {
console.log(error)
}
},
fetchVehicles: function () {
fetch(`${this.serverURL}/vehicle`)
.then(r => r.json())
.then(json => { this.vehicles = json })
.catch(error => console.error('Error retrieving vehicles: ' + error))
},
fetchModels: function () {
fetch(`${this.serverURL}/model`)
.then(r => r.json())
.then(json => { this.models = json })
.catch(error => console.error('Error retrieving models: ' + error))
},
fetchMakes: function () {
fetch(`${this.serverURL}/make`)
.then(r => r.json())
.then(json => { this.makes = json })
.catch(error => console.error('Error retrieving makes: ' + error))
},
fetchDrivers: function () {
fetch(`${this.serverURL}/driver`)
.then(r => r.json())
.then(json => { this.drivers = json })
.catch(error => console.error('Error retrieving drivers: ' + error))
},
submitNewVehicle: function () {
const vehicle = this.vehicle
fetch(`${this.serverURL}/vehicle`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(vehicle)
}).then(r => r.json())
.then(json => {
this.vehicles.push(json)
this.vehicle = {name: '', make: null, model: null, driver: null}
})
.catch(ex => console.error('Unable to save vehicle', ex))
}
}
</script>
<!-- Per Component Custom CSS Rules -->
<style>
#garage {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
}
</style>
分解它
因为这是一个大型组件,我们将分部分进行。
<template>
<div id="garage">
<app-header></app-header>
<vehicle-form v-model="vehicle"
:makes="makes"
:models="models"
:drivers="drivers"
@submit="submitNewVehicle()"> (1)
</vehicle-form>
<vehicle-table :vehicles="vehicles"></vehicle-table> (2)
</div>
</template>
1 | 我们已经将 submitNewVehicle() 方法(稍后我们将看到)设置为 submit 事件(我们在 VehicleForm.submit() 函数中发出的)的事件处理程序。 |
2 | 我们将 vehicles 数据变量绑定到 VehicleTable 组件的 vehicles prop。 |
<script>
import AppHeader from './AppHeader' (1)
import VehicleForm from './form/VehicleForm'
import VehicleTable from './table/VehicleTable'
export default {
components: { (1)
AppHeader,
VehicleForm,
VehicleTable
},
data: function () { (2)
return {
vehicles: [],
vehicle: {name: '', make: null, model: null, driver: null},
models: [],
makes: [],
drivers: [],
serverURL: process.env.SERVER_URL (3)
}
},
1 | 在模板中导入我们的组件供使用 |
2 | 我们的 data() 函数返回组件的初始状态。重要的是我们在 data 对象中初始化我们打算使用的所有变量,因为如果我们在之后添加一个变量,它将不被视为一个响应式属性(即对该变量的更改不会触发组件更新)。 |
3 | SERVER_URL 是在 client/config/dev.env.js 中设置的一个配置变量(test 和 prod 环境有等效的配置文件)。你可以通过更改 SERVER_URL 变量来更改以下 API 调用的基本 URL。 |
created () { (1)
this.fetchData()
},
methods: {
fetchData: async function () { (2)
try {
Promise.all([(3)
this.fetchVehicles(),
this.fetchModels(),
this.fetchModels(),
this.fetchMakes(),
this.fetchDrivers()
])
} catch (error) {
console.log(error)
}
},
1 | created 是几个生命周期钩子之一,这些钩子是在组件生命周期中的特定点被调用的方法(其它可用方法包括 beforeUpdate、updated、mounted 等。你可以从 Vue.js 文档 了解可用的生命周期钩子 |
2 | fetchData 方法是其中我们调用其他方法从 API 检索数据的。由于这些 API 调用是独立的,不需要同步运行,因此我们已将 async 关键字添加到此函数中。 |
3 | 在 try /catch 块中,我们使用 Promise API “链”了我们多个 API 调用。由于我们不会从这些方法中返回任何内容,因此不需要使用通常在 async 函数中使用的 await 关键字。 |
fetchVehicles: function () { (1)
fetch(`${this.serverURL}/vehicle`)
.then(r => r.json())
.then(json => { this.vehicles = json })
.catch(error => console.error('Error retrieving vehicles: ' + error))
},
fetchModels: function () {
fetch(`${this.serverURL}/model`)
.then(r => r.json())
.then(json => { this.models = json })
.catch(error => console.error('Error retrieving models: ' + error))
},
fetchMakes: function () {
fetch(`${this.serverURL}/make`)
.then(r => r.json())
.then(json => { this.makes = json })
.catch(error => console.error('Error retrieving makes: ' + error))
},
fetchDrivers: function () {
fetch(`${this.serverURL}/driver`)
.then(r => r.json())
.then(json => { this.drivers = json })
.catch(error => console.error('Error retrieving drivers: ' + error))
},
1 | 以下几个方法将执行前一个代码片段中所引用的相应 API 调用。我们使用 fetch API 对资源端点进行 GET 调用,解析 JSON,并存储数据在适当的 data 变量中。 |
submitNewVehicle: function () {
const vehicle = this.vehicle (1)
fetch(`${this.serverURL}/vehicle`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(vehicle)
}).then(r => r.json())
.then(json => {
this.vehicles.push(json) (2)
this.vehicle = {name: '', make: null, model: null, driver: null} (3)
})
.catch(ex => console.error('Unable to save vehicle', ex))
}
}
}
</script>
1 | 因为我们存储了 vehicle 对象(由 VehicleForm 组件使用)在顶级组件的 data 中,因此对 POST 请求保存车辆实例是微不足道的 - 我们只需从 data 中抓取变量(例如 this.vehicle ),将其转换成 JSON 字符串,并使用 fetch 进行 POST 请求。 |
2 | POST 请求将返回新创建的车辆实例,我们只需将其 push 到我们的 data.vehicles 数组中。 |
3 | 在新车辆添加到列表后,我们通过将 data.vehicle 设置为空对象来重置表单(记住用空/null 值初始化必需字段) |
<!-- Per Component Custom CSS Rules -->
(1)
<style>
#garage {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
}
</style>
1 | 此处包含了一些样式以美化应用程序的布局 - 您可以自由使用您喜欢的任何样式。请注意,这些样式受到限制或限定于组件自己的模板。 |
5.7 路由
如果您现在运行 client
应用程序(或者如果您在遵循该指南时一直运行 client:start
任务,则重新加载),您会注意到默认主页没有改变。这是因为 Vue 路由 - Vue.js 应用程序的官方路由库 - 被配置为在索引路由中显示 Welcome
组件。幸运的是,这是一个简单的更改。
按如下所示编辑文件 client/src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Garage from '@/components/Garage' (1)
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Garage', (1)
component: Garage (1)
}
]
})
1 | 用我们的 Garage 组件替换索引 / 路由的 Welcome 组件的导入和用法。 |
运行应用程序
使用 ./gradlew client:start
运行 client
项目,并浏览至 http://localhost:3000
。您应该会看到我们新的 Vue 应用程序,并能够与 Grails REST API 交互。恭喜,您已使用 Grails 构建了一个 Vue.js 应用程序!
6 后续步骤
有很多机会可以扩展此应用程序的范围。以下是你自己可以做出的一些改进建议
-
改进表单(或创建新的表单组件),以添加制造商、型号和驾驶员
-
添加对编辑现有车辆的支持,或许可以使用模态对话框进行编辑表单
-
目前 Makes & Model 域类是独立的。在 Make 和 Model 之间添加适当的 GORM 关联,并更改选择列表,以便仅为当前选择的 Make 显示 Model。您需要使用 JavaScript
Array.filter
方法。