tapestry讨论

09-06-15 fhit

jdon(其实整个国内)上面关于tapestry的讨论很少。即使有讨论也不是专业从事tapestry开发的。本人用tapestry作为自己公司的web开发框架,在2个多月的编码过程中,对tapestry应该说有了比较深入的理解,开一个帖子,希望就tapestry展开一些讨论。

tapestry5.1版本的推出,特别是在模板表达式功能的增强,对于简化代码帮助很大。对于理解能力强的人,可以考虑采用tapestry。

[该贴被fhit于2009-06-15 15:17修改过]

fhit
2009-06-15 15:35

Finally, Tapestry 5 explicitly separates actions (requests that change things) and rendering (requests that render pages) into two separate requests. Performing an action, such as clicking a link or submitting a form, results in a client side redirect to the new page. This is often called "redirect after post".

这是理解tapestry比较关键的一点,tapestry的http请求有两种不同的类型,一种是事件处理,一种是页面请求(就是我们传统的请求,每次请求都有html输出)。

tapestry的事件处理返回的是一个客户端的redirect。

这样做的缺点是多了一个请求,并且服务器一端的性能也有所下降。比如说用户注册,注册之后显示用户资料。当你点击“注册”按钮发送时,向服务器发出的是事件处理请求,服务器响应请求将用户资料保存,然后不是你想象的马上显示这个用户资料,而是向浏览器发送一个重定向,来一个显示请求,所以需要再一次的数据查询,将前一个请求中保存的用户数据再取出来显示。

但是也有优点,你用tapestry的观念处理整个系统时,系统会变得的很有条理。如果适当的运用缓存,会有很好的效果。

下面的连接可以很好的演示这个过程:

http://www.fhplan.com/index:contentclicked/AC0618D0555711DEAA3E6646CA65BCC5

你点了这个连接,就相当于在index页面上发生了一个contentclicked事件,结果是显示这个被点击的内容的详细页面。但是请注意地址栏里面的地址,结果是:http://www.fhplan.com/fhplandetail/AC0618D0555711DEAA3E6646CA65BCC5

也就是说在处理这个contentclicked事件时,我可以有机会准备用哪张页面来显示,怎么来显示,然后来一个显示请求。

[该贴被fhit于2009-06-15 15:41修改过]

[该贴被fhit于2009-06-15 15:42修改过]

fhit
2009-06-15 22:12

tapestry在启动的时候,就为你实例化了一些页面,这些页面放在池中。

当请求到来时就从池里面取出来。这个叫激活,这时onActivate被执行。这个常常用来恢复上下文。比如下面的一个url:
http://w.tcms.com:8080/section/baseindex/6E0E39B04B4B11DE8A615593CA65BCC5
就会激活sectionBaseIndex页面的onActivate(String sectionId).

而onPassivate和onActivate刚刚相反,是在请求结束的时候被执行(这个讲的不准确)。而且它的作用更多的是体现在对页面中的组件的影响。比如:页面带有一个:


String onPassivate(){
return getCurSection().getId();
}
那么页面中的所有actionlink,pagelink等等组件会感染上这个string,像这样子:

http://w.tcms.com:8080/section/baseindex.viewcontent/271308104BEB11DE8A615593CA65BCC5?t:ac=6E0E39B04B4B11DE8A615593CA65BCC5

注意t:ac。


这个是用最好的例子是,web版本的目录管理,因为需要知道当前目录。你可以采用session来保存,但是采用onActivate来做最大的好处是可以使用浏览器的前进后退按钮,让习惯非常自然。



fhit
2009-06-16 09:07

一张tapestry页面是由许多对象组成,包括嵌套的对象。在嵌套的情况下,如果子对象没有处理事件,或者返回的是void,null,false,事件会继续向高层冒泡。请看下面的例子:

http://fhadmin.psalmscn.com/section/baseindex.breadcrumb.cd/4FF00D303DC411DE8171A897CA65BCC5

sectionbaseindex页面里面有面包屑对象,面包屑对象包含cd(actionlink对象)。当cd上面发生事件时,如果breadcrumb没有onActionFromCd这个handler,那么baseindex的onAction(截获所有action),或者onActionFromBreadCrumb(从cd经过breadcrumb冒泡上来的)。

深入理解冒泡机制,并且加以合理的应用,对分散程序代码很有帮助。

[该贴被fhit于2009-06-16 09:07修改过]

fhit
2009-06-26 11:42

tapestry的onactivate方法具有特殊的性质。每次页面被激活的时候,适当的onactivate方法就会被执行。但是初学者如果不仔细研究onactivate的工作原理,会造成代码重复执行的结果。

执行的大致情况是:
1、onactivate的参数小于url传递过来的参数时,才会被执行。
比如:对于下面的链接:http://www.nbnjl.com/cities/479B56902FCC11DEA12BCAD8C0A800C1/dspcontent/0C9E5E30391711DEA40D8215CA65BCC5
cities.java的onactivate(String sectionId,String doWhat,String otherId)就会被执行。
2、在满足上面条件的情况下,按照参数个数递减,onActivate(String sectionId,String doWhat),onActivate(String sectionId),onActivate()依次被执行(如果存在的话)。还有一个例外是:onActivate(Object[] params),这个方法也会在任何情况下被执行,但是这样一来参数类型自动转换的就不起作用了。

那么在代码中如何控制多个onActivate呢?比如对于上面的链接,如果我需要更多的参数来控制页面的表现。http://www.nbnjl.com/cities/479B56902FCC11DEA12BCAD8C0A800C1/dspcontent/0C9E5E30391711DEA40D8215CA65BCC5/1

我就必须添加一个onActivate方法,带4个参数。但是不需要在这个方法里面写于3个参数方法一样的代码,不然的话相当于执行两遍。比较合理的做法是:在4个参数的onActivate方法中将第四个参数保存到page的实例变量中,然后在3个参数的onActivate中根据这个变量做逻辑判断。

fhit
2009-06-27 12:23

因为pagelink产生page的html输出,而actionlink和eventlink一般产生redirect,指向最后输出的page。

这样在结果是本页的情况下,最好采用pagelink,这样一次就解决问题。如果采用actionlink,则会多一个不必要的来回。

当然也不是绝对的。

banq
2009-06-29 09:06

很好的Tapestry资料 赞一个

fhit
2009-06-29 16:39

在我接触tapestry之前,页面输出总是字串(一个html String),常见的代码:

StringBuffer sb = new StringBuffer();
sb.append("xx");
<p class="indent">


但在tapestry,在你编程涉及的范围内,你接触到的是DOM对象。

  boolean beginRender(MarkupWriter writer)
  {
     writer.element("img",
                    "src", src);

     resources.renderInformalParameters(writer);

     writer.end();

     return false;
  }
<p class="indent">


这是你必须适应的转变。下图是一张tapestry组件输出的流程图,在这些阶段中,你可以加入你的代码来控制最终页面的行为。



看起来是非常复杂,确实也非常复杂。不过你不用了解全部,所有的阶段都有默认的行为。

一般来说,setuprender阶段准备一些模板使用的变量。我本人目前使用的还有一个cleanupRender阶段,因为这个阶段已经生成完整的DOM,所以可以将这个内容缓存起来。在前端拦截器使用这个缓存。

[该贴被fhit于2009-06-29 16:39修改过]

fhit
2009-07-02 14:55

要了解tapestry面向组件的特点,最好是来一个例子。本人用perl、python和ruby编写过一些系统,发现java的tapestry好像更组件。

组件知道周围的环境,环境知道组件的情况,就好象是活的一样。以常见的一个导航路径为例(breadcrumb面包屑):首页》论坛》设计模式、框架和架构

下面的代码是组件的java类:

package com.m3958.tcms.components;

import java.util.ArrayList;
import java.util.Collections;

import org.apache.tapestry5.Link;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.SetupRender;

import com.m3958.tcms.base.MyComponentBase;
import com.m3958.tcms.data.SectionNodeVo;
import com.m3958.tcms.utils.MyString;

public class BreadCrumb extends MyComponentBase{

	private int index;
	
	
	private ArrayList<SectionNodeVo> breadCrumb;
	
	public ArrayList<SectionNodeVo> getBreadCrumb() {
		return breadCrumb;
	}

	public void setBreadCrumb(ArrayList<SectionNodeVo> breadCrumb) {
		this.breadCrumb = breadCrumb;
	}

	private SectionNodeVo breadCrumbItem;
	
	
	public boolean isLast(){
		if(breadCrumb.size()  > index + 1){
			return false;
		}else{
			return true;
		}
	}
	
	@Parameter(required=true)
	private SectionNodeVo myCurSection;
	
	@Parameter
	private String returnPage;
	
	
	Object onActionFromCd(String sectionId){
		
		if(getLoginUser().isSuperman() || getLoginUser().isSitemanager()){
			Link link = null;
			if(MyString.isEmpty(returnPage)){
				 link = getPrl().createPageRenderLinkWithContext(getResource().getPage().getClass(), sectionId);
			}else{
				link = getPrl().createPageRenderLinkWithContext(returnPage, sectionId);
			}
			return link;

		}
		
		SectionNodeVo snv = getSitevoByManagingSite().findSectionNodevo(sectionId);
		int depth = 10;
		while(snv != null && depth > 0){
			depth--;
			if(getLoginUser().getSections().contains(snv.getId())){
				Link link = null;
				if(MyString.isEmpty(returnPage)){
					 link = getPrl().createPageRenderLinkWithContext(getResource().getPage().getClass(), sectionId);
				}else{
					link = getPrl().createPageRenderLinkWithContext(returnPage, sectionId);
				}
				return link;

			}
			snv = snv.getParent();
		}
		return null;
	}
	
	Object onActionFromMd(String sectionId){
		
		String sectionType = MyString.splitAndGet(myCurSection.getDtype(), "\\.", -1);
		
		Link link = getPrl().createPageRenderLinkWithContext("section/" + sectionType + "Create", sectionId);
		return link;
	

	}
	
	
	
	@SetupRender
	void initialize(){
		SectionNodeVo svo = getMyCurSection();
		breadCrumb = new ArrayList<SectionNodeVo>();
		while(svo != null){
			breadCrumb.add(svo);
			svo = svo.getParent();
		}
		Collections.reverse(breadCrumb);
	}

	public void setIndex(int index) {
		this.index = index;
	}

	public int getIndex() {
		return index;
	}

	public void setBreadCrumbItem(SectionNodeVo breadCrumbItem) {
		this.breadCrumbItem = breadCrumbItem;
	}

	public SectionNodeVo getBreadCrumbItem() {
		return breadCrumbItem;
	}

	public void setMyCurSection(SectionNodeVo myCurSection) {
		this.myCurSection = myCurSection;
	}

	public SectionNodeVo getMyCurSection() {
		return myCurSection;
	}

}
<p class="indent">


下面的是tml文件:

<div   xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd">
	<t:loop source="breadCrumb" value="breadCrumbItem" index="index">
		<a href="#" t:type="actionlink" t:id="cd" context="breadCrumbItem.id">${breadCrumbItem.name}(${breadCrumbItem.dtypeName})</a>
		<t:unless test="last">
			>> 
		</t:unless>
	</t:loop>
	<a href="#" t:type="actionlink" t:id="md" t:context="MyCurSection.id"><img src="${asset:context:images/newfolder.gif}" alt="新建目录" title="新建目录"/></a><br/>
	
	
</div>

fhit
2009-07-02 15:02

这是使用这个组件的页面的代码:


<span t:type="breadCrumb" t:myCurSection="curSection"></span>


或者(组件的returnPage参数是可选的,这里因为提供了returnPage参数,所以点击面包屑的时候,返回到这个页面,比如在添加文章的时候,你希望点击面包屑时回到index页面,而不是继续停留在本页):

<div t:type="breadCrumb" myCurSection="curSection" returnPage="literal:section/mySectionIndex"></div>



fhit
2009-07-02 16:13

没有接触过tapestry的朋友请特别注意这些参数问题。组件的参数和容器的关系是双向的。

在上面的代码中:

<div t:type="breadCrumb" myCurSection="curSection" returnPage="literal:section/mySectionIndex"></div>


请注意两个参数的写法是不一样的,其中一个有前缀:literal,而另一个没有。其实是省略了,写出来应该是:prop:curSection。

literal表示是字面常量,prop表示这个值是页面对应的java类的一个getCurSection而获得的值。在这种情况下,组件的myCurSection的值来自包含它的容器组件的getCurSection()方法。

而在外围容器组件中,如果组件通过另一个方式写的话,也可以获得组件的myCurSection值,并且可以修改myCurSection的值。

fhit
2009-07-10 15:31

tapestry的form组件,自己产生html form内容,并且在客户端和服务器端建立无缝的链接,当你在处理服务器端处理form事件的时候,一切都已经变成了java世界的对象,你几乎不用再做什么类型转换了。

tapestry提供了内置的html对应的组件,一般情况下,这个已经足够。但是有时候由于时间因素,或者实现困难,你还是想退回到普通的html表单处理。

如果你尝试用tapestry的form组件,然后在里面写上自己的input内容,有时候会出现错误。于是我尝试eventlink,在服务器端生成一个事件处理的callback,然后尝试将普通的html form发送到这个事件处理中去。tapestry并没有抱怨get和post问题,一切似乎可行。

tml文件(注意因为没有在<a/></a>之间写上内容,所以在页面上看不到):

<a href="#" t:type="eventlink" t:event="vote"></a>
<p class="indent">



事件处理代码:

	Object onVote() throws ServiceLocatorException, NamingException, SectionNodeVoNotFoundException{
		JSONObject jso = new JSONObject();
		
		String[] pollitems = getRequest().getParameters("pollitems");
		if(pollitems == null){
			jso.put("msg", "toolittle");
			Link link = getPrl().createPageRenderLink(getResource().getPageName());
			jso.put("next", link.toAbsoluteURI());
			return jso;
		}
		SectionNodeVo snv = getSitevo().findSectionNodeVoWithReadableId(pollName);
		
		minChoice = snv.getMinItem();
		maxChoice = snv.getMaxItem();
		if(maxChoice == 0) maxChoice = 1;
		if(minChoice == 0) minChoice = 1;
		
		if(pollitems.length > maxChoice){
			jso.put("msg", "toomuch");
			Link link = getPrl().createPageRenderLink(getResource().getPageName());
			jso.put("next", link.toAbsoluteURI());
			return jso;
		}
		
		String voteid = MyString.getRandomUUID();
		String ip = rg.getHTTPServletRequest().getRemoteAddr();
		for (int i = 0; i < pollitems.length; i++) {
			Vote v = new Vote();
			v.setTitle(voteid);
			v.setAuthorIp(ip);
			v.setCreatedAt(new Date());
			getContentsLocal().addVote(v, pollitems[i]);
		}
		jso.put("msg", "success");
		if(nextPage != null && (nextPage.startsWith("http://") || nextPage.startsWith("/"))){
			jso.put("next", nextPage);
		}else{
			if(MyString.isEmpty(nextPage)){
				Link link = getPrl().createPageRenderLink(getResource().getPageName());
				jso.put("next", link.toAbsoluteURI());
			}else{
				Link link = getPrl().createPageRenderLink(nextPage);
				jso.put("next", link.toAbsoluteURI());
			}
			
		}
		return jso;
	}
<p class="indent">

fhit
2009-07-10 15:37

现在来看一下我们的投票组件的tml文件。这里的form不是tapestry的form组件。而是手工写成,但是将form发送到前面定义的eventlink处理代码中。


<form action="/${formActionUrl}" method="post" id="pollform">
		<t:loop source="polls" value="poll" index="index">        
        	<input type="checkbox" id="checkb${index}" name="pollitems" value="${poll.id}"/> ${poll.name}<br/>
        </t:loop>
       
        <input type="submit" id="submitpoll"/>

</form>

<script type="text/javascript">

YAHOO.simplepoll.choice_num = 0;
YAHOO.simplepoll.maxChoice = ${maxChoice};
YAHOO.simplepoll.minChoice = ${minChoice};
//
//<!--
//
	var handleSuccess = function(o){
		if(o.responseText !== undefined){
			var x = o.responseText;
			YAHOO.log(o.responseText,"info");
			try{
			 	var result = YAHOO.lang.JSON.parse(o.responseText);
			}catch(e){
				alert(o.responseText,"info");
				return;
			} 
			if(result.msg == "toolittle"){
				alert("您选择太少了!");
			}else if(result.msg = "success"){
				alert("感谢您的参与");
			}else if(result.msg = "toomuch"){
				alert("您选择太多了");
			}
			window.location = result.next;
		}
	};
	
	var handleFailure = function(o){
		alert("与服务器的通讯可能有问题。");
	};

	var callback =
	{
	  success:handleSuccess,
	  failure:handleFailure,
	  argument:['foo','bar']
	};

//var sUrl = "polltest.simplepoll.pollform";




function makeRequest(e){
	YAHOO.util.Event.preventDefault(e);
	var formele = new YAHOO.util.Element('pollform');
	
	var checkboxes = formele.getElementsByTagName('input');

	var i =0;
	YAHOO.simplepoll.choice_num = 0;
	for(i=0;i<checkboxes.length;i++){
		var ele = new YAHOO.util.Element(checkboxes[i]);
 		YAHOO.log(ele.get('type'),"info");
		if(ele.get('type') == "checkbox"){
			if(ele.get('checked')){
				YAHOO.simplepoll.choice_num++;
			}
		}
	}
	YAHOO.log(formele.get('action'),"info");

	if(YAHOO.simplepoll.choice_num < YAHOO.simplepoll.minChoice){
		alert("至少选择" + YAHOO.simplepoll.minChoice + "个");
		return;
	}

	if(YAHOO.simplepoll.choice_num > YAHOO.simplepoll.maxChoice){
		alert("最多选择" + YAHOO.simplepoll.minChoice + "个");
		return;
	}		
	var formObject = document.getElementById('pollform'); 
	YAHOO.util.Connect.setForm(formObject); 
	var request = YAHOO.util.Connect.asyncRequest('POST', formele.get('action'), callback);
}

//
//-->
//
</script>
		
		 
       <script type="text/javascript">
    //
    //<!-- //
    
    function checkboxClickHandler(e) {
    	//get the resolved (non-text node) target:
    	var elTarget = YAHOO.util.Event.getTarget(e);
    	
    	//walk up the DOM tree looking for an <li>
    	//in the target's ancestry; desist when you
    	//reach the container div
    	while (elTarget.id != "pollform") {
    		var yel = new YAHOO.util.Element(elTarget);	
    		YAHOO.log(yel,"info");
    		YAHOO.log(yel.get("type"),"info");
    		//are you an li?
    		if(elTarget.nodeName.toUpperCase() == "INPUT") {
    			//yes, an li: so write out a message to the log
    			if(yel.get("type") === "checkbox"){
        			if(yel.get("checked")){
        				YAHOO.simplepoll.choice_num = YAHOO.simplepoll.choice_num + 1;
        			}else{
        				YAHOO.simplepoll.choice_num = YAHOO.simplepoll.choice_num - 1;
        				if(YAHOO.simplepoll.choice_num < 0)YAHOO.simplepoll.choice_num=0;
        			}
        			YAHOO.log(YAHOO.simplepoll.choice_num,"info");
    			}
    			//and then stop looking:
    			break;
    		} else {
    			//wasn't the container, but wasn't an li; so
    			//let's step up the DOM and keep looking:
    			elTarget = elTarget.parentNode;
    		}
    	}
    }
    //attach clickHandler as a listener for any click on
    //the container div:
   
    //
    // -->
    //
    </script>
<p class="indent">

[该贴被fhit于2009-07-10 15:42修改过]

fhit
2009-09-21 10:40

不知不觉又经过了2个月,还是不停的写代码。随着理解的加深,结果发现上面所写的东西有不少错误。说实话,经过几个月艰苦的编写代码,我总算理解为什么php和asp会这么流行。对于处于小城市的我的公司来说,找到会tapestry人才几乎是幻想。

我用tapestry开发这个玩意 :http://www.blog4job.cn,几乎耗尽了我的精力,实施java系统碰到数不清的问题,虽然最后总有解决方案,但是还是有不少问题,需要请教这里的专家,最好是有实战经验的专家。

好工作网站用到的东西有:
freebsd操作系统、mysql数据库、tapestry5、geronimo、apache httpserver、yui、velocity、ehcache。

wangfei
2009-12-11 14:05

您好!
http://www.blog4job.cn 这个网站还有问题哦。
空值问题,好像是.tml页面中的el表达式比如“article.author.id”...等,引起的。我觉得应该处理下这个空值问题,不至于页面崩溃。