這個簡短系列的第一篇文章 是 Tapestry 的介紹 —— Tapestry 是一個構建 Web 應用程序的輕量級框架。如果曾經閱讀過那篇文章,應當對 Tapestry 的工作方式有了基本的理解,並了解了如何把它的核心 API 組合在一起。您可能還不敢確定如何從頭開始實際開發 Tapestry 應用程序,但是在這篇文章中,我將解決這個問題。
我要從一種簡單的應用程序規劃方式開始,在使用 Tapestry 時,這個方式特別重要。然後,我將討論 HTML 原型在 Tapestry 開發中的角色,並解釋在編寫 Tapestry 組件之前需要具備的元素。最後,我將介紹如何開發 Tapestry 組件,並把它們與 HTML 頁面鏈接起來。您還會學到一些技巧,以確保您的規劃能夠適應使用應用程序的人,這是成功開發的關鍵,並介紹如何為了重用而規劃和開發 Tapestry 組件。
規劃應用程序
如果您屬於某類開發人員,您可能會憎恨規劃 這個詞,認為應該把時間花在做些實際的事情上!但是,規劃是開始構建 Tapestry 應用程序(或者其他類型的應用程序)的最好途徑,所以我將從介紹如何盡可能沒有痛苦地進行規劃開始。
Tapestry 框架使用實際的 HTML 頁面,把它們當作模板,並把這些模板與 Tapestry 組件鏈接在一起。然後通過部署描述符把所有這些捆綁在一起,形成耦合緊密且相當復雜的文件集。Tapestry 應用程序有以下的典型組件:
HTML 頁面(被 Tapestry 當作模板)
Tapestry 類
Java bean 和工具類
servlet 部署描述符 (web.xml)
Tapestry 的應用程序描述符(app.application)
如果一個猛子扎進去開始開發應用程序代碼,眼前很快就會充滿雜亂的注釋、難以尋找的 bug,以及有時已更新有時沒更新的模板。規劃是真正有效利用 Tapestry 的惟一途徑,所以請嘗試盡可能無痛苦地做規劃的這個三要點方式。
從問題開始
規劃的第一部分是詢問一個簡單但是非常重要的問題:這個應用程序要做什麼? 雖然看起來可能微不足道 —— 甚至陳腐 —— 但是,這是對應用程序所能提出的最重要的問題。而且,令人驚訝的是,它又是最經常被忽略的。下面是在開發過程開始時一些比較常見的問題的列表(聽起來挺熟?):
踐踏客戶和規范!
不,不是真的。即使您憎恨規劃,如果沒有與應用程序的利益相關者(stakeholder)溝通,那麼就要准備失敗了。不論代碼有多漂亮,都必須按照客戶的期望工作 —— 哪怕您對客戶的期望不以為然,也要如此。所以,現在請保留對規劃的判斷(即,它有多討厭),並嘗試我在這裡演示的方式。每次都會形成更好的應用程序!
要使用什麼技術?
什麼樣的客戶機(Web 浏覽器、移動電話、PDA)要訪問應用程序?
應用程序要在哪個平台上運行(Windows、Linux、Mac OS X、Sun Solaris,等等)?
什麼時候必須完成?
這些都是有用的問題,但是如果應用程序實際做的工作不是利益相關者所期望的,那麼所有這些都無關緊要。
把這應用於 Tapestry,如果不知道應用程序的基本目的,就不會寫出好的 Tapestry 代碼 —— 不論代碼的技術有多漂亮。Tapestry 代碼用於兩個基本目的:
與數據源交互並實現業務邏輯。
向表示組件(像 HTML 模板)提供數據。
因為編寫 Tapestry 代碼不是為了做炫耀的顯示或視覺效果,所有的 Tapestry 代碼都應當處理應用程序的核心任務。如果不知道核心任務是什麼,那麼就等著浪費大量時間開始一個空白屏幕,然後迷惑不解地想知道接下來要做什麼吧。
描繪出業務過程
一旦理解了應用程序的基本目的,就可以開始列出實現這個過程所包含的全部具體業務過程。業務過程與應用程序的目的不同。應用程序的目的 通常是簡潔的、高層的描述,概括應用程序做的每件事。業務過程 則是小得多的工作單位,通常以各種方式和其他業務過程一起重用,共同實現特定的任務。
例如,假設要編寫一個應用程序,管理在 BurgerDome 的廚房。基本目的可能只是 “讓客戶得到想要的,按單烹饪”。但是這個目的自然有許多獨立的業務過程;比如下面這些:
下新單
做一個漢堡
向漢堡加蔬菜和調料
將漢堡打包,遞送給客戶
當然,可能會發現這些過程還可以進一步細分:
做漢堡
做漢堡餡
加熱餅胚
烤餅胚
給漢堡加材料
加蔬菜
加調料
加調味品
加芝士
這些過程可能有點傻,但是很快就會看到即使一個相對簡單的應用程序也會有 50 或 100 個不同的業務過程,可以用它們或者把它們與其他過程組合在一起,實現一個基本目的(在這個示例中,是制作客戶點的漢堡)。
把業務過程轉換成組件
關於 Tapestry 中的業務過程,有趣的是,實際上只有很少的被綁定到 Tapestry 類。例如,BurgerDome 應用程序的大多數任務根本沒有綁定到顯示,而且構建一個 Tapestry 組件去給漢堡加芥茉沒有太大意義。在這個應用程序的規劃階段,描述出所有過程,會有助於確定實際需要使用 Tapestry 創建什麼業務組件,以及哪些組件要傳遞給其他類型的組件。例如,可能創建了一個 Tapestry 組件,接受漢堡訂單做為輸入,但是可以把它在添加調味品的請求中傳遞給另一個非 Tapestry 組件。
在規劃階段的末尾,應當已經描繪出一套處理業務的每個具體細節的業務對象。除此之外,應當有一套 Tapestry 對象,把這些業務對象組織成有意義的工作單元。在清單 1 中可以看到這個編程模型的實際作用:
清單 1. 簡單的訂單類
package com.burgerdome.display;
import org.apache.tapestry.annotations.Persist;
import org.apache.tapestry.html.BasePage;
import com.burgerdome.order.*;
public abstract class Order extends BasePage
{
private Order order;
public void placeOrder(int burgerType, int doneness)
{
Burger burger = new Burger(burgerType, doneness);
Order order = new Order();
order.addBurger(burger);
setOrder(order);
OrderQueue queue = OrderQueue.getInstance();
queue.add(order);
}
public void cancelOrder()
{
OrderQueue queue = OrderQueue.getInstance();
queue.remove(order);
}
protected void setOrder(Order order) {
this.order = order;
order.setID(OrderUtils.newOrderID());
}
protected Order getOrder() {
return order;
}
}
從清單 1 可以看到,Tapestry 組件用 Burger 和 Order 類與屏幕交互(假設允許客戶下訂單),這兩個類都是 BurgerDome 業務組件庫的組成部分。注意,Burger 和 Order 類不是 Tapestry 類,而且實際上根本不知道 Tapestry。另外,OrderQueue 類既不是 Tapestry 類也不是 業務組件,它處理許多業務方面:接受訂單並用有用的方式組織它們。
這裡的要點是,如果沒有仔細的規劃,這些過程就不會成為一個整體。關於確定哪個類特定於 Tapestry、哪個特定於業務,以及它們如何交互,所有這些都最好是在代碼編輯器中面對一個空白類之前做完。
創建導航圖表
一旦理解了擁有什麼 Tapestry 組件,以及基本業務組件如何協作,那麼開發應用程序的導航就相對容易了。這個時候請記住,可能還不需要編寫 HTML 頁面或者四處亂放 href。用兩頁紙畫好頁面,並用箭頭把頁面連接起來,應當就夠了。或者用另一種方法,即采用簡單的流程圖,圖上的每個方形或矩形代表一個屏幕,箭頭代表用戶在應用程序中可能經歷的各種 “路徑”。
不論如何做,規劃應用程序的導航實際上是一個簡單但卻會大大有利於開發過程的實踐。一旦開始手動構建 HTML 頁面(下一節將詳細討論),就不必再花時間考慮在頁面上有什麼鏈接、應當鏈接到哪,因為所有這些都已經規劃好了。
描繪導航的一個重要的附帶作用就是可以迅速地發現 “孤兒頁面”。孤兒頁面是這樣一些頁面,它們要麼是沒有鏈接到其他頁面,要麼是沒有應該那麼多的頁面鏈接到它們。這些頁面代表的特性是,用戶需要與之交互,但是如果不糾正導航設置,這些特性就可能無法(或者容易地)訪問到。在應用程序開發過程中,由於沒有正確地把頁面連接到其他頁面,所以孤兒頁面很常見。在開始編碼之前規劃應用程序的導航,是盡早隔離並糾正問題的好方式,這會形成更平滑的開發周期。
反復規劃
關於規劃,需要知道的最後一件事就是,規劃不可能完美,也不應當一成不變。雖然規劃很重要、很有用,但是肯定不可能考慮好每件事,甚至已經做了的也會改變。不論花多少時間考慮應用程序的目的、規劃它的導航,都會遺忘一些事情。而且,即使什麼也不忘記,也必須面對後來的特性請求或者在最想不到的地方出現 bug 或者生活本身的不可見因素;所以請保持靈活。
形成健壯的計劃,然後願意根據需要調整它,那麼在應用程序開發中就占了先機 —— 特別是在使用 Tapestry 開發應用程序時。
編寫 HTML 頁面
應用程序規劃好後,就可以編寫 HTML 頁面了。雖然從嚴格意義上講,這不是真正的 “編碼”,而且可能被當作可以留在最後再做的事情,但是應當一直從編寫 HTML 代碼開始應用程序的開發。
什麼時候開發業務對象
編寫業務對象的時機,對於具體的項目和公司是各不同的。對於許多項目來說,將使用現有的業務對象,所以根本不必編寫代碼(或者可能只是編寫少量代碼)。在某些情況下,編寫的業務對象會用於多個應用程序,所以最好在編寫具體的應用程序代碼之前開發這些對象。而在其他情況下,可以在編寫應用程序的剩余部分時開發業務對象。簡而言之,開發業務對象的正確時機主要依個人偏好和項目要求而定。
從 HTML 開始的最大理由是:客戶、最終用戶、營銷團隊、經理以及 alpha 和 beta 測試人員會看到這些頁面。雖然可以給 Java 類添加 main() 方法,並在命令行測試它們,但是多數用戶會發現 Web 浏覽器最適合測試 Web(特別是 Tapestry)應用程序。
更重要的是,Tapestry 用 HTML 文件作為它的頁面模板,所以如果沒有基本的 HTML 頁面,開發 Tapestry 組件的壓力會很大。在許多情況下,頁面設計實際上會指明您為 Tapestry 組件所做的決策。
這一節介紹編寫應用程序 HTML 的基礎。
從原型開始
在開始拼湊出成百行的 CSS 樣式表和復雜的流動布局之前,請認識到最好的應用程序原型是簡單的,有時甚至是粗陋的。最好是從一個非常基本的頁面開始,就像清單 2 所示的那樣:
清單 2. 銷售報表的原型
<html>
<head><title>Sales Report Prototype</title></head>
<body>
<h1>Prototype Sales Report</h1>
<table>
<tr><th>Total Sold</th><td>1012</td></tr>
<tr><th>Sales Price</th><td>$29.95</td></tr>
<tr><th>Manufacturing Cost</th><td>$8.22</td></tr>
</table>
<h2>Net Profit: $167718.76</h2>
<form method="GET">
<input value="Get Updated Sales" type="button" />
</form>
</body>
</html>
圖 1 顯示了這個頁面出現在 Web 浏覽器中的效果:
圖 1. Web 頁面原型
這看起來不是很像,但這就是關鍵!松散的、簡單的頁面,可以容易地移動或修改事物。添加表格行或把標題移動到頂部,做起來會很簡單,不會弄亂精心調整的布局或顏色方案。另一方面,請想像一下,如果花了幾個小時處理樣式表,才能讓標題就位,讓打印效果和顏色方案滿意,然後進入會議室,卻聽到下面這些評論,您會有什麼感受:
“這個菜單項需要再向下一點”
“我討厭橙色的陰影;公司的 logo 用海藍色。”
“我們不能用 serif 字體麼?”
關於這類評論,最糟的是,它們並不是您想得到的應用程序資金提供者、銷售人員或用戶的評論。但是,需要指出的重要的一點是,這些評論沒有一個與頁面應當做什麼、它要傳遞的基本信息是什麼有關。在原型階段保持事物簡單,可以讓應用程序的利益相關者對於應用程序的功能擁有基本的認識,而不會把大量時間浪費在顏色和設計這些可能會改變的問題上。如果有人抱怨原型中事物看起來的樣子,您可以向他們保證最後完成的 Tapestry 應用程序看起來會很棒!
這是一項未完過程
就像我在清單 1 和圖 1 中所做的那樣,請一直認真地把原型標記為原型。在 HTML 的標題上(在 title 元素中)和實際的頁面中都要放上單詞 “prototype”(h1 或 h2 元素通常可以勝任這個工作)。清楚地標記原型過程的每一部分的好處很明顯:
在向銷售人員和最終用戶介紹原型時,他們不會認為自己正在看的是完成的產品。這會擋住那些讓大多數開發人員發瘋的評論,例如 “為什麼它看起來這麼差?” 和 “我們能不能讓表格的文本用紫紅色?”
如果有些勤奮的銷售經理把您的工作展示給總經理,他們也會 認識到工作還在進行當中,而不會闖入您的辦公室,質問您為什麼付給您那麼多,您卻只拿出這麼可怕的 Web 頁面。
在開始開發 Tapestry 組件時,這些 “prototype” 標題可以提醒您頁面什麼時候已經真正完成而什麼時候仍然在進行當中。請確保在頁面編碼完成,可以部署和測試時,刪除 “prototype” 標題是所做的最後一件事。
雖然原型法一直是個好的開發實踐,但是 Tapestry 還進一步讓它成為特別有益的 實踐。在某些開發環境下,原型模板在進入實際開發時,有時會失效,但是使用 Tapestry,可以方便地用原型作為開發應用程序的模板。例如,在使用獨立的 Java servlet 時,最後必須拋開原型模板,把 HTML 拷貝粘貼到 out.println() 語句或 JSP 標記中。但是使用 Tapestry,可以把原型屬性有效地應用到開發工作中。如果要證明在 HTML 模型上花的時間沒白花,Tapestry 可以證明!
使用真實數據
基本頁面准備好之後,需要用真實數據填充模型。對於多數開發人員來說,很容易用簡單的金額(例如 $99.95)代表銷售額,後面再用一些傻乎乎的文本值,例如 “foo” 或者一些長長的拉丁字符串(對於我和其他要查看模型的人來說,這種做法毫無意義),但是請不要這麼做!由於兩個原因,使用不真實的占位符是個壞主意:
“假” 數據不能代表真實數據在頁面上會占據的空間。
頁面包含一兩個單詞時的樣子,與包含多行文本時的樣子,會有顯著不同。同樣,只有二、三位數字的數值與有五、六位數字的值看起來也不同。
我不是在建議您需要去做無數的調研,然後在頁面中使用正確的、實時的數據。只需要對於數據最後在頁面上會是什麼樣子有良好的認識就可以。例如,如果銷售價格的范圍預計是在 $50.00 和 $5,000.00 之間,那麼這對開發真實的原型就是足夠的信息。對於文本長度來說,也是一樣,不過,可能需要更具體一些的范圍;如果文本要在 1,500 和 3,000 字之間,可以規劃得很好。如果文本會在 1,500 和 30,000 字之間,那麼就很難編寫出能容納兩端范圍之間的標記(所以也應當讓利益相關者知道這一點)。
對於數據的范圍和類型有了主意之後,可以開發能夠顯示范圍兩端的原型;例如,對一個頁面,使用 $50.00 的值,另一個頁面用 $5,000.00 的值。這樣可以保證所有可能的值都會適合所分配的空間,按照預期形式換行,格式化也正確。對於文本、標題、表單字段等,也是一樣的。在頁面上出現的任何包含數據的內容,都應當用真實數據而不是一堆 “程序員” 值進行測試。
添加結構元素
現在,可以添加 JavaScript、圖片和活動導航了。在許多 Web 開發框架下,可能就是在這裡拋棄原型,開始把標記轉換成 servlet、JSP 或視圖組件的。但是,使用 Tapestry,可能已經得到了有用的模板,只需要添加一些 “分隔和修飾”,好讓原型更適合使用。
可以從為頁面添加 CSS 樣式表開始,與 div 和 span 元素一起,區分頁面不同區域並設置區域的樣式。現在開始要認真了,可能還需要改變一些頁面結構,例如從占位符列表轉換成表格,或者反過來。所有這些變化,都會給頁面添加新的結構層和樣式,使它們更加可用於應用程序中。
如果想用 JavaScript 給頁面添加動態值或處理圖片交換,現在可以把這個代碼加到頁面裡了。也可以給頁面添加圖片。不論做什麼,請記住,正在處理的頁面,會實際地 由 Tapestry 用來驅動應用程序,所以正在執行的是有用的工作,而不再是浪費時間在完善模型上。
添加結構到模板
在這個階段所做的全部工作是創建更好的模板。而且,對於 div 和 span 元素,實際上是在准備 Tapestry 交互。在清單 3 中可以看出這點,清單 3 是清單 2 所示的原型的 HTML,只是新添加了一些 div 和 spans:
清單 3. 添加銷售報表的結構
<html>
<head><title>Sales Report Prototype</title></head>
<body>
<h1>Prototype Sales Report</h1>
<div id="sales">
<table>
<tr><th>Total Sold</th><td><span id="total-sold">1012</span></td></tr>
<tr><th>Sales Price</th><td>$<span id="board-cost">29.95</span></td></tr>
<tr><th>Manufacturing Cost</th><td>$<span id="man-cost">8.22</span></td></tr>
</table>
<h2>Net Profit: $<span id="net-profit">167718.76</span></h2>
<form method="GET">
<input value="Get Updated Sales" type="button" />
</form>
</div>
</body>
</html>
雖然變化不大,但是可以看出頁面中的每個值現在有了一個 span 設置,還有一個 ID 標記。所以,頁面可以容易地轉化成 Tapestry 組件,下一節就會看到。頁面中的數據是真實數據(基於項目的規范),結構也准備好了,可以轉入 Tapestry 應用程序了。
導航性注釋
惟一剩下要做的就是創建頁面間的導航路徑。現在,可以用普通的 HTML a 元素加 href 屬性做這件事,但是在創建最終應用程序時,這些鏈接可能必須變化。如果在 Tapestry 中想避免大多數頁面間的直接鏈接,可以訪問 “即時的” Tapestry 頁面,它只使用 HTML 文件作為模板。
即使有這個秘訣,也要花相當的時間構建一套頁面間的鏈接。理解這些鏈接將去向哪裡,它們如何呈現給用戶,是構建 HTML 框架的重要部分。
結構元素、圖片和導航鏈接都設置好之後,幾乎就可以開始編寫 Tapestry 組件了。在這之前,最好是讓團隊、經理、銷售人員,可能還有 alpha 測試人員,把所有內容都運行一下。這可以確保在開始編碼之前,應用程序的觀感符合利益相關者的需要。
構建 Tapestry 組件
對於大多數開發人員,第一次使用 Tapestry 的最大驚訝就是幾乎沒什麼事可做。如果在應用程序規劃和 HTML 頁面上的工作做得很好,那麼編寫 Tapestry 代碼就變得非常簡單。Tapestry 主要作為粘合劑,將應用程序的表示和驅動應用程序的邏輯粘合起來,所以實際上在 Tapestry 中上花在編寫復雜代碼的時間驚人地少,更多的時間花在把 HTML 頁面連接到業務組件上。
實際上,這是 Tapestry 的一個真正亮點:替您鋪好路。因為編寫了標准化的 HTML,並添加了一些特定於 Tapestry 的元素,所以幾乎沒有影響 HTML 的 Tapestry 代碼。更好的是,Tapestry 標記不會影響應用程序的顯示,所以您的標記可以包含這些標記,您的設計什麼也不包含。換句話說,在使用 Tapestry 和不使用它的頁面之間,永遠不會看出視覺上的差異。而且,業務邏輯也根本不會被 Tapestry 影響。惟一真正屬於 Tapestry 的代碼就是一套簡單的類,用來把應用程序的所有片段連接起來。在使用其他許多框架時,從只有 servlet 或 JSP 的框架到諸如 Struts 或 Spring 之類更復雜的框架,通常要編寫許多特定於框架的和與框架有關的代碼。幸運的是,使用 Tapestry 時不需要這樣。
處理業務邏輯
在開始編寫 Tapestry 組件之前,還有最後一件必須要做的事:必須確保沒有業務邏輯終止於 Tapestry 代碼中。這意味著應當精心定義了(最好已經編寫了)所有業務任務的類。如果沒有做這個工作,就會被引誘著(通常是被迫)把一些邏輯放在 Tapestry 頁面中。有時,這麼做會是讓應用程序運行起來的 “最快” 途徑,但是在這類情況下,“最快” 通常是名不符實的,因為業務邏輯更改時,要花很多時間對應用程序做修改(如果因為訂購鞋的方式或冰箱送貨的方式變化,就必須修改驅動顯示的代碼,那麼這可不是好的應用程序設計!)
所以,請在應用程序的表示(已經開發的 HTML 頁面)、業務邏輯(已經准備好的 Java 類,通常在另一個 JVM 或服務器上)和把這些部分連接到一起的膠水(Tapestry 代碼)之間保持清晰的界限。遵守這個簡單的原則,會讓 Tapestry 代碼編寫起來更快,因為只是調用業務對象並用調用的結果更新表示而已。
從對象開始
開始開發 Tapestry 組件時,可以對需要至少一段動態數據或需要與業務對象交互的 HTML 頁面做一個列表。這個列表應當只有文件的名稱和頁面目的的簡短描述,例如:
Home.html:應用程序主頁。
Order.html:新訂單的主訂單頁面。
Status.html:檢查訂單狀態的頁面。
Comments.html:留言頁面。
對於列表中的每個頁面,都要創建一個新的 Java 類,可以用與頁面相同的名稱作為類名。例如,清單 4 是驅動 Comments.HTML 頁面的類的骨架代碼:
清單 4. 檢查訂單狀態的簡單 Tapestry 類
package com.burgerdome.display;
import org.apache.tapestry.annotations.*;
import org.apache.tapestry.html.BasePage;
public abstract class Status extends BasePage {
@Persist
public abstract int getOrderNumber();
public abstract void setOrderNumber(int orderNumber);
// Methods go here
}
可以從這些行開始每個 Tapestry 類:類名與它交互的文件的名稱(Status.java 用於 Status.html、Comments.java 用於 Comments.html,等等),確保類擴展自 org.apache.tapestry.html.BasePage 類。確保給 Tapestry 類提供了一個包;通常在同一包中找到所有頁面最容易。還需要導入 BasePage,而且前進一步並導入 Tapestry 注釋也是一個好主意;在開發的幾乎每個 Tapestry 組件中都會使用它們。
最後,前進一步,設置可能需要的持久變量;清單 4 中的示例保存一個訂單號,該訂單號用於在應用程序的業務對象區查詢訂單。請了解 @Persist 並不代表 Tapestry 要在數據庫或其他永久存儲中持久化或保存變量;它只表明變量在重復調用對象實例期間一直可用。這意味著可以允許用戶只輸入值(在這個示例中代表訂單)一次,然後反復使用這個值,而不需要用戶每次返回狀態頁面都輸入這個值。還請注意,沒有為持久變量聲明類型;只是提供了 “getter” 和 “setter” 方法,而 Tapestry 負責剩下的處理。類本身被標記成抽象的,這允許 Tapestry 負責設置類的實例,並把實例掛接到 Tapestry 引擎。
清單 4 中的簡單示例可以充當所有 Tapestry 對象的起點。只要修改名稱和任何需要的持久變量,讓頁面對象投入使用的工作就完成了一半(有時甚至更多)。
添加操作
下面考慮沒有綁定到頁面上的簡單值的操作。例如,在狀態頁面中,可能讓用戶輸入訂單號,然後讓另一個按鈕或鏈接向用戶提供他們的狀態。第一個操作被緊密地綁定到清單 4 所示的 setOrderNumber() 方法,第二個操作則需要查詢訂單號。清單 5 展示了處理這個任務的簡單代碼:
清單 5. 添加訂單處理
package com.burgerdome.display;
import org.apache.tapestry.annotations.*;
import org.apache.tapestry.html.BasePage;
import com.burgerdome.order.*;
public abstract class Status extends BasePage {
public abstract Order getOrder();
public abstract void setOrder(order);
@Persist
public abstract int getOrderNumber();
public abstract void setOrderNumber(int orderNumber);
public void getStatus() {
OrderQueue queue = OrderQueue.getInstance();
Order order = getOrder(getOrderNumber());
setOrder(order);
}
}
您會注意到這個代碼中的幾個新部分。首先,導入了一些業務對象;在這個示例中,這些對象在 com.burgerdome.order 包中。其次,我添加了兩個新方法:getOrder() 和 setOrder()。這兩個方法被標記為抽象的,這樣 Tapestry 會把它們實現為簡單的 “getter” 和 “setter” 方法,並為這個類創建類型為 Order 的新變量。除非確實有好的理由不這麼做,否則最好是讓 Tapestry 替您管理這些變量。
還請注意這兩個新方法被放在 @Persist 注釋上面。這意味著訂單在請求或會話間不會被持久存儲。因為訂單在變化,所以在每次請求的時候查詢它並檢查它的狀態會更容易。還請記住,因為業務對象通常在獨立的 JVM 中運行,訂單可能在另一個 JVM 中在變化,所以本地保留的對象拷貝可能會過時。
一般來說,只持久存儲對客戶來說不改變的條目(比如訂單號本身,它在請求之間不會改變)或者不由業務對象使用的條目。例如,可能有一個分配給客戶的表編號,業務對象不會在上面操作,所以讓它持久存儲在 Tapestry 中是可以接受的。
這個類中的最後一件事是添加了 getStatus() 方法,用於把訂單號(已持久存儲的)連接到訂單,該訂單是在每次請求時都要查詢的。用戶查找到他們的訂單之後,就可以容易地通過訂單的方法訪問訂單了;可以調用 order.getRemainingCookTime() 或 order.change() 方法,還可以在需要的時候讓 Tapestry 把這些請求發送回業務層。
在您自己的應用程序中,這是惟一真正需要編寫許多特定於 Tapestry 的代碼的地方:當與顯示有關的事件發生時,必須修改業務對象。用這種方式,Tapestry 把用戶的動作連接到後端代碼,後端代碼對這些動作作出響應。
回顧 HTML 鏈接
Tapestry 代碼就緒之後,就需要回到 HTML,把 HTML 與剛才編寫的代碼連接起來。實際上,要做的全部工作只是找到頁面中的所有 a 和 span 元素。對於 a 元素,先找出哪個鏈接到外部頁面 —— 在其他站點上、不是應用程序的一部分的頁面 —— 並 “丟棄” 它們(換句話說,不用考慮它們)。剩下的應當具有像這樣的鏈接:
<a href="Home.html">Return to main screen</a>
添加另一個屬性,叫做 “jwcid”,它的值為 “@PageLink”。這讓 Tapestry 知道正在創建到其他 Tapestry 頁面的鏈接。現在的鏈接看起來像這樣:
<a href="Home.html" jwcid="@PageLink">Return to main screen</a>
然後,把 href 屬性的名稱改成 “page”。page 屬性讓 Tapestry 知道要連接到其他哪個由 Tapestry 控制的頁面(這就是為什麼可以忽略外部鏈接的原因:它們在那呆著就很好)。然後,刪除 “.html” 擴展名。現在的鏈接看起來像這樣:
<a page="Home" jwcid="@PageLink">Return to main screen</a>
最後,a 元素要求 href 屬性,所以把它添加回去,但是值為 “#”。這告訴 HTML 把它連接回同一頁面,然後 Tapestry 處理實際的鏈接。最後的鏈接看起來像這樣:
<a page="Home" jwcid="@PageLink" href="#">Return to main screen</a>
用這種方式把每個 HTML 頁面中的每個鏈接進行轉換,然後應用程序的導航會在 Tapestry 和動態頁面之間開始工作,而不是在靜態的 HTML 模板之間。
添加動態數據
現在需要把假日期更新成即時值。通過 @Insert 注釋做這件事最容易。作為示例,請看清單 6,它把第一個 span 標記轉換成使用來自與這個頁面相關的 Tapestry 類的值:
清單 6. 添加真實數據到 HTML 模板
<html>
<head><title>Sales Report Prototype</title></head>
<body>
<h1>Prototype Sales Report</h1>
<div id="sales">
<table>
<tr><th>Total Sold</th><td><span jwcid="@Insert" value="ognl:totalSales"
id="total-sold">1012</span></td></tr>
<tr><th>Sales Price</th><td>$<span id="board-cost">29.95</span></td></tr>
<tr><th>Manufacturing Cost</th><td>$<span id="man-cost">8.22</span></td></tr>
</table>
<h2>Net Profit: $<span id="net-profit">167718.76</span></h2>
<form method="GET">
<input value="Get Updated Sales" type="button" />
</form>
</div>
</body>
</html>
@Insert 告訴 Tapestry 插入數據,而 value 屬性則用來指定插入什麼數據。在這個示例中,“ognl” 是 Tapestry 頁面可以使用的庫,用它可以調用 Tapestry 頁面;“totalSales” 是頁面對象中的變量名稱。添加的這段代碼的結果是頁面顯示時有了真實數據,而不是 1012 這個假值。
還需要處理所有的頁面,做類似的修改,這樣所有的假數據就被真實數據替換了。而且,因為花了時間來確保示例數據的長度是真實長度,所以在真實數據插入頁面時,不會造成任何顯示問題。
一點配置……
所有後台工作完成之後,再與幾個配置文件連接起來就是小事情了。由於使用相同的名稱,頁面和類之間已經創建了隱式的映射。現在,需要通過顯式化,讓 Tapestry 引擎知道這個隱式映射。請在 servlet 上下文的 WEB-INF/ 目錄中創建一個新文件,叫做 app.application。然後添加一個叫做 “org.apache.tapestry.page-class-packages” 的新鍵,並用 Tapestry 類所在的包的名稱作為鍵值。清單 7 展示了一個示例 app.application 文件:
清單 7. app.application 文件
<?xml version="1.0"?>
<!DOCTYPE application PUBLIC
"-//Apache Software Foundation//Tapestry Specification 4.0//EN"
"http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd">
<application>
<meta key="org.apache.tapestry.page-class-packages"
value="tutorials.directlink.pages"/>
</application>
由於已經做了很多工作,所以這一步很簡單。另外,Tapestry 框架已經設置好,所以這也很容易;簡單的文件和類名稱已經准備好,所以這只不過是連接兩套組件而已。
還需要用標准的 Web.xml 部署描述符讓 servlet 引擎知道,應用程序也在 WEB-INF/ 中。清單 8 展示了需要的內容:負責核心 Tapestry servlet 的 servlet 入口,以及這個 servlet 的 URL 映射:
清單 8. web.xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/Web-app_2_3.dtd">
<Web-app>
<display-name>BurgerDome Ordering System</display-name>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.apache.tapestry.ApplicationServlet</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app</url-pattern>
</servlet-mapping>
</Web-app>
現在已經有了 HTML 頁面、Tapestry 組件、業務對象,以及把所有這些部分連接在一起的描述符。應用程序可以運行了!
結束語
在這篇文章中,我從 Tapestry 的初始概述(來自 了解 Tapestry,第 1 部分)轉移到介紹如何用 Tapestry 框架實際地開發應用程序。我沒有強調許多代碼,而是集中在使用 Tapestry 開發應用程序的過程以及如何做好上面。雖然多數 Java 程序員可以容易地學會一個新 API,但是許多人更困難的是弄清楚如何最好地使用這個 API。我希望您從這篇文章懂得的是 Tapestry 開發中規劃的重要性,以及良好的規劃如何會有利於最終的實現。我強調的 Tapestry 開發過程的另一個好方面是重用。通過在編寫組件之前仔細考慮,最終會得到可重用的組件工具箱,而不必編寫不能共享組件的許多不同的應用程序。
雖然這篇文章的主題是 Tapestry,但是我討論的許多想法可以應用於任何編程框架,特別是基於 Web 的框架。您會發現,大多數好的應用程序都涉及到精心的規劃,為了避免不斷的重新設計,不能草草規劃。如果花時間仔細規劃,然後設計原型,那麼最後差不多總會得到更精致的、響應更好的應用程序,也會極大地提高客戶和最終用戶對最終產品滿意度。