Tiles的使用(三)
在前面的章节我们描述了Tiles使你的应用程序更加和谐统一,容易使用。我们也说了统一是良好设计的特点。因为统一就意味着可复用。复用会使得应用程序稳定,容易维护。
你也许经常希望你现有的应用程序用Tiles上技术。让它看起来更加和谐统一,或改善它的功能和设计,或两者都是。这被称为重构。
如何你熟悉一般的重构技术,迁移一个应用程序到 Tiles中,类似于Extract Method。
建立Tiles框架
首先要做好备份,这一步非常非常重要!
测试默认的配置
把web.xml中debug和detail参数设置为2,再重启应用程序。仔细查看日志条目,看有没有新的错误产生。运行所有的单元测试,通过点击应用程序确认运行是否正常。
Reviewing the pages
接下来的事情是仔细看你的页面,确定总的布局类型和每个布局的区域划分。花时间思考怎么命名。每个组件需要自己的标识符。
1.) 确定布局 纵观整个应用程序,你会发现各种不同的菜单,对话框-列表页,视图页,等等。重点不是页面要包含哪些内容,而是页面的各部分怎么放在一起才合适。要有 header和footer吗,要有menu吗,menu放在边上还是顶部?所有这些,相关位置比实际内容更重要。试着去确定一些共用的布局,再着重处理整个应用程序中的可视化布局(visual layouts),而不是页面内容。
<%-- messages --%> <TR class="message"> <TD colspan="3"> <logic:messagesPresent> <bean:message key="errors.header"/> <html:messages id="error"> <LI><bean:write name="error"/></LI> </html:messages> <bean:message key="errors.footer"/> </logic:messagesPresent> </TD> </TR> |
一个包含自身代码的单独的块是最好的选择。效果就像是以一个Java函数。
如果显示代码像这样没有注释,在提取(extract) tile前添加一些注释很有用;如果页面的片断看起来像一个tile,但在每个页面输出不同东西的,别担心!Tiles也可以传输字符串常量到一个tile 中,使得其余的标记能被复用。这时只需设置一个能够被替换的标签,像${subtitle},它就可以被传入的字符串替换。
在开始时确定一个很好的命名方案很有用。我们对某个组件的命名应该能够一幕了然。下面先解释一下tiles的术语:
Layout(布局) -- 一个用来描述页面中各个tiles位置的JSP。
Tiles -- 把文本片断和从一个现有的页面抽取(extract)出来的标记组合在一起。
Markup -- 是放置在文件中的命令集,提供一种比可视化的文本更好的格式指令。 HTML就是一种应用。
chrome-- 是window应用程序内容区外面的那部分,像工具栏,菜单栏,标题栏。 HTML chrome用标签创建,位于window应用程序内部,但功能相同。
tiles能够使用静态的内容,标记或两者的混合体。tiles表示导航控制,公用资源,并可以在两个页面间复用。公用资源可以包含一张决定logo位置或能被用于多张表单的按钮(像Save和Cancel)。其它的tiles包含一个单个页面的内容。这些tiles 可以在页面的中间,常常被另外一些有菜单栏和HTML chrome功能的tiles包围。
你可以把tile分类放到不同文件夹中,然后用"."号来引用,当然你也可以用"@",只要合乎逻辑。
/pages ./article/Form.jsp ./channel/Channels.jsp /tiles ./header.jsp./message.jsp ------------------------------------------------------------------------ ------------------------------------------------------------------ <definition name=".pages.Form"> . . . </definition> |
使用 <tiles:insert>重构页面
重构刚开始要慢慢进行,尽量做很小的改动,直到第一个步骤完成。随着工作的继续,从以前的工作中吸取教训,你的步伐会越来越大。
大多数情况,使用那些在Tiles配置文件中声明和能从ActionForward中调用的Definition的目的是减少页面。方法是把创建的扩展列表页面保存好,插入到tiles中。但刚开始,最简单的是用<tiles:insert>标记创建一个页面。整个过程如下:
1.)选择一个好的初始页面
最好是选择一个简单的页面,提取共同的组件,然后把它们一个一个插入到原始页面。你应用程序的欢迎或登陆页面是一个好的选择。这些页面往往混合了能被复用和自定义的内容,相对比较简单。一个内部(interior)页面也是个好选择,如果它不包含太多的chrome。看下面一个内部页面:
Our starter page: /pages/View.jsp --------------------------------- ------------------------------------------------------------------------------------------- --------------- <%@ taglib uri="/tags/struts-html" prefix="html" %> <%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-logic" prefix="logic" %> <%@ taglib uri="/tags/request" prefix="req" %> <!-- HEADER --> <HTML> <HEAD> <html:base/> <LINK rel="stylesheet" type="text/css" href="<html:rewrite forward='baseStyle'/>"> <TITLE>Artimus - Article</TITLE> </HEAD> <BODY> <TABLE class="outer"> <TR> <TD> <TABLE class="inner"> <!-- MESSAGE --> <TR> <TD class="message" colspan="3" width="100%"><html:errors/></TD> </TR> <TR> <TD class="heading" colspan="3"> <H2><bean:write name="articleForm" property="title"/></H2></TD> </TR> <TR> <TD class="author" colspan="3">by <bean:write name="articleForm" property="creator"/> </TD> </TR> <TR> <TD class="article" colspan="3"> <bean:write name="articleForm" property="content" filter="false"/></TD> </TR> <%-- CONTRIBUTOR PANEL --% > <req:isUserInRole role="contributor"> <TR> <TD colspan="3"><HR /></TD> </TR> <TR> <%-- DELETE --% > <logic:equal name="articleForm" property="marked" value="0"> <html:form action="/admin/article/Delete"> <TD class="input"><html:submit >DELETE</html:submit></TD> <html:hidden property="article"/> </html:form> </logic:equal> <%-- RESTORE -- %> <logic:equal name="articleForm" property="marked" value="1"> <html:form action="/admin/article/Restore"> <TD class="input"> <html:submit>RESTORE</html:submit> </TD> <ht ml:hidden property="article"/> </html:form> </logic:equal> <html:form action="/admin/article/Edit"> <TD class="button" colspan="2"> <html:hidden property="article"/> <html:submit>EDIT</html:submit> <html:cancel> ;CANCEL</html:cancel> </TD> </html:form> </TR> </req: isUserInRole> <!-- NAVBAR -- > </TABLE> </TD> </TR> <TR> <TD class="navbar"> <html:link forward="done">DONE</html:link> </TD> </TR> </TABLE> </BODY> </HTML> |
An extracted tile: /tiles/header.jsp ----------------------------------------------- ------------------------------------------------------------------------------------------- - <%@ taglib uri="/tags/struts-html" prefix="html" % > <HTML> <HEAD> <html:base/> <LINK rel="stylesheet" type="text/css" href="<html:rewrite forward='baseStyle'/>"> <TITLE>Artimus - View Article</TITLE> </HEAD> <BODY οnlοad="document.forms[0].elements [0].focus();"> <!-- OUTER TABLE --> <TABLE class="outer"> <TR> <TD align="center"> <!-- INNER TABLE -- > <TABLE class="inner"> <TR> <TD class="navbar" colspan="3">View Article</TD> </TR> |
Inserting an extracted tile: /pages/article/View.jsp ------------------------- ------------------------------------------------------------------------------------------- ----------------------- <%@ taglib uri="/tags/struts-html" prefix="html" % > <%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-logic" prefix="logic" %> <%@ taglib uri="/tags/tiles" prefix="tiles" %> <%@ taglib uri="/tags/request" prefix="req" %> <!-- HEAD --> <tiles:insert page="/tiles/header.jsp"/> <!-- MESSAGE -- > <TR> <TD class="message" colspan="3" width="100%"><html:errors/></TD> </TR> <!-- ... -- > </HTML> |
很快你第一个抽取的tile被重新插入到页面中,在你转移下一个tile前,先测试页面,确认页面能被打开。
处理完毕后,页面会由一些插入的tiles组成,就像下面所示:
A refactored page: /pages/View.jsp (completed) ------------------------------------- ------------------------------------------------------------------------------------------- ----------- <%@ taglib uri="/tags/tiles" prefix="tiles" %> <tiles:insert page="/tiles/header.jsp"/> <tiles:insert page="/tiles/message.jsp"/> <tiles:insert page="/tiles/view.jsp"/> <tiles:insert page="/tiles/navbar.jsp"/> |
如果文件中有些tiles需要被自定义,你能够使用<tiles:put>标记发送一个自定义的值给tile:
Inserting dynamic content: /pages/View.jsp (revised) |
下面是使用 <tiles:getAsString>标记输出动态文本:
Writing dynamic content with getAsString ------------------------------------------- ------------------------------------------------------------------------------------------- ---- <%@ taglib uri="/tags/struts-html" prefix="html" %> <%@ taglib uri="/tags/tiles" prefix="tiles" % > <HTML> <HEAD> <html:base/> <LINK rel="stylesheet" type="text/css" href="<html:rewrite forward='baseStyle'/>"> <TITLE><tiles:getAsString name="title"/></TITLE> </HEAD> <BODY οnlοad="document.forms [0].elements[0].focus();"> <!-- OUTER TABLE --> <TABLE class="outer"> <TR> <TD align="center"> <!-- INNER TABLE -- > <TABLE class="inner"> <TR> <TD class="navbar" colspan="3"><tiles:getAsString name="subtitle"/></TD> </TR> |
2.)抽取 tiles
重构的主要工作是决定页面的哪个部分是tile,移动那个片断到自己的文件中再用它来替代那个片断。
这个给出了每个详细的步骤:
- 选择剪切块
- 打开一个新文件
- 把这个块粘贴进来
- 作为一个JSP(或HTML)保存
- 插入引入tile声明的标签库
- 关闭新文件
- 放置<tile:insert page="/path/to/new/page"/>标签的地方片断就会引用
注意:在重构前设置了Tiles Definitions的页面已经能够显示。页面被编排的不同,但内容和外观还是和以前一样的。Definition常常包含body或内容tile。一些页面可以没有自己的tile,但它们可以使用共享的tile 。在不同的情况下,你可以在一个表单中使用不同的按钮。一个页面可以包含创建记录的按钮,另一个页面可以包含更新记录的按钮。这些不同的页面可以使用含有title string和动态内容的相同Definition来创建。
下面是抽取过程中的注意点:
- 所有被tile使用的自定义标记都必须放到tile中
- 所有自定义标记元素的开始和结束都必须在同一个tile中
- 避免将HTML元素传递到另一个tile中
- 使用注释
- 考虑平衡
- 有效的技术
1. tile能够继承HTML的资源,像CSS;但它不会继承涉及JSP的资源(标签库等)。当页面执行的时候web浏览器能解析CSS,但每个tle实际是一个单独的JSP或servlet,它需要自己调用JSP资源。
2. 如果你使用<html:html>标记,将该元素的标签就必须放置在一个tile布局的开头和结尾。这一限制不适用于HTML元素。你能够在一个header tile中打开<body>元素,在footer tile中关闭</body>。但自定义标签要生效的话,tile必须完整。JSP标签元素的开始和结束必须在同一个tile中。
3. 在实际操作中,你可以决定第一个tile打开一个元素,像一个表格;第二个tile提供内容,像表格中的行;在第三个tile中关闭这个元素,这种情况是可以使用的。但你最好把许开始和结束元素放到同一个tile中,这样更加容易发现标签错误。即使只在中间的tile中提供表格的行,它也可以使用一个完全的行标签 <TR>...</TR>。
4. 多使用注释,让你的代码更清晰,更加容易维护,就像下面所示的标准的JavaDoc:
<%-- /** * Global page header, without automatic form select. * See headerForm.jsp for version with form select. * Imports global style sheet. * Opens HEAD, BODY, and TABLE. Another tile must close these. * @author Ted Husted * @license ASF 1.0 */ --%> |
5. 分离页面到tiles中的工作就像是把信息存到数据库中。你可以完全的去除多余的信息,但如果你这样做,某些部分会变得更加小,甚至引发维护和性能问题的矛盾。你可能有两3个不同的内容的header或footer文件。但如果标签要改变,显然3个文件比30或300个文件更容易修改。因此对于很多编程任务,这种折衷代价还是适用的。
6. 如果你要同时修改引入的CSS和其它标签,别犹豫,创建一些能够自动完成查找替换的文件,这会让你节约很多的时间。
在你完成上一节(抽取练习)的步骤到你的初始页面确认还能正常工作后,你能够开始本节的内容了。这是四个步骤:
- 移动页面到布局文件夹中
- 重命名body tile
- 转换插入标记为一个布局和Definition
- 更新ActionForward
1. 移动重构的页面到本地布局文件夹,例如:/tiles/layouts。被重构的页面应该变成一系列的<tile:insert>标记,而页面的原始内容放到其它tile中而减少了。
2. 一个抽取出来的tiles有可能是原来页面的一个body,如果是这样,就要考虑重命名这个移动的页面,这就是说原来页面的核心内容仍然在原来的地方。如果你需要修改页面内容,只要移动或重命名文件夹后再更新一下<tile:insert>标记,这样做将使你的改动带来的影响最小化。
<tiles:insert page="/tiles/view.jsp"/> to <tiles:insert page="/pages/view.jsp"/> |
<definition> <tiles:insert put="title" value ="Artimus - View Article"/> <tiles:insert put="subtitle" value ="View Article"/> <tiles:insert name="header" page="/tiles/header.jsp"/> <tiles:insert name="message" page="/tiles/message.jsp"/> <tiles:insert name="content" page="/pages/view.jsp"/> <tiles:insert name="navbar" page="/tiles/navbar.jsp"/> </definition> |
<!DOCTYPE tiles-definitions PUBLIC "-//Apache Software Foundation//DTD Tiles Configuration//EN" "http://jakarta.apache.org/struts/dtds/tiles-config.dtd"> <tiles-definitions> <definition name=".article.view" path="/pages/tiles/layouts/Base.jsp> <tiles:put name="title" value ="Artimus - View Article"/> <tiles:put name="subtitle" value="View Article"/> <tiles:put name="header" value="/tiles/header.jsp"/> <tiles:put name="message" value="/tiles/message.jsp"/> <tiles:put name="content" value="/pages/view.jsp"/> <tiles:put name="navbar" value="/tiles/navbar.jsp"/> </definition> </tiles-definitions> |
现在到布局页面中,把<tiles:insert>标记改为<tiles:get>,然后删除page属性(因为现在它是Definition 的一部分),任何的<tiles:put>标记都能变为<tiles:useAttribute>。保留name属性,但要删除value属性(因为value 也是Definition 的一部分了),操作见下:
A Tiles layout page: /tiles/layouts/Base.jsp --------------------------------------- ------------------------------------------------------------------------------------------- --------- <%@ taglib uri="/tags/tiles" prefix="tiles" %> <tiles:useAttribute name="title"/> <tiles:useAttribute name="subtitle"/> <tiles:get name="header"> <tiles:get name="message"/> <tiles:get name="content"/> <tiles:get name="navbar"/> |
4.最后,用Tiles Definition替代ActionForward中相关的JSP:
<action path="/article/View" type="org.apache.scaffold.struts.ProcessAction" parameter="org.apache.artimus.article.FindByArticle" name="articleForm" scope="reques t" validate="false"> <forward name="success" path=".article.View"/> </action> |
下面的图表示Tiles ActionServlet截获Definition到布局页面的流程:
titles(JSP/HTML/text) [<header fragment> <content fragment> <footer fragment>] (做的真烂!~_~) |
在运行时,Tiles ActionServlet会截获ActionForward,再次在Tiles配置文件的Definition中检查它的路径。如果发现有一个匹配的,它会在response中包含Definition的每个tile。容器然后处理包含的每个tile,HTML tiles被容器的HTML服务实现,JSP tiles被容器的JSP服务实现。
如果ActionForward路径不是一个Definition的名称,ActionServlet会把它作为普通的URL处理。
注意:如果你重构的页面不能显示原来的内容,首先确定在tiles你已经引入了所有需要的标签库。如果标签库没有被包含,浏览器会忽略这种标签,标签就不会正常显示。如果这个没问题,创建一个新的页面,一步一步把页面放到新的页面来找出错误。你会发现一个tile的问题常常是一个元素的打开和关闭不符合规定。另一个规定是检查关于ActionMapping的输入参数的路径,你应更仔细的检查Definition的名称。在改为Tiles后如果你得到的错误为:必须为绝对路径(Path must be absolute),意思是说你试着用一个Definition作为定向的路径但它在Tiles配置文件中没有被找到。在检查完Definition后,Tiles把路径传递给父类的方法,Structs创建它作为系统路径。点号用来指明目录的相对路径,因此会提示"Path must be absolute"。
测试你的更改时,要重载应用程序以便当前的Structs和Tiles配置能够装载到内存中。
当你用<tiles:insert>重构页面并把它转变为一个Definition的步骤都处理完,通过测试后,你可以期待把其它的页面直接改为Definition了。你要做下列步骤:
- 拷贝一个已有的Definition ,并使用相同的布局.,再给它取个新名称。
- 粘贴和保存你重构页面的代码片断。
- 修改新的Definition,引用刚刚存储和转换的代码片断。
- 测试,重复上面的步骤。
注意:当你第一次把一个页面块转变为一个Tiles时,也许还要为它花一些时间,因为你还要为新的tiles创建一些JSP页面,以及一些额外的内容。一旦为每个tile的JSP创建好后,只有到下次修改JSP时才会被重新创建,一切又都会恢复正常。以后如果你要编辑tile的内容,只有那个JSP会被重新编译。
使你的基础布局规范化
在上面的例子中,我们在布局文件中列出了一串<tiles:insert>标记。如果你喜欢,也可以在你的布局页面中使用HTML和JSP代码,放置顶级标记是个好主意,像<HTML>或<html:html>,以便它们不会被放入需要做其它事情tiles之中。
在下面的例子中,从header和navbar tiles中抽取了顶级元素,然后把它们放到了基本布局中。我们可以把Base.jsp重命名为Article.jsp,更清楚的表达这个tile的功能。在应用程序中的其它页面也能使用一个不同的布局。
Revised layout tile (/tiles/layouts/Article.jsp) --------------------------------------------------------------------------------------- --------------------------------------------------- <%@ taglib uri="/tags/tiles" prefix="tiles" %> <%@ taglib uri="/tags/struts-html" prefix="html" %> <html:html> <HEAD> <html:base/> <LINK rel="stylesheet" type="text/css" href="<html:rewrite forward='baseStyle'/>"> <TITLE>Artimus - <bean:write name="title"/></TITLE> </HEAD> <tiles:useAttribute name="title"/> <tiles:get name="header"/> <tiles:get name="message"/> <tiles:get name="content"/> <tiles:get name="navbar"/> </BODY> </html:html> |
在将页面转换为布局和Definition时, 最后可能很容易成为这样:
<definition name=".article.View" path="/tiles/layouts/Article.jsp"> <put name="title" value="View Article" /> <put name="header" value="/tiles/header.jsp" /> <put name="messages" value="/tiles/messages.jsp" /> <put name="content" value="/pages/articles/view.jsp" /> <put name="navbar" value="/tiles/navbar.jsp" /> </definition> <definition name=".article.View" path="/tiles/layouts/Article.jsp"> <put name="title" value="Search Result" /> <put name="header" value="/tiles/header.jsp" /> <put name="messages" value="/tiles/messages.jsp" /> <put name="content" value="/pages/articles/result.jsp" /> <put name="navbar" value="/tiles/navbar.jsp" /> </definition> |
<definition name=".article.Base" path="/tiles/layouts/Article.jsp"> <put name="title" value="${title}"/> <put name="header" value="/tiles/header.jsp"/> <put name="message" value="/tiles/message.jsp"/> <put name="content" value="${content}"/> <put name="navbar" value="/tiles/navbar.jsp"/> </definition> <definition name=".article.View" extends=".article.Base"> <put name="title" value="View Article"/> <put name="content" value="/pages/article/view.jsp"/> </definition> <definition name=".article.Result" extends=".article.Base"> <put name="title" value ="Article Search Result"/> <put name="content" value="/pages/article/result.jsp"/> </definition> |
有个约定,我们在第一和第四个条目(title和content)放置标记,作为子类Definition需要去重载的扩展点。如果基本的Definition被直接使用,这些标签会被直接打印出来。对Tiles来说${}标签没有特殊的意思。
另一个惯例布局JSP首字母大写,tile JSP首字母小写。这表明布局页面能被直接使用,因为它是一个完整的JSP,布局JSP就像调用方法一样来使用tile页面。但这只是一个惯例,其它的命名方式也能工作的很好。
- 通常会拷贝一个相似的文件,在tag.xml中创建一个新的Definition。
- 用存在页面、tile和其它自定义信息更新Definition的路径。
- 打开一个现有的页面。
- 删除头和尾内容,留下核心的内容和tag导入语句。
- 检查和修正核心内容,在Definition确认标签是否符合规定。一个tile可以打开一个元素,像<TABLE>,在另一个tile中务必要关闭它。移除所有不必要的tag引入声明。随意的添加一些注释。
- 更新Struts配置文件(struts-config.xml))中新Definition的路径,包括所有的input属性。
- 重载tag和Struts配置文件。
- 检查页面。
- 反复直至满意。
首先,你可能先用一些页面来呈现应用程序的轮廓。一旦你构造好程序框架,通过页面的关联(page tree)依次完成和重构每个页面并不困难。这确保你不会失去任何东西。它也能发现以前开发过程中的一些无用的页面。
管理迁移
迁移一个应用程序为Tiles结构并不困难但也不要小看它。对项目要确保有足够的时间规划。开始少数的页面可能会花去几个小时,一旦确定了结构,添加每个页面就只需几分钟了。
如果你在网站要采用一个新的界面,最好是先把它转换成Tiles结构,再修改界面,免得同时进行两项新的任务。一旦迁移到Tiles后,改变一个界面将更快速简单。
开始迁移的最好的时间是在你知道将要对站点进行改版,而手头有没有新设计要去修改时。如果你已经把应用程序迁移到Tiles中,即使新设计最后才定下来,改变工作将非常顺利。不推荐一次做两个步骤--尤其是你第一次迁移的时候。
那么迁移的底线是什么呢?一个有25个表达页面的小型的应用程序,大约有140000 kbytes个标记代码,可能被迁移到55个tiles中,而代码减少到120000 kbytes,15%的多余代码被去除。
现在新的页面创建更加快速,容易和现有的页面保持一致。要改变整个站定的布局时,只要修改整体布局或少数几个tiles,而不是站点的每个页面了。