如何避免在play2中到处传递参数?


125

在play1中,我通常会获得动作中的所有数据,并直接在视图中使用它们。由于我们不需要在视图中显式声明参数,因此这非常容易。

但是在play2中,我发现我们必须request在视图的头部声明所有参数(包括),将所有数据放入动作并将它们传递到视图中将非常无聊。

例如,如果需要在首页显示从数据库加载的菜单,则必须在以下位置进行定义main.scala.html

@(title: String, menus: Seq[Menu])(content: Html)    

<html><head><title>@title</title></head>
<body>
    <div>
    @for(menu<-menus) {
       <a href="#">@menu.name</a>
    }
    </div>
    @content
</body></html>

然后,我必须在每个子页面中声明它:

@(menus: Seq[Menu])

@main("SubPage", menus) {
   ...
}

然后,我必须获取菜单并将其传递给每个动作以进行查看:

def index = Action {
   val menus = Menu.findAll()
   Ok(views.html.index(menus))
}

def index2 = Action {
   val menus = Menu.findAll()
   Ok(views.html.index2(menus))
}

def index3 = Action {
   val menus = Menu.findAll()
   Ok(views.html.index(menus3))
}

目前,它只是中的一个参数main.scala.html,如果有很多,该怎么办?

所以最后,我决定Menu.findAll()直接考虑所有因素:

@(title: String)(content: Html)    

<html><head><title>@title</title></head>
<body>
    <div>
    @for(menu<-Menu.findAll()) {
       <a href="#">@menu.name</a>
    }
    </div>
    @content
</body></html>

我不知道这是好还是推荐,对此是否有更好的解决方案?


也许play2应该添加例如Lift的摘要
Freewind

Answers:


229

在我看来,模板是静态类型的事实实际上是一件好事:可以保证,如果模板编译成功,调用模板不会失败。

但是,它确实在调用站点上增加了一些样板。但是您可以减少它(而不会失去静态键入优势)。

在Scala中,我看到了两种实现方法:通过动作组合或使用隐式参数。在Java中,我建议使用Http.Context.args映射存储有用的值并从模板中检索有用的值,而不必显式传递为模板参数。

使用隐式参数

menus参数放在main.scala.html模板参数的末尾,并将其标记为“隐式”:

@(title: String)(content: Html)(implicit menus: Seq[Menu])    

<html>
  <head><title>@title</title></head>
  <body>
    <div>
      @for(menu<-menus) {
        <a href="#">@menu.name</a>
      }
    </div>
    @content
  </body>
</html>

现在,如果您有调用此主模板的模板,则在这些模板中也将它声明为隐式参数的情况下,可以让Scala编译器将menus参数隐式传递给main模板:

@()(implicit menus: Seq[Menu])

@main("SubPage") {
  ...
}

但是,如果要从控制器隐式传递它,则需要提供它作为隐式值,该值可在调用模板的作用域中使用。例如,您可以在控制器中声明以下方法:

implicit val menu: Seq[Menu] = Menu.findAll

然后,您可以在操作中编写以下内容:

def index = Action {
  Ok(views.html.index())
}

def index2 = Action {
  Ok(views.html.index2())
}

您可以在此博客文章此代码示例中找到有关此方法的更多信息。

更新:一篇好的博客贴子展示这种模式也被写在这里

使用动作合成

实际上,将RequestHeader值传递给模板通常很有用(例如,参见本示例)。这不会为您的控制器代码添加太多样板,因为您可以轻松编写接收隐式请求值的操作:

def index = Action { implicit request =>
  Ok(views.html.index()) // The `request` value is implicitly passed by the compiler
}

因此,由于模板通常至少会收到此隐式参数,因此您可以将其替换为包含菜单等更丰富的值。您可以通过使用Play 2 的动作组合机制来做到这一点。

为此,您必须定义您的Context类,并包装基础请求:

case class Context(menus: Seq[Menu], request: Request[AnyContent])
        extends WrappedRequest(request)

然后,您可以定义以下ActionWithMenu方法:

def ActionWithMenu(f: Context => Result) = {
  Action { request =>
    f(Context(Menu.findAll, request))
  }
}

可以这样使用:

def index = ActionWithMenu { implicit context =>
  Ok(views.html.index())
}

您可以将上下文作为模板中的隐式参数。例如main.scala.html

@(title: String)(content: Html)(implicit context: Context)

<html><head><title>@title</title></head>
  <body>
    <div>
      @for(menu <- context.menus) {
        <a href="#">@menu.name</a>
      }
    </div>
    @content
  </body>
</html>

使用动作组合可以使您将模板所需的所有隐式值聚合为一个值,但是另一方面,您可能会失去灵活性。

使用Http.Context(Java)

由于Java没有Scala的隐式机制或类似机制,因此,如果要避免显式传递模板参数,一种可行的方法是将它们存储在Http.Context仅在请求期间有效的对象中。该对象包含argstype 的值Map<String, Object>

因此,您可以按照文档中的说明编写拦截器开始:

public class Menus extends Action.Simple {

    public Result call(Http.Context ctx) throws Throwable {
        ctx.args.put("menus", Menu.find.all());
        return delegate.call(ctx);
    }

    public static List<Menu> current() {
        return (List<Menu>)Http.Context.current().args.get("menus");
    }
}

静态方法只是从当前上下文中检索菜单的快捷方式。然后注释您的控制器以与Menus动作拦截器混合:

@With(Menus.class)
public class Application extends Controller {
    // …
}

最后,menus从模板中检索值,如下所示:

@(title: String)(content: Html)
<html>
  <head><title>@title</title></head>
  <body>
    <div>
      @for(menu <- Menus.current()) {
        <a href="#">@menu.name</a>
      }
    </div>
    @content
  </body>
</html>

您是说菜单而不是菜单?“隐式Val菜单:Seq [Menu] = Menu.findAll”
Ben McCann

1
另外,由于我的项目目前仅用Java编写,是否有可能走动作组合路线,仅用Scala编写我的拦截器,而保留所有用Java编写的动作?
Ben McCann 2012年

“ menu”或“ menus”,没关系:),重要的是类型:Seq [Menu]。我编辑了答案,并添加了Java模式来处理此问题。
朱利安·理查德·福伊

3
在最后一个代码块中,您调用,@for(menu <- Menus.current()) {但是Menus从未定义(放置菜单(小写):)ctx.args.put("menus", Menu.find.all());。有什么理由吗?就像Play可以将其转换为大写字母还是其他?
西里尔N.12年

1
@ cx42net Menus定义了一个类(Java拦截器)。@adis是的,但是您可以将它们存储在另一个地方,甚至可以保存在缓存中。
朱利安·理查德·福伊

19

我这样做的方法是为我的导航/菜单创建一个新的控制器,然后从视图中调用它

因此,您可以定义NavController

object NavController extends Controller {

  private val navList = "Home" :: "About" :: "Contact" :: Nil

  def nav = views.html.nav(navList)

}

nav.scala.html

@(navLinks: Seq[String])

@for(nav <- navLinks) {
  <a href="#">@nav</a>
}

然后在我的主视图中,我可以这样称呼NavController

@(title: String)(content: Html)
<!DOCTYPE html>
<html>
  <head>
    <title>@title</title>
  </head>
  <body>
     @NavController.nav
     @content
  </body>
</html>

NavController在Java中的外观如何?我找不到使控制器返回html的方法。
米卡

因此,碰巧您在寻求帮助后立即找到了解决方案:)控制器方法应如下所示。public static play.api.templates.Html sidebar(){return(play.api.templates.Html)sidebar.render(“ message”); }
Mika

1
从视图调用控制器是一种好习惯吗?我不想成为一个顽固的人,所以出于真正的好奇心问。
0fnt 2014年

此外,基于这样的要求,你不能做的东西,你能不能......,例如用户特定的设置..
0fnt

14

我支持stian的回答。这是获得结果的非常快速的方法。

我只是从Java + Play1.0迁移到Java + Play2.0,模板是迄今为止最难的部分,而我发现实现基本模板(标题,标题等)的最佳方法是使用Http。 。上下文。

使用标记可以实现非常好的语法。

views
  |
  \--- tags
         |
         \------context
                  |
                  \-----get.scala.html
                  \-----set.scala.html

其中get.scala.html是:

@(key:String)
@{play.mvc.Http.Context.current().args.get(key)}

并且set.scala.html是:

@(key:String,value:AnyRef)
@{play.mvc.Http.Context.current().args.put(key,value)}

表示您可以在任何模板中编写以下内容

@import tags._
@context.set("myKey","myValue")
@context.get("myKey")

因此,它非常可读且美观。

这就是我选择的方式。stian-好的建议。证明向下滚动以查看所有答案很重要。:)

传递HTML变量

我还没有想出如何传递HTML变量。

@(标题:字符串,内容:HTML)

但是,我知道如何将它们作为块传递。

@(标题:字符串)(内容:HTML)

因此,您可能希望将set.scala.html替换为

@(key:String)(value:AnyRef)
@{play.mvc.Http.Context.current().args.put(key,value)}

这样,您可以像这样传递Html块

@context.set("head"){ 
     <meta description="something here"/> 
     @callSomeFunction(withParameter)
}

编辑:我的“设置”实现的副作用

在Play中常见的用例it模板继承。

您有一个base_template.html,然后有扩展了base_template.html的page_template.html。

base_template.html可能看起来像

<html> 
    <head>
        <title> @context.get("title")</title>
    </head>
    <body>
       @context.get("body")
    </body>
</html>

而页面模板可能看起来像

@context.set("body){
    some page common context here.. 
    @context.get("body")
}
@base_template()

然后您会看到一个页面(假设为login_page.html)

@context.set("title"){login}
@context.set("body"){
    login stuff..
}

@page_template()

这里要注意的重要一点是您两次设置了“ body”。一次在“ login_page.html”中,然后在“ page_template.html”中。

只要您像我上面建议的那样实现set.scala.html,这似乎就会触发副作用。

@{play.mvc.Http.Context.current().put(key,value)}

因为该页面将两次显示“登录内容...”,因为put返回的值是我们第二次输入相同键时弹出的值。(请参阅Java文档中的put签名)。

Scala提供了一种更好的方式来修改地图

@{play.mvc.Http.Context.current().args(key)=value}

不会引起这种副作用。


在Scala控制器中,我尝试做play.mvc.Htt.Context.current()中没有put方法。我想念什么吗?
0fnt

尝试将args调用上下文后的当前内容置于当前状态。
guy mograbi

13

如果您使用的是Java,并且只想以最简单的方式,而不必编写拦截器并使用@With注释,则还可以直接从模板访问HTTP上下文。

例如,如果您需要模板中可用的变量,则可以使用以下命令将其添加到HTTP上下文中:

Http.Context.current().args.put("menus", menus)

然后,您可以使用以下模板从模板访问它:

@Http.Context.current().args.get("menus").asInstanceOf[List<Menu>]

显然,如果使用Http.Context.current()。args.put(“”,“”)乱扔方法,则最好使用拦截器,但在简单情况下,它可以解决问题。


嗨,斯蒂安,请看一下我在答案中的最后一次编辑。我刚刚发现,如果您使用相同的键在args上使用两次“ put”输入,则会产生讨厌的副作用。您应该改用... args(key)= value。
guy mograbi 2012年

6

从Stian的答案中,我尝试了另一种方法。这对我有用。

用JAVA代码

import play.mvc.Http.Context;
Context.current().args.put("isRegisterDone", isRegisterDone);

在HTML模板头中

@import Http.Context
@isOk = @{ Option(Context.current().args.get("isOk")).getOrElse(false).asInstanceOf[Boolean] } 

和使用

@if(isOk) {
   <div>OK</div>
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.