这篇文章的重点是显著地更改 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') } } |