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" }