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:
  • form
    <g:form action="flow">
            <g:submitButton name="event" value="..."/>
         ...
        </g:form>
  • link
    <g:link action="flow" event="event" />
通过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
需要注意的是:
  • 从Action返回的Model,自动进入flow范围
  • 跨状态的Model不要放在request中
  • 状态迁移时,flash中的对象自动进入request
  • 在呈现View State前,flow和conversation范围的对象自动进入view model
  • 保存到flash、flow和conversation中的对象,必须实现java.io.Serializable
  • 对于无法串行化的事物,使用transient。对于domain class,它们是:闭包、GORM事件等。如:
    class Book implements Serializable {
            String title
            transient onLoad = {
                println "I'm loading"
            }
        }
有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()
        }