|
欢迎阅读第二年度的 精通 Grails。正如我在 2008 年的最后一篇文章中许诺的一样,在新的一年将使用新的应用程序。再见了,Trip Planner!让我们欢迎 blog 发布系统(blog publishing system)!
我已经将这个应用程序命名为 Blogito。在西班牙语中,它表示 “little blog”,或者是对笛卡儿的 Cogito ergo sum(“我思故我在”)表示敬意。可从 blogito.org 下载这个完整的应用程序。在接下来的几篇文章中,您将一步步构建核心的功能。
这篇文章的重点是显著地更改 Grails 应用程序的外观。去年的 Trip Planner 的外观很怪异,恐怕只有开发人员才会喜欢(说句公道话,与外观相比,我对核心功能更感兴趣)。在本文中,通过使用一些 CSS 和局部模板进行调整,将得到一个外观新颖的 Grails 应用程序。在这个过程中,您还可以简单温习一下 Grails 特性,比如 scaffold、自动时间戳、修改默认模板、创建自定义 TagLib,以及调整关键配置文件(比如 Bootstrap.groovy 和 URLMapper.groovy)。
 |
关于本系列
Grails 是一个现代的 Web 开发框架,它将熟悉的 Java 技术(比如 Spring 和 Hibernate)和最新的实践(比如约定优于配置)结合起来。Grails 是用 Groovy 编写的,它使您能够与遗留的 Java 代码无缝集成,同时又增加了脚本语言的灵活性和动态性。学习了 Grails 之后,您将对 Web 开发有新的看法。
|
|
在开始之前,必须安装 Grails 1.1。撰写本文时,它还是 beta 版。
安装 Grails 1.1
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
$ ln -s /opt/grails-1.1-beta1 grails
$ ls -l | grep "grails"
lrwxr-xr-x 1 sdavis admin 17 Dec 5 11:12 grails -> grails-1.1-beta1/
drwxr-xr-x 14 sdavis admin 476 Nov 10 2006 grails-0.3.1
drwxr-xr-x 16 sdavis admin 544 Feb 9 2007 grails-0.4.1
drwxr-xr-x 17 sdavis admin 578 Apr 6 2007 grails-0.4.2
drwxr-xr-x 17 sdavis admin 578 Jun 15 2007 grails-0.5
drwxr-xr-x 19 sdavis admin 646 Jul 30 2007 grails-0.5.6
drwxr-xr-x 18 sdavis admin 612 Sep 18 2007 grails-0.6
drwxr-xr-x 19 sdavis admin 646 Feb 19 2008 grails-1.0
drwxr-xr-x 18 sdavis admin 612 Apr 5 2008 grails-1.0.2
drwxr-xr-x 18 sdavis admin 612 Oct 9 21:46 grails-1.0.3
drwxr-xr-x 18 sdavis admin 612 Nov 24 20:43 grails-1.0.4
drwxr-xr-x 18 sdavis admin 612 Dec 5 11:13 grails-1.1-beta1
|
在 Windows® 系统上,最好是直接更改 %GRAILS_HOME% 变量。在变更之后,不要忘记重新启动现有的命令提示符。
输入 grails -version 以确保使用了最新的版本,并且正确设置了 GRAILS_HOME 变量。现在,输入应该如清单 2 所示:
清单 2. grails -version 的输出结果
$ 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 中的代码:
清单 3. 创建 Entry 类
class Entry {
static constraints = {
title()
summary(maxSize:1000)
dateCreated()
lastUpdated()
}
String title
String summary
Date dateCreated
Date lastUpdated
}
|
每个 Entry 有一个 title 和 summary 字段。将 maxSize 限制范围设置为 1,000 个字符,这会导致动态地构造 HTML 表单,从而为 summary 字段提供文本区域(而不是简单的文本字段)。
 |
下一篇文章:为 blog 条目添加内容
在下一篇文章中,将添加一个能够保存 blog 条目的实际内容的 body 字段。在本文中,我忽略了 body 字段,因为要完整地实现它,必须先理解 Grails 如何处理用户身份验证和文件上传。Blogito 允许终端用户上传各种类型的数据 — HTML、图像,甚至 MP3。
|
|
记住,dateCreated 和 lastUpdated 是 Grails 中比较神奇的字段名。这些时间戳字段非常适合 blog 应用程序 — 它们允许在列表的顶部保留最新的 Entry。
在域类准备就绪之后,下一步就是创建一个控制器。输入 grails create-controller Entry。将清单 4 中的代码添加到 grails-app/controllers/EntryController.groovy:
清单 4. 创建 EntryController
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):
图 2. 不包含时间戳字段的表单
更改排序的顺序
添加新条目时,默认情况下是根据 ID 对表进行排序的。blog 通常以逆时针顺序对条目进行排序 — 最新的排在前面。在以前版本的 Grails 中,要更改默认的排序顺序,则必须在 EntryController.groovy 中手动编辑列表闭包。在现有的代码行下面添加两个排序代码行并不困难(见清单 6)。问题是不能再从幕后动态构建这个代码(可以查找 src/templates/scaffolding/Controller.groovy 或输入 grails generate-controller Entry 查看默认的底层实现)。
清单 6. Grails 1.0.x 中的排序
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 所示:
图 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 每次启动时插入新的记录:
清单 8. 在开发模式中添加伪记录
import grails.util.GrailsUtil
class BootStrap {
def init = { servletContext ->
switch(GrailsUtil.environment){
case "development":
new Entry(
title:"Grails 1.1 beta is out", summary:"Check out the new features").save()
new Entry(
title:"Just Released - Groovy 1.6 beta 2", summary:"It is looking good.").save()
break
case "production":
break
}
}
def destroy = {
}
}
|
再次重启 Grails。这一次,条目表中将出现现有的记录,如图 4 所示:
图 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">。
清单 9. 更改 list.gsp 视图
<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 头部和段落标记中。
 |
CSS 101:<div> 和 <span>
当谈论 CSS 和其他与图像设计(而不是软件工程)关系更紧密的技术时,一些程序员就显得厌烦。但是我承认,当 CSS 变得复杂时,它可以是非常 复杂的。但反之亦然:当 CSS 比较简单时,它可以是非常简单的。
HTML 标记可以分为两大类:块和内联。块标记(比如 <h1>、<p> 和 <div>)通常用于包含大的、杂乱的内容块。浏览器通常在每个块元素的末尾抛出一个隐式的新行。<h1> 和 <p> 有预定义的外观。我们通常将信息块包含在 <div> 标记中,以便为其命名和自定义样式。
内联元素(比如 <a>、<strong> 和 <span>)通常用来包含一个或两个单词,而不是整个段落。内联元素的末尾没有添加隐式的新行。像块元素一样,像 <strong> 和 <em> 这样的内联元素有与之关联的默认格式,而 <span> 则必须通过 CSS 来应用格式。
对不熟悉的人而言,命名 <span> 和 <div> 元素的方式可能会造成混乱。需要在页面的多个元素上重用 class 属性。需要创建 CSS 类,比如 entry 和 entry-date,以让具有相同类的所有元素同时出现。它们在样式表中显示时带有前导点:例如,.entry 和 .entry-date。
您还可能碰到带有 id 属性的元素。id 在 HTML 文档中必须是惟一的。在本文的后面,将创建一个 <div id="header">。这意味着每页只能有一个 header 元素。id 在样式表中显示时带有一个前导 hash,比如在 #header 中。
要快速回顾 CSS 基础知识,请查看 参考资料。
|
|
在浏览器中刷新列表视图(见图 5)。这还不算是进步。但是添加一些新的 CSS 指令之后,它的外观将有很大的改善。
图 5. 没有使用 CSS 的新列表
将清单 10 中的 CSS 添加到 web-app/css/main.css 的底部:
清单 10. list.gsp 视图的 CSS 自定义
/* Blogito customizations */
.entry {
padding-bottom: 2em;
}
.entry-date {
color: #999;
}
|
再次刷新浏览器将看到更加好看的外观(见图 6)。现在还没有充分利用 CSS,但是已经拥有一个好的起点。
图 6. 带有 CSS 的新列表
创建 Date TagLib
 |
CSS 102:em 和 px
在浏览 main.css 时,就会看到许多字体的大小是用像素来表示的。从技术上讲,使用固定的大小(这很常见)是没有错误的,但这会影响使用。对于有视觉缺陷的用户和具有超大监视器(或超低分辨率)的机器而言,固定大小的字体都是不理想的。在 web 管理员的显示器上看起来很好的东西在其他地方显示时往往收不到这么好的效果 — 包括从电影屏幕到 iPhone 手机等众多显示屏上。
CSS 中的像素度量并不都是不好的 — 它们非常适合具有固定大小的元素,比如图像。但是总体而言,相对度量单元(比如 em)更适用于字体大小。1em 等效于由浏览器或周围父元素设置的默认字体大小的 100%。2em 是字体大小的 2 倍,依此类推。
参见 参考资料 获得更多信息。
|
|
现在,需要使 lastUpdated 日期外观更加友好。最好将可重用代码片段放在自定义 TagLib 中。输入 grails create-tag-lib Date。将清单 11 中的代码添加到 grails-app/taglib/DateTagLib.groovy:
清单 11. 针对 DateTagLib 的代码
import java.text.SimpleDateFormat
class DateTagLib {
def longDate = {attrs, body ->
//parse the incoming date
def b = attrs.body ?: body()
def d = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(b)
//if no format attribute is supplied, use this
def pattern = attrs["format"] ?: "EEEE, MMM d, yyyy"
out << new SimpleDateFormat(pattern).format(d)
}
}
|
现在,将 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 剪切粘贴过来)。
清单 13. 针对 _entry.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 是一个链接到主页的超链接。
清单 16. 针对 _header.gsp 局部模板的代码
<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>
|
 |
CSS 103:padding 和 margin
用于给块元素留出一些空间的 CSS 方框模型(box model)乍看起来有些迷惑。简单而言,padding 增加了块内的空间,而 margin 增加了块外的空间。
清单 18 的头部使用 padding 来增加文本和蓝色方框边缘之间的空间。它使用 margin 来增加蓝色方框外部的 header <div> 和 nav <div> 之间的空间。
可以将一个方框的四面设置为一致的 padding: 2em; 或 margin: 2em;。要设置方框的某个边的空间,可以使用 margin-top、margin-right、margin-bottom 或 margin-left 直接引用它。如果想要通过一行代码为某条边(如清单 18 所示)设置不同的 padding,TRBL(Top、Right、Bottom 和 Left 的缩写)将帮助您记住正确的顺序。这样,记忆四条边的顺序就很容易了。
参见 参考资料 获得更多关于 CSS 方框模型的信息。
|
|
最后,再为 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 导航到主页。
图 8. 展示新的头部
在登录之前隐藏导航栏
您还需要处理一个容易弄错的标志,它表示这是一个 Grails 应用程序:导航栏。尽管我们在下一篇文章中才进行身份验证,但是现在可以为未验证的用户关闭导航栏。这可以通过将 <div> 包含在简单的 <g:if> 测试来实现。这个测试查找存储在会话范围中的 user 变量。
像清单 19 那样修改 list.gsp 和 show.gsp:
清单 19. 在登录之前隐藏导航栏
<g:if test="${session.user}">
<div class="nav">
<span class="menuButton">
<a class="home" href="${createLinkTo(dir:'')}">Home</a>
</span>
<span class="menuButton">
<g:link class="create" action="create">New Entry</g:link>
</span>
</div>
</g:if>
|
在 show.gsp 中,在按钮 <div> 的周围添加相同的测试(您最不愿意看到的事情就是用户编辑未经验证或删除 blog 条目,不是吗?)。
最后,对 list.gsp 的外观进行调整。将 paginateButtons <div> 从 body <div> 移出,如清单 20 所示。这使导航栏能够横跨整个屏幕,从而在屏幕的底部添加一个漂亮的可视锚。
清单 20. 将 paginateButtons <div> 从 body <div> 移出,改善外观
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="layout" content="main" />
<title>Blogito</title>
</head>
<body>
<g:if test="${session.user}">
<div class="nav">
<span class="menuButton">
<a class="home" href="${createLinkTo(dir:'')}">Home</a>
</span>
<span class="menuButton">
<g:link class="create" action="create">New Entry</g:link>
</span>
</div>
</g:if>
<div class="body">
<div class="list">
<g:each in="${entryInstanceList}" status="i" var="entryInstance">
<g:render template="entry" bean="${entryInstance}" var="entryInstance" />
</g:each>
</div>
</div>
<div class="paginateButtons">
<g:paginate total="${Entry.count()}" />
</div>
</body>
</html>
|
再添加一些 CSS,如清单 21 所示,确保 paginateButtons <div> 出现在 body <div> 的底部,而不是旁边:
清单 21. 确保 paginateButtons <div> 出现在屏幕底部的 CSS
.paginateButtons{
clear: left;
}
|
最后一次刷新浏览器。您的屏幕应该如图 9 所示:
图 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')
}
}
|
结束语
本文的目标是显示如何改变 Grails 应用程序的外观。仅需几行 CSS 就可以改变颜色、字体和块元素周围的空间。通过局部模板和 TagLibs 可以创建一些可重用的代码片段。最后,您还可以利用 Grails 框架的所有优点,并且获得一个拥有独特外观的应用程序。
下一期文章继续探讨 Blogito 应用程序。您将添加一个 User 域类,从而让多个人添加 blog 条目。此外,您还将研究 Grails 编解码器,并且进一步了解自定义 URL 映射。不要忘记可以通过 http://blogito.org 下载完整的应用程序。到那时,就可以享受精通 Grails 带来的乐趣了。
|