我们开始来构建一个基础的Hi-Lo猜谜游戏。
在这个游戏中,计算机会选择一个介于1到10之间的数字。你尝试猜出这个数字,点击一些链接。最后,计算器会告诉你确认目标数字你需要猜多少次。即使是像这样一个简单的示例,也能体现Tapestry中的几个重要概念:
l 将一个应用程序分段放到各自独立的几个page中
l 将信息从给一个page传送到另外一个page
l 响应用户的交互
l 在服务器端session中存储客户端信息
我们将用几个小块的来构建这个小巧的应用程序,使用Tapestry来进行这种迭代式的开发非常容易。
页面流程非常简单,包含三个page:Index(起始page),Guess以及GameOver。Index page对应用程序进行介绍,并包含一个开始猜谜游戏的链接。Guess page像用户显示10个链接,加上一些诸如“too low”,“too high”的提示信息。GameOver page告诉用户在找到目标数字之前他们已经猜测了多少次。
Index Page
先来处理Index page和模板。像下面这样创建Index.tml:
<html t:type="layout" title="Hi/Lo Guess"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">
<p>
I'm thinking of a number between one and ten ...
</p>
<p>
<a href="#">start guessing</a>
</p>
</html>
然后编辑对应的Java类,Index.java,删除其正文(不过目前你可以把import语句仍保留在那里)。
Index.java
package com.example.tutorial1.pages;
public class Index
{
}
运行应用程序,我们将会看到起始界面:
然而,现在点击这个链接一点反应都不会有,因为它现在还只是一个预留用来占位的<a>标记而已,并不是一个实际的Tapestry component。让我们来想想当用户点击这个链接时应该要发生些什么:
l 会有一个介于1到10之间的随机数据被选出来
l 花费的猜测次数应该被重置为0
l 用户应该被指引至Guess page以进行猜测
第一步我们得找到用户应该在什么时候点击这个“start guessing”链接。在一个典型的web应用程序框架中,我们最开始考虑的可能是URL和处理器,或者是某些类型的XML配置文件。不过现在是Tapestry了,因此与我们相伴工作的是类中的component和方法。
首先是component。我们想要在继续Guess page之前执行一个动作(选择数字)。ActionLink component就是我们所需要的;它会常见一个带有URL的链接,这个URL会触发我们代码中的一个动作事件……不过到这里我们已经超前了。首先还是要把<a>标记转成一个ActionLink component:
Index.tml(局部)
<p>
<t:actionlink t:id="start">start guessing</t:actionlink>
</p>
如果你刷新浏览器并将鼠标停留在“start guessing”上面,将会看到起URL现在已经是 /tutorial1/index.start 了,其表示的是page的名称(“index”)和component的id(“start”)。
现在如果你点击链接,页面会显示一个错误:
Tapestry要告诉我们的是需要为这个事件提供某种类型的事件处理器。这是个什么东西呢?
事件处理器就是Java类中的一个带有特殊名称的方法。方法的形式如 onEventnameFromComponent-id……这里我们想要的是一个叫做onActionFromStart()的方法。那我们是如何知道“action”才是正确的事件名称的呢?因为ActionLink就是这么规定的,这也是为什么它被命名为ActionLink的原因。
Tapestry再一次为我们提供了选择的余地;如果你不喜欢约定的命名方式,可以把@注解放在方法的前头,它能给予你按自己喜好命名方法的自由。有关于此的详细信息,见。本教程我们还是坚持使用约定命名的方式吧。
在处理一个component事件请求(由ActionLink component的URL触发的请求类型)时,Tapestry将会找到这个component并在其上触发一个component 时间。这是我们服务器端的代码所需要的回调,用来了解用户正在客户端上面做些什么。让我们先从一个空的事件处理器开始:
Index.java
package com.example.tutorial1.pages;
public class Index
{
void onActionFromStart()
{
}
}
在浏览器中我们可以通过点击刷新按钮的操作来重新尝试一下刚刚失败的component 事件请求……或者我们也可以重启应用程序。两种情况下,我们看到的时候默认的行为效果,就是简单地重新渲染了一下page。
注意事件处理方法并不必得是public的;它也可以是protected、private或者package private(如这个示例)的。根据约定,这样的方法都应该是package private的,如没有其它理由那么最少量的字符输入量就是依据了。
呃……目前只能对于我们会让方法得到调用这一点保持信任。这不怎么好……什么能快速的让我们确认这件事情呢?一种方法就是让方法抛出异常,不过这有点笨啊。
那么这样如何:向方法添加一个@注解:
Index.java(局部)
import org.apache.tapestry5.annotations.Log;
. . .
void onActionFromStart()
{
}
接下来在你点击链接时,就会在Eclipse的console面板中看到如下信息:
[DEBUG] pages.Index [ENTER] onActionFromStart()
[DEBUG] pages.Index [ EXIT] onActionFromStart
[INFO] AppModule.TimingFilter Request time: 3 ms
[INFO] AppModule.TimingFilter Request time: 5 ms
注解会指示Tapestry对方法的进入和退出记录日志。你就可以看到传入方法的参数,还有方法的返回值了……当然还有方法抛出的异常。这是一个强大的调试工具。这就是Tapestry的元编程能力的一个例子,我们会在本教程中相当多的用到它。
为什么我们会看到一次点击有两次请求呢?因为Tapestry运用了一种基于模式的方法,每次的component事件之后Tapestry一般执行的都是一次重定向redirect。因此第一次请求是来处理动作的,第二次请求是来重新渲染Index page的。你可以在浏览器中看到,因为URL仍旧是“/tutorial1”(渲染Index page的URL)。后续我们会回过头来扯这个。
我们已经准备好进行下一步了,涉及到将Index和Guess page连接到一起。Index会选择一个目标数字给用户去Guess,然后“接力棒”交给Guess page。
开始考虑关于Guess page的事情。它需要一个变量,其中存储的是目标值,还需要一个可以让Index page调用的方法,由其来设置目标值。
Guess.java
package com.example.tutorial1.pages;
public class Guess
{
private int target;
void setup(int target)
{
this.target = target;
}
}
在跟Index.java所处的同一个文件夹下面常见Guess.java文件。接下来,我们可以修改Index来调用Guess page类的setup()方法:
Index.java(修订版)
package com.example.tutorial1.pages;
import java.util.Random;
import org.apache.tapestry5.annotations.InjectPage;
import org.apache.tapestry5.annotations.Log;
public class Index
{
private final Random random = new Random(System.nanoTime());
@InjectPage
private Guess guess;
Object onActionFromStart()
{
int target = random.nextInt(10) + 1;
guess.setup(target);
return guess;
}
}
现在新的事件处理方法可以选择目标数字了,并且会告诉Guess page这个事情。因为Tapestry是一个被管理起来的环境,所以我们不用创建Guess的一个实例……管理Guess page的生命周期是Tapestry该管的事情。因此,我们该找Tapestry去要Guess page,就使用@InjectPage 注解。
Tapestry的page或者component中所有的属性域都必须是非public的。
当我们有了这个Guess page实例,就可以跟往常一样调用其方法了。
从事件处理器方法返回一个page实例,会指示Tapestry将一个客户端重定向发送给返回的page,而不是发送一个重定向给当前的page。如此当用户一点击“start guessing”链接,就可以看见Guess page了。
在你创建自己的应用程序时,要确保存储在final变量中的对象是线程安全的。似乎有违常理,但final是在许多个线程之间共享的。一般的实例变量则不是。幸运的是,Random的实现事实上就是线程安全的。
因此……让我们点击链接试试会看到什么:
啊!我们没有创建Guess page 的模板。Tapestry确实希望我们创建一个,所以我们最好这样做。
src/main/resources/com/example/tutorial/pages/Guess.tml
<html t:type="layout" title="Guess The Number"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">
<p>
The secret number is: ${target}.
</p>
</html>
点击浏览器上的返回按钮,然后再次点击“start guessing”链接。离我们的目标更接近了:
如果你向下滚动,会发现有一行说Guess.tml模板有一行存在错误。我们有一个叫做target的域,但它是private的,而且没有对应的属性,因此Tapestry不能访问到它。
我们只需要加上确实的JavaBean访问器getTarget()(最好setTarget()也加上)就行了。或者我们也可以让Tapestry来编写这些方法:
@Property
private int target;
@注解非常简单的指示Tapestry为你编写getter和setter方法。你仅仅只需要在你准备在模板用引用这个属性域时才这样做。
我们已经非常接近了,不过还有最后一个大的事情要处理。当你刷新了页面,你会看到target变成了0!
之前提过,Tapestry会在处理完事件请求之后发送给客户端一个重定向。这意味着页面的渲染发生在一个全新的请求之中。同时,每个请求的最后,Tapestry都会将每个实例变量的值擦除。因此这就意味着在component 事件请求期间target是一个非0的数字……但有时如果从网页浏览器处进来一个新的page渲染请求要渲染Guess page,那target属性域的值就会回到其默认的0。
这里的解决方案就是标记出其值应该在从一个请求到接下来的请求(再接下来、再接下来……)中持续存在。这就是@注解的由来了:
@Property
@Persist
private int target;
这跟数据库的持久化(这在之后的章节中会讲到)一毛钱的关系都木有。只是意在让值存储在请求之间共享的HttpSession中。
回到Index page并再次点击链接。最后,我们有了一个目标数字:
对于我们的起步阶段这就够了。让我们把Guess page 整出来,让用户可以做猜测。我们将显示猜测的次数,并且在他们做猜测的时候让次数累加。之后我们要关注猜测是高了还是低了,或者已经选择了正确的值。
在构建Tapestry的page时,你有时会先从Java代码开始,并构建对应的模板,而有时又从模板开始,并构建对应的Java代码。两种方式都是可以的。这里,我们先从模板中的标记开始,然后再来理会Java代码该怎么写才对。
Guess.tml(修订版)
<html t:type="layout" title="Guess The Number"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
xmlns:p="tapestry:parameter">
<p>
The secret number is: ${target}.
</p>
<strong>Guess number ${guessCount}</strong>
<p>Make a guess from the options below:</p>
<ul class="list-inline">
<t:loop source="1..10" value="current">
<li>
<t:actionlink t:id="makeGuess" context="current">${current}
</t:actionlink>
</li>
</t:loop>
</ul>
</html>
如此看起来我们是需要一个从1开始guessCount属性的。
我们也看到了一个新的component,Loop component。Loop component会迭代传入其source参数的值,对于每个值都在其正文中渲染一遍。在渲染其正文之前,它会更新其value参数。
这个特殊的属性表达式 1..10,会生成包含在从1到10中的一系列值。一般当你使用Loop component时,是在迭代整个一个List或者Collection的值,比如一次数据库查询的结果集。
因此,Loop component会将current属性设置为1,然后渲染其正文(就是<li>标记,还有ActionLink component)。然后它会将current属性设置为2然后再次渲染其正文……一直到10。
还要注意的是我们正在使用ActionLink component;现在它不再足够了解用户在ActionLink上的点击操作……我们需要了解用户点击的是哪次迭代输出的链接。Context参数可以让一个值被添加到ActionLink的URL之上,而我们则可以在事件处理方法中得到这个值。
ActionLink的值将会是 /tutorial1/guess.makeguess/3。就是page的名称,“Guess”,component的id,“makeGuess”,还有上下文的值,“3”。
Guess.java(修订版)
package com.example.tutorial1.pages;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
public class Guess
{
@Property
@Persist
private int target, guessCount;
@Property
private int current;
void setup(int target)
{
this.target = target;
guessCount = 1;
}
void onActionFromMakeGuess(int value)
{
guessCount++;
}
}
Guess的修订版本包含两个新的属性:current和guessCount。还有一个来自于makeGuess ActionLink component的动作事件的处理器;当前它只是累加这个计数而已。
注意onActionFromMakeGuess()方法现在有了一个参数:这个参数就是被ActionLink编码到URL中的上线文的值。当用户点击了链接时,Tapestry会自动从URL获取到字符串,将其转换为一个int并将这个int传递给事件处理器方法。并不要你写多余的什么代码。
到此,page有了部分的可操作性:
下一步就是实际去检查用户提供的值是否跟目标值匹配,并提供反馈信息:无非就是踩得高了,低了或者对了。如果猜对了,我们会切换到GameOver page,附上一条消息,比如“You guessed the number 5 in 2 guesses”。
我们先从 Guess page开始,现在需要的是一个新的属性,用来存储将展示给用户的消息,还需要一个属性域注入 GameOver page:
Guess.java(局部)
@Property
@Persist(PersistenceConstants.FLASH)
private String message;
@InjectPage
private GameOver gameOver;
第一步完后,我们会看到@Persist注解有了一些变化,其中由名称提供了一个持久化的策略。FLASH是一种内置的在会话中储值的策略,而只用于一个请求……它是为这类反馈消息而特别被设计出来的。如果你在浏览器中敲击键盘上的F5来刷新,page会被渲染而消息会消失。
接下来,再onActionFromMakeGuess()事件处理器方法中我们需要更多的逻辑:
Guess.java(局部)
Object onActionFromMakeGuess(int value)
{
if (value == target)
{
gameOver.setup(target, guessCount);
return gameOver;
}
guessCount++;
message = String.format("Your guess of %d is too %s.", value,
value < target ? "low" : "high");
return null;
}
再一次,非常直接的。如果值是正确的,那么我们会配置好GameOver page并返回它,致使page发生重定向。否则,我们会累加猜测的次数,并格式化输出一条消息展示给用户。
在模板中,我们只需要增加一些标记来展示消息就行了。
Guess.tml(局部)
<strong>Guess number ${guessCount}</strong>
<t:if test="message">
<p>
<strong>${message}</strong>
</p>
</t:if>
这块代码使用了Tapestry的 component。If component会计算器 test 参数,而如果其值被计算出来为true的话,就渲染其正文。被绑定到test的属性不必是一个boolean;Tapestry会将null当做是false,将零当做是false而非零当做是true,它会将空的Collection当做是false……而对于String(比如message),它会将空字符串(那种为null,或者只有一些空格的)当做是false,而非空字符串当做是true。
我们用“GameOver”page来收官了:
GameOver.java
package com.example.tutorial1.pages;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
public class GameOver
{
@Property
@Persist
private int target, guessCount;
void setup(int target, int guessCount)
{
this.target = target;
this.guessCount = guessCount;
}
}
GameOver.tml
<html t:type="layout" title="Game Over"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
xmlns:p="tapestry:parameter">
<p>
You guessed the number
<strong>${target}</strong>
in
<strong>${guessCount}</strong>
guesses.
</p>
</html>
结果是当你猜对了的时候,应该是这样的:
如上这些包含了Tapestry的一些基础知识;我们已经展示了将page链接到一起以及用代码将信息在page之间传递,还有将数据融入URL的基础知识。
这个玩具应用程序还有重构的余地;例如,使其从GameOver page处开始一个新的游戏(并且要以代码不会重复的方式)成为可能。此外,稍后我们会见到其它的在page之间共享信息的方式,比起这里展示的设置并持久化的方法少了些笨重。
接下来:让我们看看Tapestry如何处理HTML表单和用户输入。
接下来是: