1. Tile 布局
如果站点能够重用相同的布局(使用 HTML 表格来实现)和图像,而不必重复相同的 HTML 代码,这样不是很好吗?
Tile 在为站点创建共同的外观方面特别出色。话虽这样说,许多开发人员并没有认识到 Tiles 在创建用 JSP 实现的可重用组件方面同样也很出色。
如果您发现自己在多个页面上重复相同的 HTML 代码,就可考虑对那些页面使用 tile 布局。类似地,如果在不同页面上的不同地方使用相同的 HTML 或 JSP 标签,这种情形也很适合使用 tile 来创建小型可视组件。
作为 Tiles 框架的一个例子,下面将重构一个简单的股票报价应用程序来利用 tile 布局,如图 3 所示。
这个简单的示例应用程序主要包含一个股票报价页面,它具有一个接受单个参数(即股票代码)的表单(index.jsp
)。 另一个页面显示股票报价的值(quote.jsp
)。
研究一下下面这两个代码清单。您将重构它们以使用各种各样的 tile 布局。
index.jsp
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<html>
<head>
<title>Stock Quote</title>
</head>
<body>
<table width="500" border="0" cellspacing="0" cellpadding="0">
<tr>
<td> </td>
</tr>
<tr bgcolor="#36566E">
<td height="68" width="48%">
<div align="left">
<img src="images/hp_logo_rickhightower.gif"
width="220"
height="74">
</div>
</td>
</tr>
<tr>
<td> </td>
</tr>
</table>
<html:form action="Lookup">
<table width="45%" border="0">
<tr>
<td><bean:message key="app.symbol" />:</td>
<td><html:text property="symbol" /></td>
</tr>
<tr>
<td colspan="2" align="center"><html:submit /></td>
</tr>
</table>
</html:form>
</body>
</html>
quote.jsp
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<html>
<head>
<title>Stock Quote</title>
</head>
<body>
<table width="500" border="0" cellspacing="0" cellpadding="0">
<tr>
<td> </td>
</tr>
<tr bgcolor="#36566E">
<td height="68" width="48%">
<div align="left">
<img src="images/hp_logo_rickhightower.gif" width="220" height="74">
</div>
</td>
</tr>
<tr>
<td> </td>
</tr>
<tr>
<td> </td>
</tr>
<tr>
<td> </td>
</tr>
<tr>
<td>
<bean:message key="app.price" />: <%= request.getAttribute("PRICE") %>
</td>
</tr>
<tr>
<td> </td>
</tr>
</table>
</body>
</html>
欲学习如何使用 Tiles 框架,您首先必须编写一个 tile 布局,然后重构上述两个例子页面,以便它们不必重复如此多的 HTML 代码。
为了创建一个 tile 布局,您需要做以下事情:
- 找出两个页面的相似之处。
- 创建一个新的布局页面。
- 创建两个新的内容页面,它们仅包含 和 之间的不同之处。
EmployeeListing.jsp
DeptListing.jsp
- 将 tile 布局插入页面 ―― 也就是让 和 在它们的页面中插入 tile 布局,并将内容作为参数传递,同时传递其他必要的参数(比如标题)。
EmployeeListing.jsp
DeptListing.jsp
由于找出两个页面之间的相似之处需要 HTML 布局和 Web 站点适用性方面的技能,事实证明这项工作更像一门艺术,而不是像一门科学。由于某些原因,拥有紫色的头发和纹身是有所帮助的。如果您不这样认为,可以问我的朋友 Boo。
本教程重点集中于 Struts,而不是 HTML 布局和 Web 站点适用性方面的必要技能。 因此,您不会了解关于纹身和紫色头发方面的内容。事实上,例子中的 HTML 布局是刻意简单化的,以防分散我们对 Tiles 框架的注意力。
一旦找出了页面之间的相似之处(这是困难的部分),您就能够创建新的布局页面(这是容易的部分)。为了创建一个 tile 布局,您必须做以下事情:
- 使用标签库指令将 tile 标签库导入 JSP,同时导入需要的其他任何标签库。
- 使用字符串参数来显示像页面这样使用 标签的内容。
tiles:getAsString
- 使用 标签将 tile 插入布局的适当区域。
tiles:insert
- 使用
tiles:put
标签向内部 tile 传递任何需要的参数 ―― 这个标签是tiles:insert
标签的子标签。
将 tile 标签库导入 JSP,同时导入需要的其他任何标签库,如下所示(siteLayout.jsp
):
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/struts-tiles.tld" prefix="tiles" %
注意:要使用 tile 标签库,不要忘了包括 web.xml
文件中的标签库:
<taglib>
<taglib-uri>/WEB-INF/struts-tiles.tld</taglib-uri>
<taglib-location>/WEB-INF/struts-tiles.tld
</taglib-location>
</taglib>
接下来使用字符串参数显示诸如页面标题之类的内容。您不仅需要更改页面的内容,而且还需要更改出现在浏览器中的标题。为此,需要传入 tile 布局将要使用的标题:
<html>
<head>
<title>
<tiles:getAsString name="title" ignore="true"/>
</title>
</head>
注意该代码中使用了tiles:getAsString
标签来显示字符串参数。您不仅能够传递字符串参数,而且能够传递要插入这个页面的其他页面。这里假设调用 JSP 页面向这个 tile 布局传递了一个标题;否则,标题将是空白。
注意: ignore
属性:
ignore
属性如果为 true,这意味着在缺失该属性的情况下忽略它。否则,如果 ignore
属性为 false,那么在没有传递该参数的情况下,Tiles 框架将抛出异常,页面将不会显示出来(false 是默认值)。
要插入内容 JSP,可使用 tiles:insert
标签,它插入该框架作为 tile 来引用的任何页面或 Web 资源。这个标签实际上在 tile 布局中定义了一个区域。 记住,tile 布局的目标是将 tile 布置到该布局中。下面是向该布局插入一个 tile 的例子:
<tiles:insert attribute="content"/>
上面这个例子非常简单。如果想要插入一个 tile,并向它传递当前页面范围内的项,那该怎么办呢?例如,使用 Tiles 框架给 header.jsp
传递一个标题参数(在 tile 范围内)是可以做到的。
在插入 tile 的任何时候,您都可以选择性地给它传递参数。传递给 tile 的参数将被放入该 tile 的标题范围(称为“标题属性”)。例如,除了让标题显示在浏览器的标题栏之外,可能还希望该标题出现在页面的页眉区域。
header.jsp
文件将完成这个任务。虽然标题变量在该 tile 布局页面范围之内,但它不在该 tile 布局所插入的 tile 的范围之内。脆弱方法每个 tile 和 tile 布局都获取它自己的环境 ―― 也就是它自己的 tile 范围。因而,您必须像下面这样给页眉 tile 传递该 tile 变量:
<tiles:insert attribute="header" ignore="true">
<tiles:put name="title"
beanName="title" beanScope="tile"/>
</tiles:insert>
tiles:put
标签将这个 tile 布局范围内的 tile 参数放进页眉 tile 的范围。然后页眉 tile 就能够像 tile 布局所做的那样,通过 tiles:getAsString
标签来使用这个参数。参数名称就是页眉的 tile 范围内的属性名称。 bean 参数是当前范围内(siteLayout.jsp
)的 bean 的名称。 beanScope 是您在其中查找这个属性的范围(可能的值是页面、tile、请求、会话和应用程序)。 您可以从任何范围向该 tile 传递 bean。
接下来,您会看到 quote.jsp
和 index.jsp
将要使用的这个新布局页面(siteLayout.jsp
)的完整清单:
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/struts-tiles.tld" prefix="tiles" %>
<html>
<head>
<title>
<tiles:getAsString name="title" ignore="true"/>
</title>
</head>
<body>
<table width="500" border="0" cellspacing="0" cellpadding="0">
<tr bgcolor="#36566E">
<td height="68" width="48%">
<div align="left">
<img src="images/hp_logo_rickhightower.gif"
width="220" height="74">
</div>
</td>
</tr>
<tr>
<td height="68" width="2000">
<tiles:insert attribute="header" ignore="true">
<tiles:put name="title"
beanName="title" beanScope="tile"/>
</tiles:insert>
</td>
</tr>
<tr>
<td>
<div align="center">
<tiles:insert attribute="content"/>
</div>
</td>
</tr>
<tr>
<td>
<tiles:insert attribute="footer" ignore="true"/>
</td>
</tr>
</table>
</body>
</html>
请花点时间研究一下上面的代码。注意 tile 是如何插入不同区域的(页眉、页脚、内容),以及如何利用 HTML 布局来为 tile 定义区域,从而为应用程序定义完整的布局。
现在已经定义好了使用 tiles 的 tile 布局,您需要使用该布局。 index.jsp
和 quote.jsp
都将使用同一个布局。虽然这对两个页面来说似乎是大量的工作,但是对于真实的 Web 应用程序,你可能会对 20 个或更多的页面使用同一个布局。 通过这种方式,您不必在 20 个位置重复 HTML 或包括 JSP 片断。
注意:为什么不就使用 jsp:include
呢?
在适当的位置包括 JSP 片断是重用 HTML 的脆弱方法。设想一下包括相同的 5 个 JSP 片断的 20 个页面 ―― 您必须重复 100 次。
为了使用 tile,您需要执行以下步骤:
- 使用
taglib
指令导入 tile 标签库。 - 使用
tiles:insert
标签来将 tile 布局插入当前页面。 - 使用
tiles:put
来传递字符串参数。 - 使用
tiles:put
来传入参数 tile。
通过使用 tile 布局,您能够在一个位置中将站点布局所需要的整个 HTML 外部化,然后只需将它插入每个页面。观察一下下面的例子,它显示了如何把 tile 布局插入 index.jsp
:
<%@ taglib uri="/WEB-INF/struts-tiles.tld" prefix="tiles" %>
<tiles:insert page="/siteLayout.jsp" flush="true">
<tiles:put name="title" type="string"
value="Get Rick Hightower Stock Quote" />
<tiles:put name="header" value="/header.jsp" />
<tiles:put name="footer" value="/footer.jsp" />
<tiles:put name="content" value="/indexContent.jsp"/>
</tiles:insert>
现在,当您想要在 quote.jsp
中做相同的事情时,只需更改内容和页眉。
您需要使用插入标签来调用 tile 布局(显示函数)。(注意用来将 tile 布局插入当前页面的 tiles:insert
标签):
<tiles:insert page="/siteLayout.jsp" flush="true">
page 属性指定了上一节中定义的 tile 布局。如果 flush
属性被设置为 true,这个 tile(以及到目前为止的页面)将在页面的其余部分之前(或在缓冲区满而迫使执行刷新时)写到浏览器。
要更改 quote.jsp
和 header.jsp
之间的页面 tile,可使用子标签 tiles:put
:
<tiles:put name="title" type="string"
value="Get Stock Quote" />
注意 tiles:put
是如何向 tile 布局传递字符串参数的。 tiles:put
的 name
属性标签指定了参数名称。tiles:put
的 type
属性指定了参数的类型。最后,value
参数用于传递 title
属性的值。这允许您在使用 tiles:insert
标签来调用 tile 布局(显示函数)时,把简单的字符串作为参数来传递。这些参数将成为该 tile 布局属性;也就是被插入该 tile 布局的 tile 范围。
注意您是如何将三个 tile 作为页眉、页脚和内容参数来传递的( header.jsp
、footer.jsp
和 indexContent.jsp
):
<tiles:put name="header" value="/header.jsp" />
<tiles:put name="footer" value="/footer.jsp" />
<tiles:put name="content" value="/indexContent.jsp"/>
header.jsp
页面将被插入该 tile 布局的页眉区域。 footer.jsp
页面将被插入该 tile 布局的页脚区域。indexContent.jsp
页面将被插入该 tile 布局的内容区域。 如果想插入不同的内容和 tile,只需改变内容参数的值。
注意用于 index.jsp
的表单不再驻留在 index.jsp
中。该表单现在驻留在 indexContent.jsp
中,如下面所列出的:
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<html:form action="Lookup">
<table width="45%" border="0">
<tr>
<td><bean:message key="app.symbol" />:</td>
<td><html:text property="symbol" /></td>
</tr>
<tr>
<td colspan="2" align="center"><html:submit /></td>
</tr>
</table>
</html:form>
除了将 tile 指定为 JSP 页面外,您还能够在 tiles:put
标签的正文内将文本作为 tile 来传递。quote.jsp
所做的正好就是这样:
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/struts-tiles.tld" prefix="tiles" %>
<tiles:insert page="/siteLayout.jsp" flush="true">
<tiles:put name="title" type="string" value="Rick Hightower Stock Quote" />
<tiles:put name="header" value="/header.jsp" />
<tiles:put name="footer" value="/footer.jsp" />
<tiles:put name="content" type="string">
<bean:message key="app.price"/>
<%= request.getAttribute("PRICE") %>
</tiles:put>
</tiles:insert
注意 tiles:put
标签的标签体包含 quote.jsp
的内容。其他每项内容都是该布局所定义的,都与上一个例子中使用的 tile 相同。这样的优点是能够减少系统中的 JSP 页面的数目。 关于哪种方法工作得最好,很久以来一直存在争议,我的结论是它取决于 put 标签体中有多少代码。
您看到这里存在的问题了吗?存在这样一条规则:不要重复您自己(Don't repeat yourself,DRY),而您已经违背了这点规则。您知道为什么吗?
2. Tile 定义
遗憾的是,quote.jsp
和 index.jsp
都违背了 DRY 规则,它们都重复定义了页眉和页脚参数。由于它们都使用相同的参数值,因此不必在两个页面上重复相同的参数是很理想的。
设想有这样一个真实的应用程序,其中的 tile 布局包括更多的区域(比如说 8 个)和使用该 tile 布局的更多页面。您会发现每次想使用某个 tile 布局时都要重复每个参数是一件很痛苦的事情。既然大多数页面都将使用相同的页眉和页脚,那么在单个位置而不是在每个页面定义它们将会带来好处。
回顾一下前面的显示函数类比,tile 布局在某些方面类似一个显示函数。 您使用 tiles:insert
来调用 tile 布局,并且使用 tiles:put
来传入参数。参数是能够插入 tile 布局区域的其他 JSP 页面或字符串。
您现在需要定义对应于页眉和页脚区域的默认参数的能力。Tile 框架还允许您使用定义(definition)来给 tile 布局传递默认参数。
在本节中,您将学习如何创建和使用定义。定义(definition)定义了 tile 布局的默认参数。定义(definition)可以在 JSP 代码或 XML 中定义。在结束本节的学习时,您将能够同时使用这两种方法创建定义。
您将发现使用 JSP 页面来创建定义是最容易的方法,因为它需要最少的配置。
要创建一个 JSP 定义,请执行以下步骤:
- 使用
taglib
指令导入 tile 标签库。 - 使用
logic:notPresent
标签来确保该定义仅被定义一次。 - 使用
tiles:definition
标签来定义该定义,同时传递定义了 tile 布局的 JSP 页面以及新创建的定义的范围。 - 使用
tiles:put
标签定义默认参数。
在下面的清单中,siteLayoutDefinition.jsp
定义了这样一个定义,它使用 siteLayout.jsp
作为 tile 布局,并定义了页眉和页脚的默认参数(以及其他参数):
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
<%@ taglib uri="/WEB-INF/struts-tiles.tld" prefix="tiles" %>
<logic:notPresent name="siteLayoutDef" scope="application">
<tiles:definition id="siteLayoutDef" page="/siteLayout.jsp" scope="application">
<tiles:put name="title" type="string" value="Rick Hightower Stock Quote System" />
<tiles:put name="header" value="/header.jsp" />
<tiles:put name="footer" value="/footer.jsp" />
<tiles:put name="content" type="string">
Content goes here
</tiles:put>
</tiles:definition>
</logic:notPresent>
tiles:definition
标签定义了一个ComponentDefinition(org.apache.struts.tiles.ComponentDefinition)
类型的 JavaBean。 ComponentDefinition
具有用于传递给它的所有属性的 getter 和 setter 方法。logic:notPresent
标签通过在定义之前检查它是否已经在范围中,从而确保 ComponentDefinition
对每个应用程序仅创建一次。
注意: 默认设置可能会带来麻烦。
注意您还要为内容和标题定义默认参数。然而,这被认为是很糟糕的做法。为什么这样说呢?如果有人忘了使用这个标题,他们将取得默认值。由于标题应该随每个页面而改变,您不应该为它定义默认值。那样的话,如果有人忘了传递这个标题,tile 布局就会失败。为了使 tile 在这种情况下失败,您需要做以下两件事情:
- 不要在定义中定义默认值。
- 在使用
tiles:insert
标签定义 tile 布局中的区域时,不要将ignore
设置为 true。
Tile 定义的使用类似于直接使用 tile 布局。唯一的区别:您将指定定义而不是指定 tile 布局 JSP 页面,并且您将使用 tiles:put
传入更少的参数。
要使用 tile 定义,请执行以下步骤:
- 使用
taglib
指令导入 tile 标签库。 - 使用 jsp:include 来包含定义该定义的 JSP 页面。
- 使用 tiles:insert标签,不过要指定“定义 bean(definition bean)”名称和范围而不是指定 tile 布局页面。
- 使用 tiles:put属性来仅指定标题和内容(不指定页眉和页脚)。
下面是一个使用 tile 定义的例子(index2.jsp
):
<%@ taglib uri="/WEB-INF/struts-tiles.tld" prefix="tiles" %>
<jsp:include page="siteLayoutDefinition.jsp"/>
<tiles:insert beanName="siteLayoutDef" beanScope="application">
<tiles:put name="title" type="string"
value="Get Rick Hightower Stock Quote 2" />
<tiles:put name="content" value="indexContent2.jsp"/>
</tiles:insert>
注意代码中使用了 beanName
属性 siteLayoutDef
来指定上一节定义的定义。beanName
属性值对应于上一节中的 bean 定义的 id
属性值。请注意例子中使用 tiles:put
来指定两个参数而不是四个参数,这意味着更少的键入工作和维护更少的代码 ―― 这就是 DRY 的乐趣。
您看到这里存在的问题了吗?您必须为定义创建一个 JSP 页面(siteLayoutDefinition.jsp
),为内容创建一个页面(indexContent.jsp
),为 index.jsp
本身创建一个页面,为布局创建一个页面(siteLayout.jsp
),为页眉创建一个页面,以及为页脚创建一个页面。吆! 您总计要创建 6 个而不是 1 个 JSP 页面(而这还只是一个 简单 的例子)。就算您获得了可重用性,但这是以牺牲简单性为代价的。
关于这个例子的另一个不可思议之处在于该定义本身。JSP 页面的本意是为了以文档为中心的方式表达可视内容。然而,该定义没有任何内容本质上是可视的。事实上,它主要不过就是配置。一个布局可能有多组定义,因此您会发现每个定义都有一个 JSP 页面真是一件麻烦事情。将所有配置保留在单个位置很不错,但是如何做到这点呢?
注意:对定义使用 jsp:include
而不是 @page include
。
如果以前使用过 tile,您也许已经看到过使用包含指令(@page include
)而不是使用动态包含操作(jsp:include
)的例子。 我更喜欢 jsp:include
,因为包含指令在编译时执行,而且,除非包含它的页面改变了,否则新的 JSP 定义就不会重新定义。使用 jsp:include
操作吧,它会帮您省去一些开发方面的麻烦。事实证明两者之间的性能差别是可以忽略的(指令要稍微快一点),但是过时的 JSP 定义的痛苦使人们躲避去使用它。
XML 定义解决了非可视化的 JSPpage 暴露的问题。与其为每个 JSPpage 定义一个定义,您可以在单个配置文件中定义所有配置。然而在能够开始使用 XML 定义之前,您需要首先使用对应的 Struts 的 Tiles 插件:
<plug-in className="org.apache.struts.tiles.TilesPlugin" >
<set-property property="definitions-config"
value="/WEB-INF/tiles-defs.xml" />
<set-property property="moduleAware" value="true" />
<set-property property="definitions-parser-validate" value="true" />
</plug-in>
您需要向 struts 配置文件添加上述代码。注意 definition-config
属性指定了将包含基于 XML 的定义的 XML 文件。这段代码还指定这个 tile 引擎是模块感知的(module-aware),并指定它验证 XML 文件:
一旦定义了该插件,创建 XML 定义就变得容易了。 您只需在 tile 定义文件(例如 tiles-def.xml
)中添加另一个条目:
<tiles-definitions>
<definition name="siteLayoutDef" path="/siteLayout.jsp">
<put name="title" value="Rick Hightower Stock Quote System" />
<put name="header" value="/header.jsp" />
<put name="footer" value="/footer.jsp" />
<put name="content" type="string">
Content goes here
</put>
</definition>
根元素是 tiles-definition;
―― 这个模块的所有 tile 定义都将定义在 tiles-definition 元素内。
definition 元素指定一个 tile 定义。上面定义的定义在功能上等价于前面定义的 JSP 版本。注意该定义的属性稍有区别:使用 name
而不是 id
,以及使用 path
而不是 page
。(很气人,不是吗?)如果您知道如何定义一个基于 JSP 的定义,那么定义基于 XML 的定义将证明只是小孩子玩的游戏,因为它们在形式和功能上几乎是完全相同的。
现在已经定义好了 XML 定义,您需要更改 quote.jsp
和 index.jsp
以使用它。事实证明该定义的使用和以前几乎没有区别:唯一的区别是传递给 tiles:insert
标签的属性,如下所示(index3.jsp
):
<%@ taglib uri="/WEB-INF/struts-tiles.tld" prefix="tiles" %>
<tiles:insert definition="siteLayoutDef">
<tiles:put name="title" type="string"
value="Get Rick Hightower Stock Quote 3" />
<tiles:put name="content" value="indexContent3.jsp"/>
</tiles:insert>
注意您现在使用 definition 属性来指定 tile 定义文件(tiles-def.xml
)中创建的定义,而不是使用 beanName
和 beanScope
。还要注意您需要在定义 JSP 中使用 jsp:include
或 logic:notPresent
。
一旦您转变思路,开始使用 XML 定义而不是使用 JSP 定义,那么 tile 的使用将变得更容易一些。您将只需编写更少的代码和维护更少的非可视化 JSP 页面。
(前一篇) 掌握Tiles 框架 (一)---Tiles入门和Tiles 框架和体系结构 (后一篇) 掌握Tiles 框架 (三)—高级 tile 主题和列表