6.5 Web流(Flow)
概述
Grails基于Spring Web Flow项目来支持创建Web流(Flow)。一个Web流(Flow)就是一个会话,它跨越多个请求并保持着流(Flow)作用域的状态。 一个Web流(Flow)也定义了开始和结束状态。 .
Web流(Flow)无需HTTP session,但作为替代,它将状态存储在序列化表单中,然后通过Grails来回传递的request参数中的执行流中的key进行还原。这相比其他使用HttpSession来保存状态的应用来说更具有可扩展性,尤其是在内存和集群方面.
Web流(Flow)本质是高级的状态机,它管理着一个状态到下个状态"流"的执行。因为为你管理着状态,你就勿需担心用户在进入多步骤流(Flow)的操作(action) ,因为Web流(Flow)已经帮你管理了,因此Web流(Flow)在处理象网上购物、宾馆预定及任何多页面的工作流的应用具有出乎意料的简单.
创建流
创建一个流(Flow)只需简单的创建一个普通的Grails控制器(controller),然后添加一个以规约Flow结尾的操作。例如:
class BookController {
def index = {
redirect(action:"shoppingCart")
}
def shoppingCartFlow = {
…
}
}
注意,当重定向或引用流(Flow)时,可以把它当做一个操作(action)而省略掉流(Flow)前缀。换句话说,上面流的操作(action)名为shoppingCart.
6.5.1 开始与结束状态
如上所述,一个流(Flow)定义了开始和结束状态。一个开始状态是当用户第一次开始一个会话(或流(Flow))。Grails的开始流(Flow)是第一个带有代码块的方法调用。例如:
class BookController {
…
def shoppingCartFlow = {
showCart {
on("checkout").to "enterPersonalDetails"
on("continueShopping").to "displayCatalogue"
}
…
displayCatalogue {
redirect(controller:"catalogue", action:"show")
}
displayInvoice()
}
}
这里,showCart节点是这个流的开始状态。因为这个showCart状态并没有定义一个操作(action)或重定向,只被视为是一个视图状态。 通过规约,指向grails-app/views/book/shoppingCart/showCart.gsp视图 .
注意,这不像正规的控制器(controller)操作(action),这个视图被存储于与其流名字匹配的grails-app/views/book/shoppingCart目录中 .
shoppingCart流(Flow)也可能拥有两个结束状态。第一个是displayCatalogue,执行外部重定向到另一个控制器(controller)和操作(action),从而结束流(Flow)。第二个是displayInvoice是一个最终状态,因为它根本没有任何事件,只是简单的渲染一个名为grails-app/views/book/shoppingCart/displayInvoice.gsp的视图,并在同一时间终止流(Flow).
一旦一个流(Flow)结束,它只能从开始状态重新开始,对于showCart不会来自任何其他状态.
6.5.2 操作(Action)状态和视图状态
视图状态
视图状态没有定义操作(action)或redirect。下面是一个视图状态示例:
enterPersonalDetails {
on("submit").to "enterShipping"
on("return").to "showCart"
}
它默认查找一个名为grails-app/views/book/shoppingCart/enterPersonalDetails.gsp的视图。注意,enterPersonalDetails定义了两个事件:submit和return。视图负责触发(triggering)这些事件。假如你想让视图用于渲染,使用render方法来完成:
enterPersonalDetails {
render(view:"enterDetailsView")
on("submit").to "enterShipping"
on("return").to "showCart"
}
现在,它将查找grails-app/views/book/shoppingCart/enterDetailsView.gsp。假如使用共享视图,视图参数以/ 开头:
enterPersonalDetails {
render(view:"/shared/enterDetailsView")
on("submit").to "enterShipping"
on("return").to "showCart"
}
现在,它将查找 grails-app/views/shared/enterDetailsView.gsp
操作(Action)状态
操作(Action)状态只执行代码但不渲染任何视图。操作(Action)的结果被用于控制流(Flow)的切换。为了创建一个操作操作(Action)状态,你需要定义一个被用于执行的操作。 这通过调用action方法实现并传递它的一个代码块来执行:
listBooks {
action {
[ bookList:Book.list() ]
}
on("success").to "showCatalogue"
on(Exception).to "handleError"
}
正如你看到的,一个操作看上去非常类似于一个控制器(controller)操作(action),实际上,假如你需要可以重用控制器(controller)操作(action)。假如这个操作没有错误成功返回,success事件将被触发。在这里,返回一个map,它被视为"model"看待,并自动放置于流(flow)作用域.
此外,在上面的示例中也使用了下面的异常处理程序来处理错误:
on(Exception).to "handleError"
这使当流(Flow)切换到状态出现异常的情况下调用handleError.
你可以编写与流(flow)请求上下文相互作用更复杂的操作(action):
processPurchaseOrder {
action {
def a = flow.address
def p = flow.person
def pd = flow.paymentDetails
def cartItems = flow.cartItems
flow.clear()
def o = new Order(person:p, shippingAddress:a, paymentDetails:pd) o.invoiceNumber = new Random().nextInt(9999999) cartItems.each { o.addToItems(it) } o.save() [order:o] } on("error").to "confirmPurchase" on(Exception).to "confirmPurchase" on("success").to "displayInvoice" }
这是一个更复杂的操作(action),用于收集所有来自流(flow)作用域信息,并创建一个Order对象。然后,把Order作为模型返回。这里值得注意的重要事情是与请求上下文和 "流(flow)作用域"的相互作用.
切换操作
另一种形式的操作(action)被称之为切换操作(action)。一旦一个event被触发,切换操作优先于状态切换被直接执行。普通的切换操作如下 :
enterPersonalDetails {
on("submit") {
log.trace "Going to enter shipping"
}.to "enterShipping"
on("return").to "showCart"
}
注意,我们是怎样传递一个代码块给submit事件,它只是简单的记录这个切换。切换状态对于数据绑定与验证是非常有用的,将在后面部分涵盖.
6.5.3 流(Flow)执行事件
为了执行流流从一个状态到下一个状态的 切换 ,你需要一些方法来触发一个 event ,指出流流下一步该做什么。事件的触发可以来自于任何视图状态和操作状态.
来自于一个视图状态的触发事件
正如之前所讨论的,在早前代码列表内流的开始状态可能处理两个事件。一个checkout和一个continueShopping事件:
def shoppingCartFlow = {
showCart {
on("checkout").to "enterPersonalDetails"
on("continueShopping").to "displayCatalogue"
}
…
}
因为showCart事件是一个视图状态,它会渲染 grails-app/book/shoppingCart/showCart.gsp视图. 在视图内部,你需要拥有一个用于触发流(Flow)执行的组件.在一个表单中,这可使用submitButton标签:
<g:form action="shoppingCart"> <g:submitButton name="continueShopping" value="Continue Shopping"></g:submitButton> <g:submitButton name="checkout" value="Checkout"></g:submitButton> </g:form>
这个表格必须提交返回shoppingCart流流。每个submitButton标签的name属性标示哪个事件将被触发。假如,你没有表格,你同样可以用link标签来触发一个事件,如下:
<g:link action="shoppingCart" event="checkout" />
来自于一个操作(Action)的触发事件
为了触发来自于一个操作(action)的一个事件,你需要调用一个方法。例如,这里内置的error()和success()方法。下面的示例在切换操作中验证失败后触发error()事件:
enterPersonalDetails {
on("submit") {
def p = new Person(params)
flow.person = p
if(!p.validate())return error()
}.to "enterShipping"
on("return").to "showCart"
}
在这种情况下,因为错误,切换操作将使流回到enterPersonalDetails状态.
有了一种操作状态,你也能触发事件来重定向流:
shippingNeeded {
action {
if(params.shippingRequired) yes()
else no()
}
on("yes").to "enterShipping"
on("no").to "enterPayment"
}
6.5.4 流(Flow)的作用域
作用域基础
在以前的示例中,你可能会注意到我们在“流作用域(flow scope)”中已经使用了一个特殊的流(flow)来存储对象,在Grails中共有5种不同的作用域可供你使用 :
-
request- 仅在当前的请求中存储对象 -
flash- 仅在当前和下一请求中存储对象 -
flow- 在工作流中存储对象,当流到达结束状态,移出这些对象 -
conversation- 在会谈(conversation)中存储对象,包括根工作流和其下的子工作流 -
session- 在用户会话(session)中存储对象
Grails的service类可以自动的定位web flow的作用域,详细请参考 Services .
此外从一个action中返回的模型映射(model map)将会自动设置成flow范围,比如在一个转换(transition)的操作中,你可以象下面这样使用流(flow)作用域 :
enterPersonalDetails {
on("submit") {
[person:new Person(params)]
}.to "enterShipping"
on("return").to "showCart"
}
要知道每一个状态总是创建一个新的请求,因此保存在request作用域中的对象在其随后的视图状态中不再有效,要想在状态之间传递对象,需要使用除了request之外的其他作用域。此外还有注意,Web流(Flow)将 :
- 在状态转换的时候,会将对象从flash作用域移动到request作用域;
- 在渲染以前,将会合并flow和conversation作用域的对象到视图模型中(因此你不需要在视图中引用这些对象的时候,再包含一个作用域前缀了).
流(Flow)的作用域和序列化
当你将对象放到 flash, flow 或conversation 作用域中的时候,要确保对象已经实现了java.io.Serializable接口,否则将会报错。 这在domain类尤为显著,因为领域类通常在视图中渲染的时候被放到相应的作用域中,比如下面的领域类示例 :
class Book {
String title
}
为了能够让Book类的实例可以放到流(flow)作用域中,你需要修改如下:
class Book implements Serializable { String title }
这也会影响到领域类中的关联和闭包,看下面示例:
class Book implements Serializable { String title Author author }
此处如果Author关联没有实现Serializable,你同样也会得到一个错误。此外在GORM events中使用的闭包比如onLoad, onSave等也会受到影响,下例的领域类如果放到flow作用域中,将会产生一个错误:
class Book implements Serializable { String title def onLoad = { println "I'm loading" } }
这是因为onLoad事件中的代码块必能被序列化,要想避免这种错误,需要将所有的事件声明为transient :
class Book implements Serializable { String title transient onLoad = { println "I'm loading" } }
6.5.5 数据绑定和验证
在 开始和结束状态 部分, 开始状态的第一个示例触发一个切换到 enterPersonalDetails 状态。这个状态渲染一个视图,并等待用户键入请求信息 :
enterPersonalDetails {
on("submit").to "enterShipping"
on("return").to "showCart"
}
一个视图包含一个带有两个提交按钮的表格,每个都触发提交事件或返回事件:
<g:form action="shoppingCart"> <!-- Other fields --> <g:submitButton name="submit" value="Continue"></g:submitButton> <g:submitButton name="return" value="Back"></g:submitButton> </g:form>
然而,怎么样捕捉被表格提交的信息?为了捕捉表格信息我们可以使用流切换操作:
enterPersonalDetails {
on("submit") {
flow.person = new Person(params)
!flow.person.validate() ? error() : success()
}.to "enterShipping"
on("return").to "showCart"
}
注意,我们是怎样执行来自请求参数的绑定,把Person实体放置于流(flow)作用域中。同样有趣的是,我们执行 验证,并在验证失败是调用error()方法 .这个流(flow)的动机即停止切换并返回 enterPersonalDetails 视图,因此,有效的项通过user进入,否则,切换继续并转到enterShipping state.
就像正规操作(action),流(flow)操作(action)也支持 命令对象概念,通过定义闭包的第一个参数 :
enterPersonalDetails {
on("submit") { PersonDetailsCommand cmd ->
flow.personDetails = cmd
!flow.personDetails.validate() ? error() : success()
}.to "enterShipping"
on("return").to "showCart"
}
6.5.6 子流程和会话
Grails的Web Flow集成同样支持子流(subflows)。一个子流在一个流中就像一个流。拿下面search流作为示例:
def searchFlow = {
displaySearchForm {
on("submit").to "executeSearch"
}
executeSearch {
action {
[results:searchService.executeSearch(params.q)]
}
on("success").to "displayResults"
on("error").to "displaySearchForm"
}
displayResults {
on("searchDeeper").to "extendedSearch"
on("searchAgain").to "displaySearchForm"
}
extendedSearch {
subflow(extendedSearchFlow) // <--- extended search subflow
on("moreResults").to "displayMoreResults"
on("noResults").to "displayNoMoreResults"
}
displayMoreResults()
displayNoMoreResults()
}
它在extendedSearch状态中引用了一个子流。子流完全是另一个流 :
def extendedSearchFlow = {
startExtendedSearch {
on("findMore").to "searchMore"
on("searchAgain").to "noResults"
}
searchMore {
action {
def results = searchService.deepSearch(ctx.conversation.query)
if(!results)return error()
conversation.extendedResults = results
}
on("success").to "moreResults"
on("error").to "noResults"
}
moreResults()
noResults()
}
注意,它是怎样把extendedResults放置于会话范围的。这个范围不同于流范围,因为它允许你横跨整个会话而不只是这个流。同样注意结束状态(每个子流的 moreResults 或 noResults在主流中触发事件 :
extendedSearch {
subflow(extendedSearchFlow) // <--- extended search subflow
on("moreResults").to "displayMoreResults"
on("noResults").to "displayNoMoreResults"
}
本文介绍 Grails 中的 Web Flow 功能,包括流的创建、状态管理、数据绑定验证等核心机制。同时探讨了作用域、子流程等高级特性。
5132

被折叠的 条评论
为什么被折叠?



