按照Tim Berners-Lee的原始设想,互联网的核心概念是超链接(hyperlink), 每一个可访问的资源都具有自己的URI(Universal Resource Identifier), 我们通过唯一的url可以访问到这些资源。从概念上说,每一个页面可以由一个两元组[title, url]来进行描述,title是url显示在界面上时的可读表述。在这一描述下,我们可以建立基本的页面浏览模型,包括浏览历史模型:我们可以把浏览过的页面保存在浏览历史列表中,通过选择历史列表中的条目实现页面跳转。但是随着网页的动态性不断增加,页面的资源语义逐渐丧失,url所对应的不再是一种静态的资源表述概念,而逐渐演变成为一种动态的访问函数. 例如
/view.jsp?objectName=MyObj&objectEvent=ViewDetail&id=1
一个url模式所对应的网页个数在理论上可能是无限多的. 从单一数据值到函数是系统复杂性本质上的一种飞跃. 为了不致在这种无限的世界中迷失方向,我们显然需要对于浏览过程进行更加有效的组织,建立更加有约束性的导航模型. 一种常见的导航模式是在页面的上方显示一条线性的导航路径, 与浏览历史模型不同的是, 页面转换时不是
list > view item 1 ==> list > view item 1 > view item 2
而是
list > view item1 ==> list > view item2
为了支持导航路径, 最简单的方式是将页面模型扩展为三元组[id, title, urlExpr], 其中urlExpr是动态表达式, 而id是固定的值. 例如 id=view, urlExpr=/view.jsp?objectName=MyObj&objectEvent=ViewDetail&id=${id}
导航路径的变化规则为navHistory.removeAfter(newPage.id).add(newPage)
为了进一步约化导航路径, 可以将页面模型扩展为
[id, title, urlExpr, group, before, beforeGroup],
其中group定义了页面的分组, 同组的页面在导航路径中相互替代, 而before和beforeGroup定义了页面在导航路径中的相对顺序. 例如对于
[id='list' beforeGroup="detail"] [id='view' group='detail'] [id='update' group='detail']
在页面转换时, 导航路径变化不是
list > view item1 ==> list > view item1 > update item1
而是
list > view item1 ==> list > update item1
在以上的页面模型中, 每一个id对应的是一个不同的页面模板(页面布局), 但是有时我们也需要在同一个页面布局中浏览无限分级的栏目, 此时可以将页面模型扩展为
[id, title, urlExpr, group, before, beforeGroup, path]
将以上的导航模型应用于一般的web应用时还需要克服一个小小的困难: 动态url的副作用. 例如/update.do?id=1&value=2这种具有动作语义的url访问直接破坏了页面的浏览模型,例如现在我们不再能够按F5键刷新页面,不能通过window.location=window.location调用来刷新页面,在页面回退时我们也将遇到种种困难。为了防止重复提交,一种常见的设计模式是分离POST和GET方法,即通过Form POST向上提交数据,处理完毕后通过redirect指令通知浏览器再次发起GET方法得到新的页面.具体做法如下
/redirect_after_action.do?id=1&value=2
在执行完action之后, 服务器调用response.sendRedirect(get_url), 通知前台浏览器使用新的url重新取回页面. 这样最终我们可以确保所有页面都是可以通过url直接访问的(没有隐含的post参数),而且是可以重复访问的(无副作用因而可以反复刷新)!
另一种方式是使用ajax方式提交更新请求,当ajax访问成功后, 在回调函数中进行页面跳转.例如:
new js.Ajax().setObjectName('Test')
.setObjectEvent('Update').addForm(myForm)
.callRemote(function(returnValue){
window.location = '/view.jsp?id=1';
})
这里我们也可以根据returnValue的不同选择跳转到不同页面.
在Witrix平台中, 基于Jsplet框架我们定义了PageFlow对象, 它将可配置的导航模型引入到任意页面的组织过程中. 在跳转到一个新的页面的时候, 访问url如下:
/pageflow.jsp?objectName=MyNav&objectEvent=NavToPage&_pageId=view&id=3
在重新访问历史页面的时候,只需要
/pageflow.jsp?objectName=MyNav&objectEvent=NavToHistoryPage&_pageId=view
基于jsplet框架的对象化特性,MyNav对象在session中保持了flow的所有状态变量,不需要任何框架上特殊的支持。我们可以在任意页面跳出pageflow, 并在任何时刻选择跳回pageflow, 这些动作不会影响到flow对象的状态。而通过objectScope的设置我们可以精细的控制flow对象的生命周期。同时基于对象化设置,访问页面时我们只需要资源的相对名称(relative identifier), 例如对于各种不同的flow, 页面id都可以叫做view, 通过_pageId=view来访问。
Apache软件基金会旗下有一个beehive项目,
http://beehive.apache.org/docs/1.0m1/pageflow/pageflow_overview.html, 其中也定义了所谓pageflow的概念, 这是始创于BEA的一项技术. 但是与Witrix Page Flow不同的是, Beehive Page Flow的核心概念仍然是从struts引申而出的action的概念,所谓的flow是action的连接,而不是通过资源id之间的连接。虽然beeive宣称支持各种对象injection, 并且管理了flow对象的生命周期,但是它并没有规范出this指针这一面向对象最重要的概念之一。Witrix Page Flow关注的重点是已存在的页面之间的有效组织, 它所描述的是某种具有静态资源语义的页面模板之间的关联, 这种关联是较松散的,而不是通过action强关联的. 实际上基于Ajax技术和jsplet框架的消息路由机制, 在每一个页面上都可以进行多种业务操作,甚至更换页面,而不发生pageId的变动, 并不是所有的操作过程都需要在page flow描述中对外暴露。另外一个比较细节化的不同之处是Beehive使用Java Annotation, 而Witrix PageFlow使用xml描述文件, 并实现了pageflow的继承和部分覆盖等高级构造方法. 使用xml描述文件的必要性在于需要复用WebAction, 支持复杂的变换操作, 并集成tpl模板引擎等. Annotation的方式看似简单,但这种简单性同时将系统中的所有组分绑定到了一起, 它也不允许复杂的变换操作的存在. 实际上Beehive的目标并不是真正支持页面编制(包括页面上的页面操作)和页面组织的分离.