这篇文章的重点是显著地更改 Grails 应用程序的外观。去年的 Trip Planner 的外观很怪异,恐怕只有开发人员才会喜欢(说句公道话,与外观相比,我对核心功能更感兴趣)。在本文中,通过使用一些 CSS 和局部模板进行调整,将得到一个外观新颖的 Grails 应用程序。在这个过程中,您还可以简单温习一下 Grails 特性,比如 scaffold、自动时间戳、修改默认模板、创建自定义 TagLib,以及调整关键配置文件(比如 Bootstrap.groovy 和 URLMapper.groovy)。
|
在开始之前,必须安装 Grails 1.1。撰写本文时,它还是 beta 版。
Grails 在 Java 1.5 或 1.6 上运行表现最佳。通过命令提示符输入 java -version,确保 Java 版本是比较新的。
Java 1.5 或 1.6 就绪之后,安装 Grails 的步骤就很简单了:
- 从 Grails 站点 下载 grails.zip 文件。
- 解压缩 grails.zip。
- 创建一个
GRAILS_HOME环境变量。 - 将 GRAILS_HOME/bin 添加到
PATH。
如果您使用的应用程序是使用上一版本的 Grails 编写的,则可以输入 grails upgrade 将其迁移到最新的版本。但如果需要处理多个版本的 Grails,应该怎么办呢?
如果运行的是 UNIX®-esque OS(UNIX、Linux®,或 OS X)系统,通过将 $GRAILS_HOME 环境变量指向 symlink 就可以轻松处理 Grails 的多个版本。在我的系统上,将 GRAILS_HOME 指向 /opt/grails。这个步骤完成之后,通过快捷的 ln -s 就可以在各个版本之间切换,如清单 1 所示:
清单 1. 为 UNIX、Linux 或 Mac OS X 系统上的 $GRAILS_HOME 创建一个 symlink
在 Windows® 系统上,最好是直接更改 %GRAILS_HOME% 变量。在变更之后,不要忘记重新启动现有的命令提示符。
输入 grails -version 以确保使用了最新的版本,并且正确设置了 GRAILS_HOME 变量。现在,输入应该如清单 2 所示:
$ grails -version Welcome to Grails 1.1-beta2 - http://grails.org/ Licensed under Apache Standard License 2.0 Grails home is set to: /opt/grails |
现在 Grails 1.1 已经安装完成,可以创建新的应用程序了。
输入 grails create-app blogito 以生成初始的目录结构。转到新的 blogito 目录并输入 grails create-domain-class Entry,以创建表示 blog 条目的类。在 grails-app/domain 找到 Entry.groovy,并添加清单 3 中的代码:
每个 Entry 有一个 title 和 summary 字段。将 maxSize 限制范围设置为 1,000 个字符,这会导致动态地构造 HTML 表单,从而为 summary 字段提供文本区域(而不是简单的文本字段)。
|
记住,dateCreated 和 lastUpdated 是 Grails 中比较神奇的字段名。这些时间戳字段非常适合 blog 应用程序 — 它们允许在列表的顶部保留最新的 Entry。
在域类准备就绪之后,下一步就是创建一个控制器。输入 grails create-controller Entry。将清单 4 中的代码添加到 grails-app/controllers/EntryController.groovy:
class EntryController {
def scaffold = Entry
}
|
表面上看起来很简单的 def scaffold = Entry 行指示 Grails 为 Entry 类构造其余的支持。您随后将获得一个条目表,其中 Entry 类中的每个字段都有一个列(以及一个主键 ID 字段和一个乐观锁定的版本字段)。您还获得完整的 Groovy 服务器页面(Groovy Server Pages,GSP),它们提供很普通但至关重要的 Create/Retrieve/Update/Delete (CRUD) 功能。
输入 grails run-app 并通过 Web 浏览器访问 http://localhost:8080/blogito。单击 EntryController,然后单击 New Entry。这样做的好处是所有 Entry 字段都出现在创建表单中(如图 1 所示)。但这也有不好的地方 — 用户不应该处理这些时间戳字段。您需要调整默认的模板来解决这个问题。
图 1. Create Entry 表单中可编辑的时间戳字段

您可以输入 grails generate-views Entry 手动地从 GSP 文件中删除 dateCreated 和 lastUpdated 字段,但这不能从根本上解决问题。您可能希望这些字段永远不出现在创建和编辑表单中。最好是在 def scaffold 中更改模板。
输入 grails install-templates。在 src/templates/scaffolding 中查找 create.gsp 和 edit.gsp。在每个文件中,将 dateCreated 和 lastUpdated 添加到 excludedProps,如清单 5 所示:
清单 5. 从 list.gsp 和 show.gsp 模板中删除时间戳字段
excludedProps = ['version',
'id',
'dateCreated',
'lastUpdated',
Events.ONLOAD_EVENT,
Events.BEFORE_DELETE_EVENT,
Events.BEFORE_INSERT_EVENT,
Events.BEFORE_UPDATE_EVENT]
|
重启 Grails,确保时间戳字段不再出现(参见图 2):

添加新条目时,默认情况下是根据 ID 对表进行排序的。blog 通常以逆时针顺序对条目进行排序 — 最新的排在前面。在以前版本的 Grails 中,要更改默认的排序顺序,则必须在 EntryController.groovy 中手动编辑列表闭包。在现有的代码行下面添加两个排序代码行并不困难(见清单 6)。问题是不能再从幕后动态构建这个代码(可以查找 src/templates/scaffolding/Controller.groovy 或输入 grails generate-controller Entry 查看默认的底层实现)。
def list = {
if(!params.max) params.max = 10
if(!params.sort) params.sort = "lastUpdated"
if(!params.order) params.order = "desc"
[ entryList: Entry.list( params ) ]
}
|
Grails 1.1 将一个很简单但极为有用的特性添加到静态映射块,即 sort。将清单 7 中的映射块添加到 Entry.groovy。通过在域类中处理排序,您可以继续对控制器执行 def scaffold 操作。
清单 7. 将 sort 添加到 static mapping 块
class Entry {
static constraints = {
title()
summary(maxSize:1000)
dateCreated()
lastUpdated()
}
static mapping = {
sort "lastUpdated":"desc"
}
String title
String summary
Date dateCreated
Date lastUpdated
}
|
重启 Grails,确保编辑后的条目移动到列表的顶端,如图 3 所示:

每次重启 Grails 时将丢失现有的条目,您注意到了吗?记住,这是一个特性,而不是 bug。在每次启动 Grails 时将创建条目表,并且在关闭 Grails 时删除它们。打开 grails-app/conf/DataSource.groovy 验证这个特性。很明显,开发模式中的 db-create 值设置为 create-drop。
可以将该值更改为 update,但这也不是很理想。在开发过程的前期,模式是很不稳定的 — 您可以随时添加或删除字段,或修改限制条件等等。在所有东西稳定下来之前,我觉得最好将 db-create 设置为 create-drop。
在开发模式中经常要重新输入样例数据,为了使这个操作没那么繁琐,可以为 grails-app/conf/BootStrap.groovy 添加一些逻辑。清单 8 中的代码在 Grails 每次启动时插入新的记录:
再次重启 Grails。这一次,条目表中将出现现有的记录,如图 4 所示:

列表视图中的默认 HTML 表对入门人员已经足够好,但对 Blogito 而言,这明显不是长期解决办法。blog 页面通常垂直地显示 date、title 和 summary 字段,而不是横向地显示(每次显示一个字段)。
为进行这种更改,输入 grails generate-views Entry。前面动态构造的 GSP 文件现在应该出现在 grails-app/views/entry 中。在文本编辑器中打开 list.gsp。在头部将标题从 Entry List 更改为 Blogito。删除 <h1> 和 <g:if> 块,然后用清单 9 中的代码代替现有的 <div class="list">。
<div class="list">
<g:each in="${entryInstanceList}" status="i" var="entryInstance">
<div class="entry">
<span class="entry-date">${entryInstance.lastUpdated}</span>
<h2><g:link action="show" id="${entryInstance.id}">${entryInstance.title}</g:link></h2>
<p>${entryInstance.summary}</p>
</div>
</g:each>
</div>
|
注意,这些代码是经过大大简化的。可以删除 <fieldValue> 标记 — 它们帮助将域类绑定到 HTML 表单字段,但在这里没有实用价值。每个 Entry 都包含在一个指定的 <div> 中,而 lastUpdated 字段则包含在指定的 <span> 中。这些类属性连接到随后将构建的 CSS 格式中。title 和 summary 字段包含在普通的 HTML 头部和段落标记中。
|
在浏览器中刷新列表视图(见图 5)。这还不算是进步。但是添加一些新的 CSS 指令之后,它的外观将有很大的改善。

将清单 10 中的 CSS 添加到 web-app/css/main.css 的底部:
/* Blogito customizations */
.entry {
padding-bottom: 2em;
}
.entry-date {
color: #999;
}
|
再次刷新浏览器将看到更加好看的外观(见图 6)。现在还没有充分利用 CSS,但是已经拥有一个好的起点。

|
现在,需要使 lastUpdated 日期外观更加友好。最好将可重用代码片段放在自定义 TagLib 中。输入 grails create-tag-lib Date。将清单 11 中的代码添加到 grails-app/taglib/DateTagLib.groovy:
现在,将 lastUpdated 字段包含在 grails-app/views/entry/list.gsp 中刚才创建的 <g:longDate> 标记中,如清单 12 所示:
清单 12. 在 list.gsp 中使用 <g:longDate>
<div class="entry">
<span class="entry-date"><g:longDate>${entryInstance.lastUpdated}</g:longDate></span>
<h2>${entryInstance.title}</h2>
<p>${entryInstance.summary}</p>
</div>
|
重启 Grails 并刷新 Web 浏览器。您将看到日期的新格式,如图 7 所示:
图 7. 使用自定义 <g:longDate> 标记创建的新日期格式

这个布局非常漂亮。我打算在 show.gsp 中重用它。在 grails-app/views/entry 中创建 _entry.gsp,并添加清单 13 中所示的代码(当然,可以从 list.gsp 剪切粘贴过来)。
<div class="entry">
<span class="entry-date"><g:longDate>${entryInstance.lastUpdated}</g:longDate></span>
<h2><g:link action="show" id="${entryInstance.id}">${entryInstance.title}</g:link></h2>
<p>${entryInstance.summary}</p>
</div>
|
为了使用刚才创建的局部模板,需要像清单 14 那样调整 list.gsp:
清单 14. 在 list.gsp 中使用 _entry.gsp 局部模板
<div class="list">
<g:each in="${entryInstanceList}" status="i" var="entryInstance">
<g:render template="entry" bean="${entryInstance}" var="entryInstance" />
</g:each>
</div>
|
现在还可以在 list.gsp 中重用局部模板,如清单 15 所示:
清单 15. 在 show.gsp 中使用 _entry.gsp 局部模板
<div class="body">
<g:render template="entry" bean="${entryInstance}" var="entryInstance" />
<div class="buttons">
<!-- snip -->
</div>
</div>
|
在浏览器中刷新列表视图。它将和前面完全一样。现在单击条目的标题,确保它也适用于这个视图。
各个部分将协调地显示。现在需要用自己的标志来代替 Grails 标志。
我没有看到在 list.gsp 或 show.gsp 的其他地方引用了 Grails 徽标。记住,Grails 使用 SiteMesh 将最终页面的不同部分结合起来。查看 grails-app/views/layouts/main.gsp 就会看到包含 grails_logo.jpg 文件的位置。
在 grails-app/views/layouts 中创建另一个名为 _header.gsp 的局部模板。添加清单 16 中的代码。注意,Blogito 是一个链接到主页的超链接。
<div id="header"> <p><g:link class="header-main" controller="entry">Blogito</g:link></p> <p class="header-sub">A tiny little blog</p> </div> |
现在像清单 17 那样编辑 main.gsp,以包含 _header.gsp 文件:
清单 17. 使用新 _header.gsp 局部模板的 Main.gsp
<body>
<div id="spinner" class="spinner" style="display:none;">
<img src="${createLinkTo(dir:'images',file:'spinner.gif')}" alt="Spinner" />
</div>
<g:render template="/layouts/header"/>
<g:layoutBody />
</body>
|
|
最后,再为 web-app/css/main.css 添加一些代码,如清单 18 所示:
清单 18. _header.gsp 局部模板的 CSS 格式
#header {
background: #67c;
padding: 2em 1em 2em 1em;
margin-bottom: 1em;
}
a.header-main:link, a.header-main:visited {
color: #fff;
font-size: 3em;
font-weight: bold;
}
.header-sub {
color: #fff;
font-size: 1.25em;
font-style: italic;
}
|
刷新浏览器查看发生了什么变化(见图 8)。单击条目的标题,然后在头部单击 Blogito 导航到主页。

您还需要处理一个容易弄错的标志,它表示这是一个 Grails 应用程序:导航栏。尽管我们在下一篇文章中才进行身份验证,但是现在可以为未验证的用户关闭导航栏。这可以通过将 <div> 包含在简单的 <g:if> 测试来实现。这个测试查找存储在会话范围中的 user 变量。
像清单 19 那样修改 list.gsp 和 show.gsp:
在 show.gsp 中,在按钮 <div> 的周围添加相同的测试(您最不愿意看到的事情就是用户编辑未经验证或删除 blog 条目,不是吗?)。
最后,对 list.gsp 的外观进行调整。将 paginateButtons <div> 从 body <div> 移出,如清单 20 所示。这使导航栏能够横跨整个屏幕,从而在屏幕的底部添加一个漂亮的可视锚。
清单 20. 将 paginateButtons <div> 从 body <div> 移出,改善外观
再添加一些 CSS,如清单 21 所示,确保 paginateButtons <div> 出现在 body <div> 的底部,而不是旁边:
清单 21. 确保 paginateButtons <div> 出现在屏幕底部的 CSS
.paginateButtons{
clear: left;
}
|
最后一次刷新浏览器。您的屏幕应该如图 9 所示:

现在,一切准备就绪了,此时应该将 EntryController 设置为默认主页。为此,需要添加一个将 /(URL http://localhost:9090/blogito/ 中的尾部反斜杠)重新定向到 EntryController 的映射。根据清单 22 编辑 grails-app/conf/UrlMappings.groovy:
清单 22. 将 EntryController 设置为默认主页
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?"{
constraints {
// apply constraints here
}
}
"/"(controller:"entry")
"500"(view:'/error')
}
}
|
本文介绍如何通过CSS和局部模板调整Grails应用的外观,包括定制头部、隐藏导航栏及优化列表视图。
501

被折叠的 条评论
为什么被折叠?



