Spring Web Flow为构建复杂的页面流提供了便利,以简化Web应用开发且以Spring为基础的Grails自然不会错过这一特性。
Spring Web Flow本质上是一种高级的状态机,它跨多个Request,在Flow内保存状态。同时,它并不使用Http Session,而是使用一个序列化的form,在恢复时使用一个通过request parameter传递的execution key。
Grails中的Flow约定如下:
- 正常的Grails Controller
- 每个Flow就是以‘Flow’结尾的Action
- 每个flow状态都在这个action中定义
现在来看看Grails中定义的一个Web Flow:
class BookController {
def index = {
redirect(action:"shoppingCart")
}
def shoppingCartFlow = {
...
}
}
既然Web Flow是一种高级状态机,那么当然就不能不谈它的状态。Web Flow中定义的开始状态和结束状态:
- 开始状态:flow定义中的第一个状态
- 结束状态:没有后续状态的状态。
开始状态只能有一个,而结束状态则不然。看看例子:
def shoppingCartFlow = {
showCart { //开始状态
on("checkout").to "enterPersonalDetails"
on("continueShopping").to "displayCatalogue"
}
…
displayCatalogue { //结束状态1
redirect(controller:"catalogue", action:"show")
}
displayInvoice() //结束状态2
}
前面已经说过,Web Flow是为了解决复杂页面流而存在的,既然是这样,那当然少不了显示页面等待用户输入的环节。这正是View State的作用。在View State中不能定义action(不要误解,这不是指Controller的Action)和redirect,同时页面位置的约定:grails-app/views/controller/flow/viewstate名。当然,你也可以使用render来更改它。以下就是一个View State的例子:
enterPersonalDetails {
//render可省去,使用缺省页面
render(view:"enterDetailsView")
//on("event").to "next state"
on("submit").to "enterShipping"
on("return").to "showCart"
}
要是你从未用过Web Flow,对此可能会感到迷惑。不要紧,在后面我们会对它进行更深入的描述。在此,你只需要把以上的on中的内容当作按钮即可。这样上面的代码就好懂了:点击submit,到enterShipping状态;点击return,到showCart状态;如果不喜欢缺省的页面,使用render来给出view的名字。
除了View State,Web Flow中还有另一种状态:Action State。它并不显示页面,而是执行代码,然后根据结果决定下一步的状态,其中包含action的定义。Action State非常适合用来进行判断或数据准备等动作。
listBooks {
action {
//执行代码在此定义
//model自动放入flow范围
[ bookList:Book.list() ]
}
on("success").to "showCatalogue" //成功时
on(Exception).to "handleError" //失败时
}
同样,我们以后会谈到on的内容。这里只要把on中的值认为是前面action中的返回值即可,而Exception,则可认为是如果前面action抛出异常,则如何如何。
Web Flow中还有另一种特殊的Action:Transition Action。它是在状态迁移之前被执行,特别适合数据绑定和验证。下面的例子就是在一个View State中定义个一个Transition Action:
enterPersonalDetails {
on("submit") {
//执行代码定义于此
log.trace "Going to enter shipping"
}.to "enterShipping"
on("return").to "showCart"
}
看完了状态,接下来看看状态机中的事件,它的作用是触发状态迁移。Web Flow中有2种触发方式:
- 通过View State触发
- 通过Action State触发
View State触发事件的方式主要是通过form和link:
通过Action State触发,调用方法,内置方法包括error()和success():
- 在Transition Action中
enterPersonalDetails {
on("submit") {
def p = new Person(params)
flow.person = p
//出错将导致状态不迁移
if(!p.validate())return error()
}.to "enterShipping"
on("return").to "showCart"
}
- 在Action State中
shippingNeeded {
action {
if(params.shippingRequired) yes()
else no()
}
on("yes").to "enterShipping"
on("no").to "enterPayment"
}
Web Flow的作用域如下:
- request、flash、session,这个是我们非常熟悉的了
- flow:开始状态~结束状态,单条流程而言
- conversation:root flow~sub flow
需要注意的是:
有View,那么自然就少不了数据绑定。这其实和前面的没有什么太大区别,只不过是捕获View State状态转移时提交的数据:
enterPersonalDetails {
on("submit") { //在提交事件Action中
flow.person = new Person(params)
!flow.person.validate() ? error() : success()
}.to "enterShipping"
on("return").to "showCart"
}
同样,你也可以使用Command对象,这样以上例子就成了:
on("submit") { PersonDetailsCommand cmd ->
flow.personDetails = cmd
...
在Web Flow中还可以使用子流程,这为流程的划分和复用提供了手段。子流程本质上还是Web Flow,而且定义也没有什么特别之处。可以这样说,任何一个Web Flow本身就可以作为另一个流程的子流程。所谓主、子关系,不过是调用关系而已。下面是一个主、子流程的例子:
- 主流程
def searchFlow = {
...
extendedSearch {
subflow(extendedSearchFlow) // 触发 subflow
//事件是subflow的结束状态
on("moreResults").to "displayMoreResults"
on("noResults").to "displayNoMoreResults"
}
...
}
- 子流程
def extendedSearchFlow = {
...
//结束状态,触发相应的事件。见前。
moreResults()
noResults()
}