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 onload="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 onload="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,而不是站點的每個頁面了。