创建你的第一个 Grails 应用程序
学习如何创建你的第一个 Grails 应用程序
作者:Zachary Klein
Grails 版本 3.3.1
1 Grails 培训
Grails 培训 - 由创建和积极维护 Grails 框架的人员开发和提供的!。
2 入门
在本指南中,你将创建你的第一个 Grails 应用程序。你将了解领域类、控制器、服务、GSP,以及单元和集成测试。本指南面向对 Grails 框架新手开发人员或希望对框架进行回顾的开发人员。
2.1 你需要什么
要完成本指南,你需要具备以下条件
-
一些空闲时间
-
一个不错的文本编辑器或 IDE
-
已安装 JDK 1.7 或更高版本,并正确配置了
JAVA_HOME
2.2 如何完成本指南
要开始操作,请执行下列步骤
-
下载并解压源
或者
Grails 指南代码库包含两个文件夹
-
initial初始项目。通常是一个简单的 Grails 应用,有部分附加代码,可帮助你快速上手。 -
complete已完成的示例。它是完成指南提出的步骤并在initial文件夹中应用这些更改的结果。
如需完成指南,转至 initial 文件夹
-
cd到grails-guides/creating-your-first-grails-app/initial
并遵循下一节中的说明。
如果你 cd 到 grails-guides/creating-your-first-grails-app/complete,则可以直接转到已完成的示例 |
3 创建你的 Grails 应用
对于本指南,你可以创建自己的起始项目,或使用本指南的代码库中包含的 initial 项目来开始操作。如果你选择使用 initial 项目,则可以安全地跳过本节并且继续 [runningTheApp] 如果你想在你的计算机上安装 Grails,请按照以下选项操作。
3.1 安装 Grails
安装 Grails
SDKMan
'sdkman' 是一个用于安装和管理 Grails 安装(以及其他 JVM 框架和语言)的流行命令行实用工具。通过在 Unix 终端中运行以下命令来安装 sdkman
$ curl -s "https://get.sdkman.io" | bash
安装完成后,安装最新版本的 Grails(本指南使用 4.0.1)
$ sdk install grails 4.0.1
sdkman 会提示你选择是否将此版本设为默认版本(选择“是”)。
$ grails --version
| Grails Version: 4.0.1
| JVM Version: 1.8.0_77
如果你运行的是 Windows,则可以使用一个遵循相同约定的 sdkman 克隆项目。你可以从 https://github.com/flofreud/posh-gvm 下载 |
从 Grails.org 下载
这不是推荐的安装 Grails 的方法,但如果以上方法失败,这里有手动安装步骤。
从 https://grails.org.cn 下载 Grails 二进制包。在方便的目录中解压包。
$ unzip ~/Downloads/grails-4.0.1.zip
使用以下环境变量编辑你的 .bashrc(大多数 Linux 风格)或 .bash_profile 文件(将这些添加到文件末尾)
将 GRAILS_HOME 环境变量设置为提取 zip 的位置
export GRAILS_HOME=/path/to/grails-4.0.1
在 Windows 上,你可以创建位于 我的计算机/高级/环境变量 下的环境变量
现在将 Grails bin 目录添加到你的 PATH 变量
export GRAILS_HOME=/path/to/grails-4.0.1
export PATH="$PATH:$GRAILS_HOME/bin
同样,在 Windows 上,你需要修改位于 我的计算机/高级/环境变量 下的 Path 环境变量
现在,您应该可以在终端窗口中键入 grails - version 并验证是否已成功安装了 Grails
$ grails --version
| Grails Version: 4.0.1
| JVM Version: 1.8.0_77
3.2 Grails 应用程序生成器
|
您知道无需安装任何其他工具就可以下载一个完整的 Grails 项目吗?访问 start.grails.org 并使用Grails 应用程序生成器生成您的 Grails 项目。您可以选择项目类型(应用程序或插件),选择 Grails 版本并选择一个配置文件,然后点击“生成项目”以下载 ZIP 文件。无需 Grails 安装! 您甚至可以使用 HTTP 工具下载项目,如
|
3.3 创建 App
创建 Grails 应用程序非常简单,一旦安装了 Grails,只需运行 create-app 命令
$ grails create-app myApp
如果您没有指定包,则应用程序名称将用作应用程序的默认包(例如,myapp)。您可以在 grails-app/conf/application.yml 中编辑默认包,您也可以选择为应用程序指定一个默认包
$ grails create-app org.grails.guides.myApp
3.4 应用程序配置文件
您可以选择为您的 Grails 应用程序指定一个配置文件。配置文件适用于许多常见的应用程序类型,包括 rest-api、angular、react 等,您甚至可以创建自己的配置文件。
要查看可用的配置文件列表,请使用 list-profiles 命令。
$ grails list-profiles
| Available Profiles
--------------------
* angular - A profile for creating applications using AngularJS
* rest-api - Profile for REST API applications
* base - The base profile extended by other profiles
* angular2 - A profile for creating Grails applications with Angular 2
* plugin - Profile for plugins designed to work across all profiles
* profile - A profile for creating new Grails profiles
* react - A profile for creating Grails applications with a React frontend
* rest-api-plugin - Profile for REST API plugins
* web - Profile for Web applications
* web-plugin - Profile for Plugins designed for Web applications
* webpack - A profile for creating applications with node-based frontends using webpack
要使用配置文件,请指定其名称,并使用 -profile 标志作为前缀
grails create-app myApp -profile rest-api
您可以选择指定一个包和版本(默认为 org.grails.profiles 和配置文件的当前版本)
grails create-app myApp -profile org.grails.profiles:react:1.0.2
要获取有关配置文件的详细信息,请使用 profile-info 命令。
$ grails profile-info plugin
Profile: plugin
--------------------
Profile for plugins designed to work across all profiles
Provided Commands:
--------------------
| Error Error occurred loading commands: grails.dev.commands.ApplicationContextCommandRegistry (Use --stacktrace to see the full trace)
| Error Error occurred loading commands: grails.dev.commands.ApplicationContextCommandRegistry (Use --stacktrace to see the full trace)
* package-plugin - Packages the plugin into a JAR file
* publish-plugin - Publishes the plugin to the Grails central repository
* help - Prints help information for a specific command
* open - Opens a file in the project
* gradle - Allows running of Gradle tasks
* clean - Cleans a Grails application's compiled sources
* compile - Compiles a Grails application
* create-command - Creates an Application Command
* create-domain-class - Creates a Domain Class
* create-service - Creates a Service
* create-unit-test - Creates a unit test
* install - Installs a Grails application or plugin into the local Maven cache
* assemble - Creates a JAR or WAR archive for production deployment
* bug-report - Creates a zip file that can be attached to issue reports for the current project
* console - Runs the Grails interactive console
* create-script - Creates a Grails script
* dependency-report - Prints out the Grails application's dependencies
* list-plugins - Lists available plugins from the Plugin Repository
* plugin-info - Prints information about the given plugin
* run-app - Runs a Grails application
* run-command - Executes Grails commands
* run-script - Executes Groovy scripts in a Grails context
* shell - Runs the Grails interactive shell
* stats - Prints statistics about the project
* stop-app - Stops the running Grails application
* test-app - Runs the applications tests
Provided Features:
--------------------
* asset-pipeline - Adds Asset Pipeline to a Grails project
* hibernate4 - Adds GORM for Hibernate 4 to the project
* hibernate5 - Adds GORM for Hibernate 5 to the project
* json-views - Adds support for JSON Views to the project
* less-asset-pipeline - Adds LESS Transpiler Asset Pipeline to a Grails project
* markup-views - Adds support for Markup Views to the project
* mongodb - Adds GORM for MongoDB to the project
* neo4j - Adds GORM for Neo4j to the project
* rx-mongodb - Adds RxGORM for MongoDB to the project
* asset-pipeline-plugin - Adds Asset Pipeline to a Grails Plugin for packaging
当您在没有 -profile 的情况下创建应用程序时,使用的默认配置文件为 web 配置文件。 |
4 运行应用程序
现在您已经创建(或下载)了 Grails 项目,是时候运行它并查看 Grails 已经为您提供了什么了。
如果您本地安装了 Grails,则可以使用 run-app 命令在项目中运行应用程序
$ cd myApp/
在未安装 Grails 的情况下运行应用程序
感谢 Grails Wrapper,从 Grails 3.2.3 或更高版本开始,您可以在未安装 Grails 的情况下运行任何 Grails 命令。如果您从 Grails 应用程序生成器 下载它,Grails Wrapper 也包含在内。
$ ./grailsw run-app
在 Grails 下运行应用程序
如果您已在计算机中安装了 Grails,只需键入
$ grails run-app
在 Grails 交互模式下运行应用程序
您还可以使用 Grails 的 交互模式 运行 Grails 运行时,您可以在此模式中发出任何 Grails 命令,而无需等待运行时为每个任务执行旋转操作。
在本指南中,我们将为大多数命令首选 Grails 包装器。
$ ./grailsw
| Enter a command name to run. Use TAB for completion:
grails>run-app //you can shutdown the app with the stop-app command
使用 Gradle 运行应用程序
最后,由于 Grails 是建立在 Spring Boot 和 Gradle 之上的,因此您还可以使用 Spring Boot 命令(如 bootRun)与您的 Grails 应用程序交互。这些命令作为 Gradle 任务提供。与 Grails 本身一样,无需在您的机器上安装 Gradle。使用 Gradle Wrapper (gradlew) 时,它将自动下载
$ ./gradlew bootRun
在运行上述任何命令后,Grails 将使用嵌入的 Tomcat 服务器启动您的应用程序,并使其默认情况下在 https://:8080 处可用。
$ ./grailsw run-app
| Running application...
Grails application running at https://:8080 in environment: development
4.1 更改默认端口
如果您想更改端口号,只需编辑 grails-app/conf/ 下的 application.yml 文件,并添加以下行
server:
port: 8090
您可以在命令调用中直接提供端口号
$ ./grailsw run-app --port=8090
| Running application...
Grails application running at https://:8090 in environment: development
4.2 自动重新加载
目前该应用程序仅显示包含有关该应用程序的一些信息的默认索引页面。默认索引页面位于 grails-app/views/index.gsp 下。
继续并编辑该文件,并在页面上更改一些内容,如下所示(第 54 行)
<div id="content" role="main">
<section class="row colset-2-its">
<h1>Welcome to My First Grails Project</h1> (1)
| 1 | 更改 <h1> 标签的文本。 |
保存所做的更改,然后在浏览器中刷新页面。您应该在主页上看到您的更改。当 Grails 检测到更改时,它将自动重新加载视图、控制器、域类和其他制品的更改,因此您不必在每次更改后重新启动该应用程序。
| 对域类关联、重命名类和其他影响应用程序“布线”的操作进行重大更改可能无法成功重新加载。 |
5 域类
Grails 是一个基于 Spring Boot 项目的 模型视图控制器 (MVC) 框架。通常,MVC 应用程序将应用程序的职责划分为三个类别
-
模型 - 定义和管理数据
-
视图 - 管理数据显示(例如 HTML 页面)
-
控制器 - 定义 Web 应用程序的逻辑,以及管理模型和视图之间的通信。控制器响应请求,从模型中获取数据,并将其传递给视图。
通常对象导向 MVC 框架要求开发者配置类对应上述三类中的哪一类。不过,比起大多数框架,Grails 更进一步,采用“惯例优于配置”的开发方式。这意味着对于 Grails 的许多工件类型(控制器、视图等),你只需在项目中特定目录中创建一个文件,Grails 会自动将其编入应用程序,无需你执行任何其他配置。
| Grails 对象关系映射器 (O-R Mapper) GORM 负责处理域类到数据库表(和其他持久性存储)的映射。GORM 是 Grails 框架中的一个强大工具,甚至可以在 Grails 项目之外独立使用。它支持关系数据库(通过 Hibernate)以及 MongoDb、Neo4j、Redis 和 Cassandra 数据源。更多信息请参阅 GORM 文档。 |
构建 MVC 应用程序时,通常从“M”(模型)开始,又称为“域模型”。在 Grails 中,域模型使用 Groovy 类在此处定义:grails-app/domain。我们创建一个域类。
5.1 创建域类
域类可以通过 Grails 生成(在这种情况下,Grails 会自动生成一个单元测试),或者你可以自行创建文件。
$ ./grailsw create-domain-class Vehicle
| Created grails-app/domain/org/grails/guides/Vehicle.groovy
| Created src/test/groovy/org/grails/guides/VehicleSpec.groovy
这将生成两个 Groovy 文件,一个是我们自己的域类,另一个是单元测试。我们看看域类是什么样的。
package org.grails.guides
class Vehicle {
static constraints = {
}
}
现在我们的域类没有属性,也没有约束。这样没什么意思,但值得注意的是,这是在应用程序中连接持久性域类所需要的全部内容。默认情况下,Hibernate 将用于配置一个数据源(默认情况下是内存 H2 数据库),并为 grails-app/domain 下的所有 Groovy 类创建表和关联。我们给这个域类添加一些属性
package org.grails.guides
class Vehicle {
String name (1)
String make
String model
static constraints = { (2)
name maxSize: 255
make inList: ['Ford', 'Chevrolet', 'Nissan']
model nullable: true
}
}
| 1 | 属性将用于在数据库中创建列(假设使用关系数据库) |
| 2 | 约束用于强制每个字段中的有效数据 - Grails 为常见场景提供了一套丰富的约束,你还可以定义自定义约束 |
5.2 数据库控制台
如果你再次运行该应用程序,你应看到与之前相同的页面。但是,你可以登录到数据库控制台并查看你的新数据库表。
浏览至 https://:8080/dbconsole 并登录。默认用户名为 sa,没有密码。默认 JDBC URL 为:jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
可以在 application.yml 的 environments development dataSource url 中查看 JDBC url |
登录到数据库控制台后,你应该在左侧侧边栏中看到你的新 VEHICLES 表。单击 + 图标展开表 - 你应该会看到一列列,包括我们刚才定义的三个 String 域,name、make 和 model。
5.3 扩展领域模型
对于我们的 Vehicle 类,make 和 model 都是纯字符串并不是很有意义,因为型号实际上应该与制造商相关联。让我们更新领域模型,使其更健壮。
创建两个新的领域类
$ ./grailsw create-domain-class Make
| Created grails-app/domain/org/grails/guides/Make.groovy
| Created src/test/groovy/org/grails/guides/Make.groovy
$ ./grailsw create-domain-class Model
| Created grails-app/domain/org/grails/guides/Model.groovy
| Created src/test/groovy/org/grails/guides/Model.groovy
使用以下内容编辑这两个文件
package org.grails.guides
class Make {
String name
static constraints = {
}
String toString() {
name
}
}
package org.grails.guides
class Model {
String name
static belongsTo = [ make: Make ]
static constraints = {
}
String toString() {
name
}
}
belongsTo 属性是 GORM 用于确定领域类之间的关联的几个属性之一。其他属性包括 hasMany 和 hasOne。有关更多信息,请参阅 GORM 文档。 |
现在,更新 Vehicle.groovy 以使用新的 Make 和 Model 类,而不是 String。
package org.grails.guides
@SuppressWarnings('GrailsDomainReservedSqlKeywordName')
class Vehicle {
Integer year
String name
Model model
Make make
static constraints = {
year min: 1900
name maxSize: 255
}
}
Grails(通过 GORM)现在将在我们的数据库中创建三个表,以供我们的三个领域类使用,并在表之间创建必要的关联。再次运行应用程序并打开数据库控制台以查看新表。
5.4 数据引导
每个 Grails 项目都包含 grails-app/init 目录下的 BootStrap.groovy 文件。该文件可用于在应用程序启动期间执行任何自定义逻辑。该文件的另一个用途是在我们的数据库中预加载一些数据。让我们创建我们的三个领域类的几个实例。
编辑 grails-app/init/org/grails/guides/BootStrap.groovy,如下所示
package org.grails.guides
class BootStrap {
def init = { servletContext ->
def nissan = new Make(name: 'Nissan').save()
def ford = new Make(name: 'Ford').save()
def titan = new Model(name: 'Titan', make: nissan).save()
def leaf = new Model(name: 'Leaf', make: nissan).save()
def windstar = new Model(name: 'Windstar', make: ford).save()
new Vehicle(name: 'Pickup', make: nissan, model: titan, year: 2012).save()
new Vehicle(name: 'Economy', make: nissan, model: leaf, year: 2014).save()
new Vehicle(name: 'Minivan', make: ford, model: windstar, year: 1990).save()
}
def destroy = {
}
}
现在重启应用程序,然后浏览到 DBConsole,你应该能够展开三个表并看到我们新创建的数据。
5.5 数据源
默认情况下,Grails 配置了一个内存 H2 数据库,它在每次重新启动应用程序时都会被删除并重建。在本指南中,这将足以满足我们的目的,但是,你可以通过配置自己的数据源轻松将其更改为本地数据库实例。我们以 MySQL 为例。
5.6 配置 MySQL 数据源
编辑 build.gradle
dependencies {
//...
runtime 'mysql:mysql-connector-java:5.1.40' (1)
| 1 | 添加 MySQL JDBC 驱动程序作为依赖项 |
确保将依赖项添加到 build.gradle 文件的 dependencies 部分,而不是 buildscript/dependencies 部分。前者用于应用程序依赖关系(在编译时、运行时或测试时需要),而后者用于作为 Gradle 构建过程的一部分所需的依赖关系(例如,管理静态资源)。 |
编辑 application.yml
dataSource:
pooled: true
jmxExport: true
driverClassName: com.mysql.jdbc.Driver (1)
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
username: sa
password: testing
environments:
development:
dataSource:
dbCreate: update
url: jdbc:mysql://127.0.0.1:3306/myapp (2)
| 1 | 将driverClassName和dialect更改为 MySQL 设置 |
| 2 | 这假定您有一个名为myapp的数据库的本地 MySQL 实例 |
5.7 Grails 控制台
现在,我们没有任何控制器或视图可用于我们的领域模型。我们很快就会实现这一目标,但现在,让我们启动Grails 控制台,以便我们可以探索 Grails 和 GORM 所提供的功能。
如果应用程序仍在运行,请使用 kbd:[Ctrl+C]或(如果以 交互模式运行 Grails,则使用stop-app命令)关闭应用程序。
启动 Grails 控制台
$ ./grailsw console
Grails 控制台应用程序将启动。此应用程序基于 Groovy 控制台,但具有其他好处:我们的整个 Grails 应用程序在后台正在运行,因此我们可以访问我们的领域类,甚至可以通过控制台将数据持久存储在数据库中。
尝试使用该控制台对新的领域模型进行操作。下面有一个简单脚本帮助您入门,同样地,有关查询、持久性、配置等的更多详细信息,请参阅 GORM 文档。
import org.grails.guides.*
def vehicles = Vehicle.list()
println vehicles.size()
def pickup = Vehicle.findByName("Pickup")
println pickup.name
println pickup.make.name
println pickup.model.name
def nissan = Make.findByName("Nissan")
def nissans = Vehicle.findAllByMake(nissan)
println nissans.size()
6 控制器
本节将重点介绍创建控制器和定义动作的基本知识。
| 虽然不是“MVC”三合会的一部分,但 Grails 也提供对 服务的支持。在任何复杂的 Grails 应用程序中,建议将核心应用程序逻辑保留在服务中。在本指南中,我们稍后将介绍服务。 |
遵循约定优于配置的原则,Grails 将 grails-app/controllers/下的任何 Groovy 类配置为控制器,无需任何其他配置。您可以自己创建 Groovy 类,或使用 create-controller命令生成控制器和关联的测试规范。
$ ./grailsw create-controller org.grails.guides.Home
| Created grails-app/controllers/org/grails/guides/HomeController.groovy
| Created src/test/groovy/org/grails/guides/HomeControllerSpec.groovy
请注意,Grails 会自动添加*Controller后缀。 |
让我们看看我们的新控制器。
package org.grails.guides
class HomeController {
def index() { }
}
Grails 创建了一个控制器,其中包含一个动作。动作是控制器中的公共方法,可响应请求。通常,控制器动作将接收请求、获取一些数据(在存在时,可以选择性地使用请求的参数或正文),并将结果呈现给浏览器(例如,作为网页)。控制器动作还可以重定向请求、转发、调用服务方法和返回 HTTP 响应代码。有关控制器动作的更多信息,请参见 Grails 文档。
我们目前还不需要在此特定动作中使用的逻辑,但我们希望它呈现一个页面。我们将在 [视图] 一节中更详细地了解 GSP 页面,但现在,让我们为要显示的 HomeController.index动作创建一个非常简单的 GSP 页面。
在 grails-app/views/home目录下创建文件index.gsp。
<html>
<html>
<head>
<meta name="layout" content="main"/>
<title>Home Page</title>
</head>
<body>
<div id="content" role="main">
<section class="row colset-2-its">
<h1>Welcome to our Home Page!</h1>
</section>
</div>
</body>
</html>
重新运行此应用,并在浏览器里输入 http:localhost:8080/home。这时你应该可以看到你的新页面了。
按照惯例,Grails 将控制器动作映射到 grails-app/views/[controller name] 目录中具有相同名称的视图。你可以覆盖这个惯例,并指定特定的视图(或者呈现完全不同的内容)。
我们将在下一节中详细介绍视图和 GSP,但现在你应该注意到我们的 index.gsp 文件基本上是一个 HTML 页面,包含几个不寻常的标记。随意按照你自己的意愿修改这个新的主页。
6.1 URL 映射
现在我们有了新的“home”页面,最好可以将其作为应用程序的着陆页面,而不是 Grails 默认的着陆页面。为此,我们需要更改我们的 UrlMappings.groovy 文件。
Grails 使用 UrlMappings.groovy 文件将请求路由到正确的控制器和动作。它们可以像将 URI 重定向到控制器和/或动作的字符串一样简单,也可以包含通配符和约束,并且变得相当复杂。
| 从Grails 文档 中详细了解 URL 映射。 |
我们来看一下默认的 URLMappings.groovy 文件。
package org.grails.guides
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?(.$format)?"{ (1)
constraints {
// apply constraints here
}
}
"/"(view:"/index") (2)
"500"(view:'/error')
"404"(view:'/notFound')
}
}
| 1 | Grails 默认的 URL 映射 - 此规则导致基于名称将请求映射到控制器和动作(以及可选的 ID 和/或格式)。因此 home/index 将映射到 HomeController,index 动作 |
| 2 | 此 URL 映射将根 URI (/) 指向特定视图。 |
让我们更改 / 规则,使其指向我们的新 HomeController。按照如下方式编辑行
package org.grails.guides
class UrlMappings {
static mappings = {
//...
"/"(controller:"home") (1)
//...
}
}
| 1 | 将 view: "/index" 更改为 controller: "home" |
按照惯例,如果存在,对没有动作名称的控制器的请求将进入 index 动作(如果不存在,则会引发错误)。你可以通过在控制器中指定 defaultAction 属性来更改此行为
package org.grails.guides
class HomeController {
static defaultAction = "homePage"
def homePage() { } (1)
}
| 1 | 不要进行此更改,这只是为了演示目的 |
既然你已经将 / 规则更改为指向你的新 HomeController,如果你重新启动应用程序,并在浏览器里输入 https://:8080,你应该会看到你的新主页。
6.2 脚手架
我们需要一些动作,以允许我们创建新的域类实例,并将它们持久存储到数据库。此外,我们需要能够编辑现有实例甚至删除它们。通常,所有这些功能都需要大量的编码,但 Grails 为我们提供了脚手架,让我们的工作变得更加轻松。
| 在 Grails 文档 中了解有关脚手架的更多信息。 |
6.3 动态脚手架
现在我们有了主页,让我们创建一个控制器来管理前面创建的领域模型。为每个领域类创建一个新的控制器,Vehicle、Make,和Model。
$ ./grailsw create-controller Vehicle
| Created grails-app/controllers/org/grails/guides/VehicleController.groovy
| Created src/test/groovy/org/grails/guides/VehicleControllerSpec.groovy
$ ./grailsw create-controller Make
| Created grails-app/controllers/org/grails/guides/MakeController.groovy
| Created src/test/groovy/org/grails/guides/MakeControllerSpec.groovy
$ ./grailsw create-controller Model
| Created grails-app/controllers/org/grails/guides/ModelController.groovy
| Created src/test/groovy/org/grails/guides/ModelControllerSpec.groovy
设置脚手架:编辑我们刚才创建的这三个控制器,用scaffolding 属性替换默认的index 操作,如下面的示例所示。
package org.grails.guides
class VehicleController {
static scaffold = Vehicle
}
package org.grails.guides
class MakeControler {
static scaffold = Make
}
package org.grails.guides
class ModelController {
static scaffold = Model
}
配置了scaffold 属性后,Grails 现在将为各个领域类生成所有必需的 CRUD(创建、读取、更新、删除)操作。它还将使用我们的领域属性和联合会动态生成带有列表、创建、显示和编辑页面的视图。这可以帮助你快速构建应用程序的基础。
重新启动应用程序,并浏览至https://:8080/vehicle - 你应该会看到我们添加到 BootStrap 中的Vehicle 实例列表。试用新的视图,创建、查看、编辑和删除了一些实例。你还可以使用 Model 和 Make 控制器进行同样的操作.
6.4 静态脚手架
动态脚手架功能强大,有时会提供您需要的所有功能(尤其是对于数据访问比呈现更重要的管理站点)。但是,您很可能会觉得需要自定义生成的视图和控制器,可能是为了更改它们的外观或添加自定义逻辑和功能。Grails 预见了这一需求,并提供了一组generate 命令,这些命令可以生成您刚才看到的控制器和/或视图,允许您修改它们以满足您的需求。
生成视图(并继续使用动态脚手架)
$ ./grailsw generate-views Vehicle
生成控制器(并继续使用动态 GSP 视图)
$ ./grailsw generate-controller Vehicle
生成视图和控制器(绕过所有动态生成)
$ ./grailsw generate-all Vehicle
生成的控制器将放在 grails-app/controller 下,生成的视图将放在 grails-app/views/vehicle 下。
要覆盖现有文件,请使用 -force 标志以及 generate-* 命令:./grailsw generate-all com.example.Vehicle -force |
让我们生成 Vehicle 的控制器和视图,并查看所得控制器。
$ ./grailsw generate-all Vehicle -force
在 grails-app/controllers/org/grails/guides/ 打开 VehicleController.groovy 文件。
import static org.springframework.http.HttpStatus.NOT_FOUND
import static org.springframework.http.HttpStatus.OK
import static org.springframework.http.HttpStatus.CREATED
import org.grails.guides.Vehicle
import grails.transaction.Transactional
@SuppressWarnings(['LineLength'])
@Transactional(readOnly = true) (1)
class VehicleController {
static namespace = 'scaffolding'
static allowedMethods = [save: 'POST', update: 'PUT', delete: 'DELETE']
def index(Integer max) {
params.max = Math.min(max ?: 10, 100) (2)
respond Vehicle.list(params), model:[vehicleCount: Vehicle.count()] (3)
}
def show(Vehicle vehicle) {
respond vehicle (3)
}
@SuppressWarnings(['FactoryMethodName', 'GrailsMassAssignment'])
def create() {
respond new Vehicle(params) (3)
}
@Transactional (1)
def save(Vehicle vehicle) {
if (vehicle == null) {
transactionStatus.setRollbackOnly()
notFound()
return
}
if (vehicle.hasErrors()) {
transactionStatus.setRollbackOnly()
respond vehicle.errors, view:'create' (3)
return
}
vehicle.save flush:true
request.withFormat { (4)
form multipartForm {
(5)
flash.message = message(code: 'default.created.message', args: [message(code: 'vehicle.label', default: 'Vehicle'), vehicle.id])
redirect vehicle
}
'*' { respond vehicle, [status: CREATED] } (3)
}
}
def edit(Vehicle vehicle) {
respond vehicle (3)
}
@Transactional (1)
def update(Vehicle vehicle) {
if (vehicle == null) {
transactionStatus.setRollbackOnly()
notFound()
return
}
if (vehicle.hasErrors()) {
transactionStatus.setRollbackOnly()
respond vehicle.errors, view:'edit' (3)
return
}
vehicle.save flush:true
request.withFormat {
form multipartForm {
(5)
flash.message = message(code: 'default.updated.message', args: [message(code: 'vehicle.label', default: 'Vehicle'), vehicle.id])
redirect vehicle (6)
}
'*' { respond vehicle, [status: OK] } (3)
}
}
@Transactional (1)
def delete(Vehicle vehicle) {
if (vehicle == null) {
transactionStatus.setRollbackOnly()
notFound()
return
}
vehicle.delete flush:true
request.withFormat {
form multipartForm {
(5)
flash.message = message(code: 'default.deleted.message', args: [message(code: 'vehicle.label', default: 'Vehicle'), vehicle.id])
redirect action: 'index', method: 'GET' (6)
}
'*' { render status: NO_CONTENT } (7)
}
}
protected void notFound() {
request.withFormat {
form multipartForm {
(5)
flash.message = message(code: 'default.not.found.message', args: [message(code: 'vehicle.label', default: 'Vehicle'), params.id])
redirect action: 'index', method: 'GET' (6)
| 1 | @Transactional 注释配置了控制器的行为或方式的事务。事务用于管理持久性和其它应一起完成的复杂操作(如果任何步骤失败,则可能会回滚)。有关事务的更多信息,请参阅Grails 文档 |
| 2 | params对象对所有控制器可用,且包含请求中任何URL参数映射。您可以通过名称引用任何参数来检索值:params.myCustomParameter将匹配此URL参数:[url]?myCustomParameter=hello。查看Grails文档了解详情。 |
| 3 | respond方法使用内容协商选择正确的类型(例如,请求的Accept消息头可能会指定JSON或XML)来使用返回给请求者的对象。respond还可接受参数映射,例如model(定义在页面上加载数据的方式)。有关如何使用respond方法的详细信息,请参阅Grails文档。 |
| 4 | request在所有控制器中可用,并且是Servlet API的HttpServletRequest类的实例。您可以访问请求头,存储请求范围内的属性,并使用此对象获取有关请求者的信息。更多信息,请参阅Grails有关request的文档。 |
| 5 | flash是一个映射,它将对象存储在会话中以供执行下一次请求时使用,并在执行完毕下一次请求后自动清除它们。这对于传递您希望下一次请求访问的错误消息或其他数据很有用。更多信息,请参阅Grails有关flash的文档。 |
| 6 | redirect方法很简单 - 它允许操作将请求重定向到另一个操作、控制器或URI。您还可以使用重定向传递参数。有关更多信息,请参阅Grails有关redirect的文档。 |
| 7 | render方法是respond的一个不那么精细的版本 - 它不执行内容协商,因此您必须准确指定要呈现的内容。您可以呈现纯文本、视图或模板、HTTP响应代码或具有字符串表示形式的任何对象。参阅Grails文档。 |
有很多代码!生成和修改脚手架控制器是一个很好的学习练习,因此,请随意试验和修改此代码 - 您可以随时还原到本指南的completed项目中的版本。
6.5 呈现响应
让我们修改我们的HomeController,以便在我们的主页面上呈现一些自定义内容。编辑grails-app/controllers/org/grails/guides/HomeController.groovy。
package org.grails.guides
class HomeController {
def index() {
respond([name: session.name ?: 'User', vehicleTotal: Vehicle.count()]) (1)
}
def updateName(String name) {
session.name = name (2)
flash.message = "Name has been updated" (3)
redirect action: 'index' (4)
}
}
| 1 | 我们调用respond方法,向请求者呈现内容的Groovy映射,其中包含会话中的name属性(如果不存在会话值,则默认为“User”)以及GORM的count方法返回的Vehicle实例的当前总数。 |
| 2 | session 是 Servlet API 的`HttpSession` 类的实例,并且在每个控制器中都可用。我们可以在会话中检索和存储属性——在本例中,我们在会话中存储具有属性 name 的 String。有关更多信息,请参阅 Grails 文档。 |
| 3 | 我们正在使用 flash 范围在下一个请求时设置要显示的消息 |
| 4 | 我们在这个动作中没有特定的内容要显示,所以我们对 index 动作发出重新定向(注意,在 Groovy 方法中,只要至少有一个参数,括号都是可选的)。 |
我们更新了 index 动作以向页面呈现一些自定义内容,并且我们创建了新的动作 updateName,该动作采用 String 参数并将其保存到 session 中以便以后检索。但是,我们需要更新我们的视图来 1. 显示新可用的内容,2. 提供一些调用 updateName 和设置 session 属性的方法。
编辑 grails-app/views/home/index.gsp
<html>
<html>
<head>
<meta name="layout" content="main"/>
<title>Home Page</title>
</head>
<body>
<div id="content" role="main">
<section class="row colset-2-its">
<h1>Welcome ${name}!</h1> (1)
<h4>${flash.message}</h4> (2)
<p>There are ${vehicleTotal} vehicles in the database.</p> (1)
<form action="/home/updateName" method="post" style="margin: 0 auto; width:320px"> (3)
<input type="text" name="name" value="" id="name">
<input type="submit" name="Update name" value="Update name" id="Update name">
</form>
</section>
</div>
</body>
</html>
| 1 | 我们可以通过使用 Groovy 字符串表达式 ${name} ${vehicleTotal} 在 GSP 页面中按名称引用我们的 "model" 中的任何值 |
| 2 | 在这里,我们正在访问我们的 flash.message 属性——如果它是 null,那么这里不会呈现任何内容。 |
| 3 | 这是一个普通的 HTML 表单,它会将 name 文本字段提交给我们刚刚创建的 updateName 动作。 |
运行应用程序,你应在 <h1> 标头中看到我们的新消息:"欢迎用户!", 以及数据库中 Vehicle 实例的当前总数。
尝试在表单中输入自己的姓名并提交——你应看到页面重新加载,并且自己的姓名将取代 "用户"。刷新页面几次。因为我们已将 name 存储在会话中,所以只要会话有效,它就会一直存在。
内容协商
请记住我们使用了 respond 方法,而不是更简单的 render 方法,将我们的 "model" 发送到页面。这意味着除了 HTML 页面之外,我们还可以使用其他格式获取我们的 model,例如 JSON 或 XML。
在终端中运行以下命令(在应用程序运行时)
$ curl -i -H "Accept: application/json" "https://:8080/home/index"
HTTP/1.1 200
X-Application-Context: application:development
Set-Cookie: JSESSIONID=008B45AAA1A820CE5C9FDC2741D345F3;path=/;HttpOnly
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 11 Jan 2017 04:06:57 GMT
{"name":"User","vehicleTotal":3}
我们使用 curl 调用了 index 动作,但我们已将 Accept 标头更改为 application/json。现在,我们不再收到 HTML 页面,而是以 JSON 格式收到相同的数据。
由于 Grails 的默认 URL 映射(如下所示),你也可以在浏览器中请求不同的内容类型
"/$controller/$action?/$id?(.$format)?" {
constraints {
// apply constraints here
}
}
注意映射中的 (.$format)? 令牌。这将匹配我们 URL 上的后缀,例如 .json 或 .xml。在浏览器中测试此项。
浏览到 https://:8080/home/index.json。你应看到我们使用 curl 检索到的相同 JSON 正文。
试将.json更改为.xml。你应当看到该模型的 XML 表示形式。内容协商允许你的控制器非常多才多艺,并从相同操作向不同的客户端返回适当的数据。
7 个视图
视图是 MVC 模式的第三个组成部分。视图负责向用户呈现数据(可能是浏览器页面、API 端点或其他某种类型的使用者)。在许多应用程序中,视图是设计为加载在浏览器中的 HTML 页面。然而,根据请求该视图的客户端类型,"视图"完全可能是一个 XML 或 JSON 文档。
Grails 的主要视图技术是 Groovy Server Page。它遵循 JSP 和 ASP 的许多惯例,但自然以 Groovy 语言为基础。GSP 页面本质上是 HTML 文档,但它们支持许多特殊标签(通常前缀为 g:)以允许对你的视图进行编程控制。你甚至可以在你的 GSP 页面中编写任意的 Groovy 代码,但这强烈不鼓励 - 理想情况下,GSP 页面应当仅包含与视图相关的逻辑和内容;任何类型的数据处理或加工应当在呈现视图之前在控制器(或服务中)发生。
你在本指南中已经使用了 GSP 视图,但让我们快速概括一下基本知识。
布局
应用程序中的 GSP 视图通常需要共享一些通用结构,可能还需要共享一些资产,比如 JavaScript 文件。Grails 使用 SiteMesh 模版技术来支持 "布局" 的理念,其本质上是 GSP 模版文件,你的 GSP 页面可以对其进行 "继承"。
惯例上,布局位于 grails-app/views/layouts 中。Grails 在默认项目中包含 main.gsp 模版,这也是 Grails 脚手架所使用的模版,并且还是默认主页。我们也在使用它。若要使用 GSP 布局,只需使用 <meta name="layout"> 标签指定布局的名称即可
<html>
<html>
<head>
<meta name="layout" content="main"/>
</head>
<!-- ... -->
你也可以创建你自己的布局。让我们为我们的主页创建一个新的布局。
$ vim grails-app/views/layouts/public.gsp
编辑你的新布局吧。我们将开始复制现有的 main.gsp,但我们将添加一个自定义徽标图像,并删除我们的页面不需要的一些布局代码。
<!doctype html>
<html lang="en" class="no-js">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>
<g:layoutTitle default="Auto Catalog"/>
</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<asset:stylesheet src="application.css"/>
<g:layoutHead/>
</head>
<body>
<div class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="/#">
<i class="fa grails-icon">
<asset:image src="logo.png"/>
</i> Auto Catalog
</a>
</div>
<div class="navbar-collapse collapse" aria-expanded="false" style="height: 0.8px;">
<ul class="nav navbar-nav navbar-right">
<g:pageProperty name="page.nav" />
</ul>
</div>
</div>
</div>
<g:layoutBody/>
<div class="footer" role="contentinfo"></div>
</body>
</html>
这个布局的重点是 <g:layoutBody> 和 <g:layoutHead> 标签。这些标签将被 SiteMesh 替换为使用该布局的任何 GSP 页面的 <head> 和 <body> 部分。
随意提供你自己的 logo.png 图像,或者使用 completed 项目中的图像(或通过这个 链接下载)。将图像放置在 grails-app/assets/images/ 目录中,然后该布局应当呈现它而不是 Grails 徽标。 |
目前不必担心新布局中的 <asset> 标签,稍后会对此加以介绍。 |
现在编辑 home/index.gsp 视图以使用新的 public 布局。
<html>
<head>
<meta name="layout" content="public"/> (1)
<title>Home Page</title>
| 1 | 将“main”更改为“public” |
刷新页面(或重新启动应用程序),您应看到实际操作中的新布局。如果您愿意,可以进一步修改 public.gsp 布局。
7.1 视图解析
Grails 如何选择要渲染的视图?根据约定,Grails 查找 grails-app/views 目录下的视图。它会尝试通过使用 views 目录下的目录匹配控制器名称来解析视图到控制器操作。例如,HomeController 将解析为 grails-app/views/home。Grails 然后会将操作映射到具有相同名称的 GSP 页面。例如,index 将解析为 index.gsp。
您还可以使用 render 方法从控制器操作渲染特定视图(覆盖 Grails 约定)。
class SomeController {
def someAction() {
render view: 'anotherView'
}
}
这将尝试解析位于 grails-app/views/some/ 下的 anotherView.gsp 页面。如果您想解析不在控制器自己的视图目录下的视图,请使用前导 / 从 grails-app/views 指定绝对路径。
class SomeController {
def someAction() {
render view: '/another/view'
}
}
这将解析 grails-app/views/another/ 下的 view.gsp。
7.2 GSP
GSP 页面可以访问一组丰富的标签。我们已经看到其中的一些标签投入实际操作。您可以从 Grails 文档获取有关可用 GSP 标签(包括如何定义自己的自定义标签)的更详细内容。
让我们使用一些 GSP 标签对我们的 index.gsp 网页进行一些改进。
编辑 grails-app/views/home/index.gsp
<%@ page import="Vehicle" %>
<html>
<head>
<meta name="layout" content="public"/>
<title>Home Page</title>
</head>
<body>
<div id="content" role="main">
<section class="row colset-2-its">
<h1>Welcome ${name}!</h1>
<g:if test="${flash.message}"> (1)
<div class="message" role="status">${flash.message}</div>
</g:if>
<p>There are ${vehicleTotal} vehicles in the database.</p>
<ul>
<g:each in="${Vehicle.list()}" var="vehicle">
<li>
<g:link controller="vehicle" action="show" id="${vehicle.id}">
${vehicle.name} - ${vehicle.year} ${vehicle.make.name} ${vehicle.model.name}
</g:link>
</li>
</g:each>
</ul>
<g:form action="updateName" style="margin: 0 auto; width:320px"> (2)
<g:textField name="name" value="" />
<g:submitButton name="Update name" />
</g:form>
</section>
</div>
</body>
</html>
| 1 | 与其在 flash.message 无论是否存在的情况下都将其渲染出来,我们使用 <g:if> 标签测试是否有消息,然后渲染该消息(使用一些自定义样式)。 |
| 2 | 用其 GSP 等价项替换纯 HTML <form> 标签。 |
让我们仔细看看 <g:if>。
<g:if test="${isThisTrue}}>
Some content
</g:if>
GSP 标签可以选择接受属性,例如本示例中的 test。不同的标签需要不同的类型的属性,但通常您最终会像在此示例中一样传递 Groovy 表达式。${ 和 } 之间的任何 Groovy 代码都会(在服务器上)经过评估,并会在已渲染页面中替换该结果。
您可以在 GSP 页面的任何地方使用 Groovy 表达式,而不仅限于标签中。请参阅我们 index.gsp 页面中的 ${flash.message}。 |
其他标签属性可能接受纯字符串或数字。例如,<g:form action="updateName">。
一个 GSP 标签也可以选择包含一个主体。就 <g:if> 而言,如果 test 表达式评估为 true(遵循 Groovy Truth 约定),才渲染此主体。其他 GSP 标签(如 <g:form>)只是将主体包含在生成的 HTML 输出中。
7.3 GSP 标记的迭代
迭代
对于迭代也有 GSP 标记——非常有用的一个标记是 <g:each>。我们来试用一下
<%@ page import="Vehicle" %> (1)
<html>
<!-- ... -->
<p>There are ${vehicleTotal} vehicles in the database.</p>
<ul>
<g:each in="${Vehicle.list()}" var="vehicle"> (2)
<li>
<! -- ... -->
</li>
</g:each>
</ul>
<!-- ... -->
<g:each> 标记会遍历某个对象集合,该集合由 in 特性提供。var 会为集合中的每个对象设置 name。Grails 会遍历这个集合(在本例中,是 Vehicle.list() 返回的 Vehicles 列表),并为每个项呈现 <g:each> 标记的主体。
| 1 | 这是一个 JSP 样式的表达式,可执行任意 Groovy 代码(不呈现结果)。我们在这里使用它导入我们的 Vehicle 类。强烈不推荐这样做——稍后我们会说明原因。 |
| 2 | 直接从 View 中访问领域模型是不好的实践 |
这种代码是糟糕的做法——我们正从 View 直接访问我们的领域模型(Vehicle),这会紧密地耦合应用程序的两个独立部分,并且通常会导致代码混乱。实现此功能的更好的方法是在 HomeController.index 操作中获取 Vehicle 列表,然后将列表添加到我们的模型对象中(传递给 respond 的对象)。然后,我们可以像访问 name 和 vehicleTotal 那样访问这个列表。继续修改控制器和视图来使用这种更好的方法——如果需要帮助,已完成的项目已经进行了此更改。 |
7.4 GSP 标记链接
我们来了解另一个常见的 GSP 标记:<g:link>
${vehicle.name} - ${vehicle.year} ${vehicle.make.name} ${vehicle.model.name}
</g:link>
</li>
<g:link> 会呈现一个 HTML <a> 标记,但它的优势在于,它允许你在遵循 Grails 公约的情况下指定你的链接目标,例如这个示例(使用 controller、action 和 id 特性)。<g:link> 足够聪明,可以遵循我们的 URL 映射。因此,如果我们更改 vehicle/show 的 URL 映射,<g:link> 标记仍将呈现正确的 URL。<g:link> 还支持更多特性,请参阅 Grails 文档 了解更多信息。
7.5 资产管道
你可能已在我们的 GSP 页面中注意到几个 <asset> 标记。这些标记由资产管道插件提供,它是 Grails 管理静态资产(图像、CSS、JavaScript 文件等)的默认工具。资产管道插件提供了一组自定义 GSP 标记,但与我们一直在研究的标记不同,它使用 asset 前缀(或命名空间)。
下面列出了最常见的 <asset> 标记
<asset:javascript src="myscript.js" /> (1)
<asset:image src="myimage.png" /> (2)
<asset:stylesheet src="mystyles.css" /> (3)
| 1 | 这个标记从 grails-app/assets/javascripts 加载 JavaScript 文件 |
| 2 | 这个标记从 grails-app/assets/images 加载图像 |
| 3 | 这个标记从 grails-app/assets/stylesheets 加载 CSS 文件 |
正如你所看到的,资产流水线遵循约定而非配置方法,遵循 Grails 的先例。但是,资产流水线是一个非常强大的框架,并且包含一个丰富的插件生态系统——你可以发现渲染 LESS 和 SASS 文件、CoffeeScript、Ember、Angular、JSX (React) 以及其他内容的插件。
资产流水线还支持最小化和压缩你的资产以及更多操作。
| 访问 asset-pipeline.com,了解有关使用资产流水线的更多信息,包括 可用插件 的目录。 |
7.6 添加 Javascript 资产
让我们使用资产流水线插件向我们的页面添加 jQuery。Grails 默认包含 jQuery。本指南中使用的 Grails 版本默认包含 jQuery 2.2.0
_grails-app/assets/javascripts/jquery-2.2.0.min.js_
但让我们改为下载最新版本。从 https://code.jqueryjs.cn/jquery-3.1.1.js 下载 jQuery
将 jquery-3.1.1.js 保存到 grails-app/assets/javascripts。
编辑 grails-app/views/home/index.gsp,在 head 块中添加以下代码段。
<asset:javascript src="jquery-3.1.1.js" />
<script type="text/javascript">
$( document ).ready(function() {
console.log( "jQuery 3.1.1 loaded!" );
});
</script>
刷新页面,并打开浏览器的开发人员控制台。你应该会在控制台日志中看到字符串 jQuery 3.1.1 loaded!。
8 服务
Grails 提供了一个“服务层”,它包含封装业务逻辑的类,并且会通过 依赖注入 连接到应用上下文,以便任何控制器都可以注入和使用它们。对于大多数应用逻辑,服务是首选方式,而不是控制器。
如果这似乎令人困惑,可以这样理解:控制器的目的是响应请求并返回相应的内容。服务可在多个控制器(以及在领域类和从其他服务)中重复使用。服务的通用性更强,并且可以帮助保持控制器整洁,防止业务逻辑重复。与控制器操作相比,对服务方法编写单元测试通常也更容易。
| 控制器用于“网络逻辑”,服务用于“业务逻辑” |
同样按照惯例,Grails 将配置 grails-app/services 目录中的任何 Groovy 类作为服务。服务将被“连接”为 Spring bean 在 Grails 应用上下文中,这意味着你可以简单地通过名称从任何其他 Spring bean(包括控制器和领域类)引用它们。
让我们添加一个功能,用于根据制造商、型号和年份生成 Vehicle 的估计值。我们将把此逻辑放到一个服务中,并从我们的应用代码调用它。
使用 create-service Grails 命令创建一个新服务
$ ./grailsw create-service ValueEstimateService
| Created grails-app/services/org/grails/guides/ValueEstimateService.groovy
| Created src/test/groovy/org/grails/guides/ValueEstimateServiceSpec.groovy
编辑 grails-app/services/org/grails/guides/ValueEstimateService.groovy
package org.grails.guides
import grails.transaction.Transactional
@Transactional
class ValueEstimateService {
def serviceMethod() {
}
}
Grails 提供了 serviceMethod 存根作为示例。删除它,并将其替换为以下内容
package org.grails.guides
import grails.transaction.Transactional
@Transactional
class ValueEstimateService {
def getEstimate(Vehicle vehicle) {
log.info 'Estimating vehicle value...'
//TODO: Call third-party valuation API
Math.round (vehicle.name.size() + vehicle.model.name.size() * vehicle.year) * 2
}
}
显然这种估算车辆价值的方法相当做作!在现实情况下,你很可能会调用第三方网络服务来获得估值,或者对自己的数据库运行查询。但是,这一示例的目的是展示在服务中可以放置何种类型的“业务逻辑”,而不是在控制器或视图中计算。
现在,让我们使用我们的新服务。
编辑 grails-app/controllers/org/grails/guides/VehicleController.groovy(我们先前生成的脚手架控制器),并添加下面显示的属性
import static org.springframework.http.HttpStatus.CREATED
import org.grails.guides.Vehicle
import grails.transaction.Transactional
@SuppressWarnings(['LineLength'])
@Transactional(readOnly = true) (1)
| 1 | 只需在我们的控制器中定义一个与我们的服务类同名的属性,Grails 就会为我们注入对该服务的引用。 |
现在(仍在编辑 VehicleController.groovy),修改 show 操作,如下所示
params.max = Math.min(max ?: 10, 100)
respond Vehicle.list(params), model:[vehicleCount: Vehicle.count()]
}
我们已经向 model 添加了一个名为 estimatedValue 的新属性。此属性的值是我们调用服务方法 getEstimate 的结果,我们将 vehicle 属性传递给 getEstimate,我们准备呈现该属性。
现在,在 show 页面中,我们可以访问 estimatedValue 属性并在页面上显示它。编辑 grails-app/views/vehicle/show.gsp,如下所示
<div id="show-vehicle" class="content scaffold-show" role="main">
<h1><g:message code="default.show.label" args="[entityName]" /></h1>
<h1>Estimated Value: <g:formatNumber number="${estimatedValue}" type="currency" currencyCode="USD" /></h1> (1)
| 1 | <g:formatNumber> 是另一个 GSP 标记,可为呈现数字提供许多有用的选项,包括货币和十进制精度。另请参阅 Grails 文档。 |
重新启动应用程序,然后浏览到 Vehicle 的显示页面,例如 https://:8080/vehicle/show/1。您应该在页面上看到“Estimated Value”。
9 测试您的应用程序
测试是 Web 应用程序开发的重要组成部分。Grails 支持三种类型的测试:单元测试、集成测试和功能测试。 单元测试 通常是最简单的一种,它着重于特定的代码片段,而不依赖于应用程序的其他部分。 集成测试 要求 Grails 环境启动并运行,并且用于测试依赖于数据库或网络资源的功能。 功能测试 要求应用程序正在运行,并且旨在几乎像用户一样使用应用程序,通过针对应用程序提出 HTTP 请求来进行练习。这些往往是最复杂、最难编写的测试。
Grails 使用的测试框架是 Spock。Spock 提供了一种基于 Groovy 语言编写测试用例的表达性语法,因此非常适合 Grails。它包括一个 JUnit 运行器,这意味着 IDE 支持实际上是通用的(任何可以运行 JUnit 测试的 IDE 都可以运行 Spock 测试)。
| Spock 是一个功能丰富的框架(即使在 Grails 应用程序之外),如果您还没有掌握它,那么值得您花时间掌握它。查看 广泛的文档,了解 Spock 的介绍。 |
(根据习惯)Grails 测试会存储在 src/test/groovy 目录(单元测试)和 src/integration-test/groovy 目录(集成/功能测试)中。
你可以使用 test-app 命令运行 Grails 测试
$ ./grailsw test-app
如果你只想运行单元测试或集成/功能测试,可以传递一个命令行标志来选择其中之一。
$ ./grailsw test-app -unit
$ ./grailsw test-app -integration
你还可以将测试类作为参数传递来运行一个特定的测试
$ ./grailsw test-app org.grails.guides.MyTestSpec
编写测试是一个非常广泛的主题,值得写一本专门的指南。实际上,最简单(且通常最有用的)测试就是单元测试,因此让我们写一个简单的单元测试来演练我们的 ValueEstimateService。
Grails 会自动为使用 create-service 命令创建的服务创建一个测试规范。打开 src/test/groovy/org/grails/guides/ValueEstimateServiceSpec。
package org.grails.guides
import grails.testing.gorm.DataTest
import grails.testing.services.ServiceUnitTest
import spock.lang.Specification
class ValueEstimateServiceSpec extends Specification implements ServiceUnitTest<ValueEstimateService>, DataTest {
def setup() {
}
def cleanup() {
}
void "test something"() {
expect:"fix me"
true == false
}
}
目前我们的测试规范有一个测试,"test something",它断言 true == false。Grails 会好心地鼓励你做正确的事情,从一个失败测试开始。
尝试运行这个测试,仅检查它是否失败
$ /grailsw test-app org.grails.guides.ValueEstimateServiceSpec
...
> There were failing tests. See the report at: file:///Users/dev/projects/creating-your-first-grails-app/complete/build/reports/tests/test/index.html
BUILD FAILED
Total time: 6.353 secs
| Tests FAILED Test execution failed
现在我们已经确认我们的测试失败了,让我们编辑此测试用例来演练我们的 getEstimate 方法。编辑 src/test/groovy/org/grails/guides/ValueEstimateServiceSpec。
package org.grails.guides
import grails.testing.gorm.DataTest
import grails.testing.services.ServiceUnitTest
import spock.lang.Specification
class ValueEstimateServiceSpec extends Specification implements ServiceUnitTest<ValueEstimateService>, DataTest {
void setupSpec() { (1)
mockDomain Make
mockDomain Model
mockDomain Vehicle
}
void testEstimateRetrieval() {
given: 'a vehicle'
def make = new Make(name: 'Test')
def model = new Model(name: 'Test', make: make)
def vehicle = new Vehicle(year: 2000, make: make, model: model, name: 'Test Vehicle')
when: 'service is called'
def estimate = service.getEstimate(vehicle)
then: 'a non-zero result is returned'
estimate > 0
and: 'estimate is not too large'
estimate < 1000000
}
}
| 1 | 在 Grails 3.3 中使用更新的测试框架模拟多个对象时,我们现在在设置期间执行模拟,不再需要 @Mock 注释。 |
我们在本次测试中让事情保持得相当简单,因为没有非常复杂的逻辑需要测试,而且这样你也可以专注于 Spock 测试用例的基本公式。Spock 提供了一组关键字,你使用它们以极具可读性的形式布设测试。
-
given指示一个“设置”块 - 在这里,你可以设置完成测试所需的任何对象或变量。 -
when和then是 Spock 最常见的“配对”之一(另一个尚未在此处使用的配对是expect/where- 它们定义一个语句和一个预期结果。 -
and仅仅是继续当前的then块,但它允许你为多个断言指定你的预期。请注意,所有这些块接受(可选的)字符串描述,这会让你的测试更加具备可读性。例如,when: "this method is called"、then: "expect this result"。
继续并重新运行此测试 - 如果一切顺利,它会无比顺利地通过。
$ ./grailsw test-app org.grails.guides.ValueEstimateServiceSpec
...
BUILD SUCCESSFUL
| Tests PASSED
10 部署你的应用
开发 Grails 应用程序的最后一步是将完成的项目构建到可部署包中。一般来说,Java web 应用程序会被部署为 WAR 文件,而 Grails 使用 war 命令就能轻松完成这一点
$ ./grailsw war
...
BUILD SUCCESSFUL
| Built application to build/libs using environment: production
|
本指南并未涵盖配置主题(尽管我们对配置文件进行了一些编辑),但在此提出,Grails 支持环境的概念,如“开发”、“测试”和“生产”。各个环境可以有其特定的配置属性和值,以便在开发与生产系统之间进行不同的设置。默认情况下,
|
获取 WAR 文件后,可以在任何 JEE 容器(如 Tomcat)中对其进行部署。
恭喜!你已构建第一个 Grails 应用程序。