Grails WEB层 Web流(Flow)

 

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定义了两个事件:submitreturn。视图负责触发(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)将 :

  1. 在状态转换的时候,会将对象从flash作用域移动到request作用域;
  2. 在渲染以前,将会合并flow和conversation作用域的对象到视图模型中(因此你不需要在视图中引用这些对象的时候,再包含一个作用域前缀了).

 

 

流(Flow)的作用域和序列化

当你将对象放到 flash, flowconversation 作用域中的时候,要确保对象已经实现了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放置于会话范围的。这个范围不同于流范围,因为它允许你横跨整个会话而不只是这个流。同样注意结束状态(每个子流的 moreResultsnoResults在主流中触发事件 :

 

extendedSearch {
         subflow(extendedSearchFlow)   // <--- extended search subflow
         on("moreResults").to "displayMoreResults"
         on("noResults").to "displayNoMoreResults"
}

你可能感兴趣的:(spring,Web,Flash,领域模型,grails)