精通 Grails: 用 Groovy 服务器页面(GSP)改变视图

Groovy服务器页面(GroovyServerPages,GSP)将Web置于GrailsWeb框架之内。在精通Grails系列的第三期中,ScottDavis介绍了如何使用GSP工作。您将了解到可以非常轻松地使用GrailsTagLibs、将GSP的部分片断组合在一起以及为自动生成(搭建)的视图自定义默认模板。

查看本系列更多内容|评论:

ScottDavis,主编,AboutGroovy.com

2008年4月01日

+

内容

本系列的前两篇文章介绍了GrailsWeb框架的基本构建块。我曾反复强调过—Grails基于模型-视图-控制器(Model-View-Controller,MVC)架构模式(请参阅参考资料),Grails利用约定优于配置将框架的各个部分组合在一起。Grails用命名直观的文件和目录代替了更容易出错的在外部配置文件中手工对这些链接进行归类的老方法。例如,在第一篇文章可以看到控制器拥有Controller后缀,存储在grails-app/controller目录。在第二篇文章了解到可以从grails-app/domain目录找到域模型。

在本月的文章中,我将通过讨论Grails视图进一步介绍MVC。视图(正如您所料)存储在grails-app/views目录内。但是视图远不止直观的目录名称这么简单。本文将讨论Groovy服务器页面(GSP)并介绍许多替代的视图选项。在本文中将学习标准的Grails标记库(TagLibs),并了解到创建自定义TagLib有多么容易。还会看到如何将GSP的常用片断提取出来放在自己的片段模板(partialtemplate)内,从而遵循DRY(Don'tRepeateYourself,不要重复自己)(请参阅参考资料)原则。最后,将学习如何为搭建的视图调整默认模板,从而在方便地自动创建视图和跳出Grail应用程序默认外观之间进行平衡。

查看Grails应用程序

Grails使用GSP作为表示层。Groovy服务器页面中的Groovy不仅代表底层技术,还代表可以快速编写一两个scriptlet的语言。从这方面来说,GSP非常类似于Java™服务器页面(JSP)技术,JSP允许在Web页面上混合使用一些Java代码,也和RHTML(RubyonRails的核心视图技术)非常相像,RHTML允许在HTML标记之间插入一些Ruby代码。

当然,Java社区长期以来都不欣赏小脚本。scriptlet会导致最低形式的技术重用—复制与粘贴—以及其他一些在技术方面为人所不齿的恶行(因为你能和因为你应该之间有巨大区别)。GSP中的G对优秀、正直的Java人员来说只应该表示一种实现语言而不是其他。GroovyTagLibs和片段模板提供了在Web页面之间共享代码和行为的一种更成熟的方式。

GSP是Grails以页面为中心的MVC观点的基础。页面是基本衡量单位。列表页面提供了到Show页面的链接。Show页面支持单击到编辑页面,诸如此类。不论是熟练的Struts开发人员还是最近的Rails爱好者,都熟悉这种Web生命周期。

之所以提到这点,是因为近几年出现了大量不以页面为中心的视图技术(请参阅参考资料)。面向组件的Web框架(例如JavaServerFaces(JSF)和Tapestry越来越受到青睐。Ajax革命派生出大量基于JavaScript的解决方案,例如Dojo和Yahoo!UI(YUI)库。富Internet应用程序(RIA)平台,例如AdobeFlash和GoogleWebToolkit(GWT)承诺能够实现方便的Web部署,并提供更加丰富、与桌面类似的用户体验。幸运的是,Grails能够轻松地处理所有这些视图技术。

MVC关注点隔离的整体要点在于:它能够使您轻松地用自己喜欢的任何视图作为Web应用程序的外观。Grails流行的插件基础设施意味着许多GSP替代物不过是grails安装的插件(请参阅参考资料获得可用插件的完整列表的链接,或者在命令行下输入grailslist-plugins)。许多插件都是由社区驱动的,是那些希望将Grail与他们喜欢的表示层技术一起使用的人们的努力结果。

虽然Grails没有内置JSF的自动挂勾(hook),但是仍然可以结合使用这两种技术。Grails应用程序是标准的JavaEE应用程序,因此可以将相应的JAR放在lib目录内,将需要的设置放在WEB-INF/web.xml配置文件内,并像平常一样编写应用程序。Grails应用程序部署在标准的servlet容器内,所以Grails对JSP的支持同对GSP的支持一样好。Grails有针对Echo2和Wicket的插件(两者都是面向组件的Web框架),所以在使用JSF或Tapestry插件方面没有任何障碍。

类似地,向Grails添加Ajax框架(例如Dojo和YUI)的步骤也没有什么特别之处:只要将它们的JavaScript库复制到web-app/js目录即可。Prototype和Scriptaculous是Grails的默认安装。RichUI插件则从各种Ajax库选择UI部件。

如果查看插件列表,那么就会看到对RIA客户机的支持——例如Flex、OpenLazlo、GWT和ZK。显然,Grails应用程序并不缺少备选的视图解决方案。但是在这里我们还是采用Grail直接支持的视图技术—GSP。

回页首

GSP101

可以使用多种方法查找GSP页面。文件扩展名.gsp就是一种很明显的方法,就好像很多以<g:开头的标记一样。事实上,GSP不过是标准HTML加上一些提供动态内容的Grails标记而已。在前一节提到的某些备选的视图技术是一层不透明的抽象层,完全将HTML、CSS和JavaScript的细节隐藏在Java、ActionScript或其他编程语言层之后。GSP是在标准HTML上的薄薄的一层Groovy层,因此在必要时,可以轻松地将它从框架中取出来,并使用原生的Web技术。

但是要在目前的TripPlanner应用程序中查找GSP的话,则会比较费力(这个系列的前两篇文章开始构建TripPlanner程序。如果没有跟上进度,那么现在是赶上来的好时机)。现在视图正在使用动态搭建(scaffold),所以trip-planner/grails-app/views目录还是空的。请在文本编辑器打开如清单1所示的grails-app/controller/TripController.groovy,查看用来启用动态搭建的命令:

清单1.TripController类

classTripController{

defscaffold=Trip

}

defscaffold=Trip行告诉Grails在运行的时候动态地生成GSP。这非常适合自动保持视图与域模型修改同步,但是如果正在学习该框架,那么它没有提供太多可学习的东西。

请在trip-planner根目录下输入grailsgenerate-allTrip。当询问是否覆盖现有控制器时,回答y(也可以回答a表示全部,这将覆盖所有内容,这样就不会反复出现提示)。现在应该看到完整的TripController类,带有名为create、edit、list和show闭包(以及其他闭包)。还应该看到grails-app/views/trip目录下有四个GSP:create.gsp,edit.gsp,list.gsp,andshow.gsp.

在这里起作用的是“约定优于配置”。当访问http://localhost:9090/trip-planner/trip/list时,就是要求TripController填充域模型对象Trip的列表,并将列表传递给trip/list.gsp视图。请在文本编辑器中查看TripController.groovy,如清单2所示:

清单2.完全填充的TripController类

classTripController{

...

deflist={

if(!params.max)params.max=10

[tripList:Trip.list(params)]

}

...

}

这个简单的闭包从数据库中检索到10条Trip记录,将它们转换为POGO,并将它们保存在名为tripList的ArrayList内。list.gsp页面随后将遍历列表,逐行构建HTML表格。

下一节研究许多流行的Grails标记,包括用来在Web页面上显示每条Trip的<g:each>标记。

回页首

Grails标记

<g:each>是常用的Grails标记。它将遍历列表中的每个项。要查看它的效果,请在文本编辑器中打开grails-app/views/trip/list.gsp(如清单3所示):

清单3.list.gsp视图

<g:eachin="${tripList}"status="i"var="trip">

<trclass="${(i%2)==0?'even':'odd'}">

<td><linkaction="show"id="${trip.id}">${trip.id?.encodeAsHTML()}</g:link></td>

<td>${trip.airline?.encodeAsHTML()}</td>

<td>${trip.name?.encodeAsHTML()}</td>

<td>${trip.city?.encodeAsHTML()}</td>

<td>${trip.startDate?.encodeAsHTML()}</td>

<td>${trip.endDate?.encodeAsHTML()}</td>

</tr>

</g:each>

<g:each>标记的status属性是个简单的计数器字段(请注意这个值用在下一行的ternary语句中,用来将CSS样式设为even或odd)。var属性允许命名用来保存当前项的变量。如果将名称改为foo,那么需要将后面的行改为${foo.airline?.encodeAsHTML()},依次类推(?.操作符是Groovy用来避免NullPointerException的方法。它可以快捷地表示“只有在airline不为null时才调用encodeAsHTML()方法,否则返回空字符串”)。

另一个常用Grails标记是<g:link>。顾名思义,它的作用是构建一个HTML<ahref>链接。当然也可以直接使用<ahref>标记,但是这个方便的标记还接受属性,例如action、id和controller。如果只考虑href的值而不考虑它前后的anchor标记,那么可以改用<g:createLink>。在list.gsp顶部能看到返回链接的第三个标记:<g:createLinkTo>。这个标记接受dir和file属性而不是controller、action和id属性。清单4显示了link和createLinkTo的作用:

清单4.link标记vs.createLinkTo标记

<divclass="nav">

<spanclass="menuButton"><aclass="home"href="${createLinkTo(dir:'')}">Home</a></span>

<spanclass="menuButton"><linkclass="create"action="create">NewTrip</g:link></span>

</div>

注意,在清单4中,可以交替使用两种不同的形式调用Grails标记—一种是在尖括号内包含标记,一种是仿效方法调用在大括号内包含标记。当在另一个标记的属性中调用方法时,大括号表示法(正式名称为表达式语言或EL语法)更合适。

在list.gsp下面的几行,能够看到另一个流行的Grails标记:<g:if>,如清单5所示。在这个示例中,它的意思是“如果flash.message属性不为null,就显示它。”

清单5.<g:if>标记

<h1>TripList</h1>

<iftest="${flash.message}">

<divclass="message">${flash.message}</div>

</g:if>

在浏览生成的视图时,还会看到其他许多Grails标记。<g:paginate>标记在数据库包含的Trip比当前显示的10条记录多时,显示“前一个”和“下一个”链接。<g:sortable>标记使列标题变为可单击,从而支持排序。看看其他GSP页面中与HTML表单有关的标记,例如<g:form>和<g:submit>。Grails的联机文档中列出了所有可用的Grails标记,并提供了它们的用法示例(请参阅参考资料)。

回页首

自定义标记库

虽然标准Grails标记很有帮助,但是最终会遇到需要自定义标记的情况。许多资深Java开发人员(包括我自己)公开表示,“自定义TagLib是合适的架构性解决方案。”,然后却偷偷地编写scriptlet,以为别人看不到。编写自定义JSPTagLib需要的工作太多,所以难以抵抗scriptlet的诱惑。scriptlet并不是正确的方法,但不幸的是,它是一种容易实现的方法。

Scriptlet破坏了HTML基于标记的范式,将原始代码直接引入视图。错误的并不是代码本身,而是它们缺少封装和重用的潜力。重用Scriptlet的惟一方式就是“复制-粘贴”。这导致bug、代码膨胀,并严重违背了DRY原则。更不用说Scriptlet在可测试性方面的匮乏了。

这就是说,我必须坦白,随着开发期限越来越紧迫,我写的JSPscriptlet的比例也相当大。JPS标准标记库(JSPStandardTagLibrary,JSTL)在这方面帮助了我很多,但是编写我自己的自定义JSP标记则完全是另一回事。在我用Java代码编写自定义JSP标记、编译标记,并将大量时间浪费在将标记库描述符(TagLibraryDescriptor,TLD)设置为正确的格式和位置时,我已经完全忘记了当初编写这个标记的理由是什么。编写测试来验证我的新JSP标记是否正确也同样麻烦—只能说我的出发点是好的。

对比之下,用Grails编写自定义TagLibs简直就是举手之劳。Grails框架使得做正确的事(包括编写测试)变得很容易。例如,我经常需要在Web页面底部加上标准的版权声明。版权声明应该是这样的:©2002-2008,FakeCoInc.AllRightsReserved.。问题在于,我希望第二个年份总是当前的年份。清单6显示了用scriptlet如何完成这个任务:

清单6.用scriptlet完成的版权声明

<divid="copyright">

&copy;2002-${Calendar.getInstance().get(Calendar.YEAR)},

FakeCoInc.AllRightsReserved.

</div>

既然知道了如何处理当前年份,那么下面就要创建一个执行同样任务的自定义标记。请输入grailscreate-tag-libDate,这会创建两个文件:grails-app/taglib/DateTagLib.groovy(TagLib)和grails-app/test/integration/DateTagLibTests.groovy(测试)。将清单7中的代码添加到DateTagLib.groovy:

清单7.简单的自定义Grails标记

classDateTagLib{

defthisYear={

out<<Calendar.getInstance().get(Calendar.YEAR)

}

}

清单7创建一个<g:thisYear>标记。可以看到,年份直接写入输出流。清单8显示了新标记的效用:

清单8.使用自定义标记的版权声明

<divid="copyright">

&copy;2002-<thisYear/>,FakeCoInc.AllRightsReserved.

</div>

您可能以为这就完成了。非常抱歉,这只完成了一半。

回页首

测试TagLibs

即使现在看起来一切正常,还是应该编写一个测试,确保这个标记日后不会出错。WorkingEffectivelywithLegacyCode的作者MichaelFeathers说过,任何没有测试的代码都是遗留代码。为了防止Feathers先生大发雷霆,请将清单9的代码添加到DateTagLibTests.groovy:

清单9.自定义标记的测试

classDateTagLibTestsextendsGroovyTestCase{

defdateTagLib

voidsetUp(){

dateTagLib=newDateTagLib()

}

voidtestThisYear(){

Stringexpected=Calendar.getInstance().get(Calendar.YEAR)

assertEquals("theyearsdon'tmatch",expected,dateTagLib.thisYear())

}

}

GroovyTestCase是在JUnit3.xTestCase之上一层薄薄的Groovy层。为只有一行代码的标记编写测试看起来似乎有些过分,但是很多时候问题的源头正是这一行代码。编写测试并不难,而且保证安全要比说抱歉更好。请输入grailstest-app运行测试。如果一切正常,应该看到如清单10所示的信息:

清单10.在Grails中通过测试

-------------------------------------------------------

Running2IntegrationTests...

RunningtestDateTagLibTests...

testThisYear...SUCCESS

RunningtestTripTests...

testSomething...SUCCESS

IntegrationTestsCompletedin506ms

-------------------------------------------------------

如果TripTests的样子让您感到惊讶,请不要担心。在输入grailscreate-domain-classTrip时,将会为您生成一个测试。实际上,每个Grailscreate命令都会生成对应的测试。确实,测试在现代软件开发中如此之重要。如果以前没有编写测试的习惯,那么Grails将优雅地将您带到正确的方向上来,您肯定不会后悔。

grailstest-app命令除了运行测试之外,还会创建很好的HTML报告。请在浏览器中打开test/reports/html/index.html,查看标准的JUnit测试报告,如图1所示。

图1.单元测试报告

Unittestreport

编写并测试过简单的自定义标记之后,现在要构建一个略微复杂一些的标记。

回页首

高级自定义标记

更复杂的标记中可以处理属性和标记体。例如,现在的版权标记还需要许多复制/粘贴工作才能满足需求。我想像下面这样将当前的行为放在真正可重用的标记内:<g:copyrightstartYear="2002">FakeCoInc.</g:copyright>。清单11显示了代码:

清单11.处理属性和标记体的Grails标记

classDateTagLib{

defthisYear={

out<<Calendar.getInstance().get(Calendar.YEAR)

}

defcopyright={attrs,body->

out<<"<divid='copyright'>"

out<<"&copy;${attrs['startYear']}-${thisYear()},${body()}"

out<<"AllRightsReserved."

out<<"</div>"

}

}

请注意:attrs是标记属性的HashMap。在这里用它提取startYear属性。我将以闭包形式调用thisYear标记(这与我用大括号时从GSP页面所做的闭包调用相同)。类似地,body也以闭包的形式传递给标记,所以调用它的方式与调用其他标记的方式相同。这样确保了我的自定义标记可以按照任意深度嵌套到GSP中。

您可能注意到,自定义TagLibs使用与标准GrailsTagLibs相同的名称空间g:。如果需要将自己的TagLibs放在自定义名称空间内,请向DateTagLib.groovy中添加staticnamespace='trip'。在GSP内,TagLib现在应该是<trip:copyrightstartYear="2002">FakeCoInc.</trip:copyright>。

回页首

片断模板

自定义标记是重用简短代码的好方法,从而避免成为只能复制/粘贴的scriptlet。但是对于更大块的GSP标记来说,可以使用片断模板。

片断模板在Grails文档中的官方称谓是模板。惟一的问题是模板这个词用得太多了,在Grails中有许多不同的意义。下一节就会看到,将安装改变搭建视图的默认模板。对这些模板的修改也包括本节要讨论的片断模板。为了减少混淆,我从Rails社区借用了一个术语,将要表达的内容称为片断模板,或者就称为片断。

片断模板是一大块能够在多个Web页面之间共享的GSP代码。例如,假设我要在所有页面底部使用一个标准的页脚。为了实现这一目的,我要创建一个名为_footer.gsp的代码片断。前面的下划线是对框架的提示(对开发人员也是个明显的提示),告诉框架这不是个完整的格式良好的GSP。如果我在grails-app/views/trip目录中创建这个文件,那么只有Trip视图才会看到它。我要将它保存在grails-app/views目录内,这样就能供所有页面全局共享。清单12显示了全局共享页脚的片断模板:

清单12.Grails片断模板

<divid="footer">

<copyrightstartYear='2002'>FakeCo,Inc.</g:copyright>

<divid="powered-by">

<imgsrc="${createLinkTo(dir:'images',file:'grails-powered.jpg')}"/>

</div>

</div>

可以看到,片断模板支持用HTML/GSP语法进行表达。对比之下,自定义TagLib是用Groovy编写的。简要来说,TagLibs一般情况下用来封闭小行为更合适,而片断模板更适于重用布局元素。

为了让这个示例能正常工作,还需要将“PoweredbyGrails”按钮下载到grails-app/web-app/images目录(请参阅参考资料)。在下载页面上会看到其他许多附属内容,从高分辨率的logo到16x16大小的favicons(浏览网站时在浏览器地址栏前显示的图标)。

清单13显示了如何在list.gsp页面底部包含新建的页脚:

清单13.呈现片断模板

<html><body>

...

<rendertemplate="/footer"/>

</body></html>

请注意,在呈现模板时,要去掉下划线。如果在trip目录下保存_footer.gsp,那么前面的斜杠也要省略。可以这样认为:grails-app/views目录是视图层次结构的根。

回页首

自定义默认搭建

有了一些良好的、可测试的、可重用的组件之后,可以将它们做为默认搭建的一部分。这部分内容是在将defscaffold=Foo放入控制器之后动态生成的。默认搭建也是输入grailsgenerate-viewsTrip或grailsgenerate-allTrip时生成GSP的来源。

要定制默认搭建,请输入grailsinstall-templates。这样会在项目中加入新的grails-app/src/templates目录。应该看到三个目录,名为artifacts、scaffolding和war。

artifacts目录容纳各种Groovy类的模板:Controller、DomainClass、TagLib,等等。例如,如果想让所有控制器都扩展一个抽象父类,那么可以在这里进行修改。全部新控制器都将基于修改过的模板代码(有些人会加入defscaffold=@artifact.name@,这样动态搭建就会成为所有控制器的默认行为)。

war目录包含所有JavaEE开发人员都熟悉的web.xml文件。如果需要添加自己的参数、过滤器或servlet,请在这里进行操作(JSF爱好者们:注意到了吗?)在输入grailswar时,这里的web.xml文件就会被包含到生成的WAR内。

scaffolding目录包含动态生成的视图的原始内容。请打开list.gsp并将<rendertemplate="/footer"/>添加到文件底部。因为这些模板是所有视图共享的,所以一定要使用全局片断模板。

调整了列表视图之后,现在需要验证修改是否生效。对默认模板的修改是少数需要重新启动服务器的操作之一。Grails重新启动之后,请用浏览器访问http://localhost:9090/trip-planner/airline/list。如果正在使用AirlineController的默认搭建,那么在页面底部就应该出现新的页脚。

回页首

结束语

本期文章总结了精通Grails的另一篇文章。现在您对GSP以及Grails可以使用的其他视图技术应该有了进一步的了解,并更好地理解了在生成的众多页面中使用的默认标记。下次您再编写scriptlet时,肯定会觉得有点“不舒服”,因为通过编写自定义Taglib可以更轻松地完成正确的事情。您看到了如何创建片断模板,还看到了将它们添加到默认搭建视图有多么容易。

下个月的GrailsWeb框架之旅的重点是Ajax。不用重新加载整个页面就能发送“微型”HTTP请求,这一能力是GoogleMaps、Flickr以及其他许多流行Web站点背后的诀窍。下个月您将在Grails中体会到相同的魔力。具体来讲,将创建一个多对多关系,并通过Ajax使用户体验变得自然而有趣。

现在要说再见了,希望您喜欢“精通Grails”系列。

相关推荐