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

126
在play1中,我通常在actions中获取所有数据,直接在views中使用它们。由于我们不需要在view中显式声明参数,因此这非常容易。
但是在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的Snippets一样添加一些东西。 - Freewind
5个回答

229

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

然而,在调用网站时确实会增加一些样板。但是您可以减少它(而不失去静态类型的优势)。

在Scala中,我看到有两种方法可以实现:通过操作组合或使用隐式参数。 在Java中,我建议使用Http.Context.args映射存储有用的值,并从模板中检索它们,而无需明确传递为模板参数。

使用隐式参数

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

@(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>

如果您有调用此主模板的模板,则在这些模板中声明 menus 参数为隐式参数,Scala编译器可以自动为您将其传递到 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的actions composition机制来实现。

要做到这一点,您必须定义您的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>

使用Action组合允许您将模板所需的所有隐式值聚合到单个值中,但另一方面,您可能会失去一些灵活性...

使用Http.Context(Java)

由于Java没有类似于Scala的隐式机制,如果您想避免显式传递模板参数,则可以将它们存储在Http.Context对象中,该对象仅存在于请求的持续时间内。该对象包含一个类型为Map<String,Object>args值。

因此,您可以首先编写一个拦截器,如文档所述:

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 动作拦截器混合使用:

静态方法只是一个简洁的方式,用于从当前上下文中检索菜单。然后,在您的控制器上注解以与 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>

1
另外,由于我的项目目前只是用Java编写的,是否有可能采用Action Composition路线,只编写Scala拦截器,而将所有操作都保留在Java中? - Ben McCann
3
在最后的代码块中,你调用了 @for(menu <- Menus.current()) { 但是 Menus 没有被定义(你放置了小写字母的 menusctx.args.put("menus", Menu.find.all());)。这是有原因的吗?比如 Play 模板引擎将其转换成大写或其他什么原因? - Cyril N.
1
@cx42net 已经定义了一个Menus类(Java拦截器)。 @adis 是的,但你可以自由地将它们存储在另一个位置,甚至是缓存中。 - Julien Richard-Foy
我不相信 Http.Context.current() 实际上会起作用。它会抛出一个错误 "There is no HTTP Context available from here",类似于 aviks problem - logan
我并不认为这有任何帮助,我承认类型安全模板的想法是好的,但方便性的代价并不是一个好的妥协。 - zinking
显示剩余11条评论

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的方法。 - Mika
所以当你寻求帮助后,很快就会找到解决方案 :) 控制器方法应该像这样。 public static play.api.templates.Html sidebar() { return (play.api.templates.Html) sidebar.render("message"); } - Mika
1
从视图调用控制器是一个好的实践吗?我不想太过苛求,只是出于真正的好奇。 - 0fnt
此外,你不能以这种方式基于请求来执行操作,比如用户特定的设置。 - 0fnt

14

如果您使用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("",""),最好使用拦截器,但对于简单情况,它可能会奏效。


嗨Stian,麻烦看一下我在答案中的最新修改。我刚刚发现如果你在参数中使用两个相同键的“put”,会产生不良反应。你应该使用...args(key)=value代替。 - guy mograbi

14

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

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

您可以通过标记实现非常漂亮的语法。

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

where get.scala.html is :

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变量。

@(title:String,content:Html)

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

@(title:String)(content: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)
}

编辑:使用我的“set”实现时的副作用

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

您有一个base_template.html,然后您有一个page_template.html,该页面扩展了base_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 docs中的put签名)。

Scala提供了更好的修改地图的方法。

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

它不会引起这种副作用。


在Scala控制器中,我尝试去做play.mvc.Http.Context.current()中没有put方法的操作。我是不是漏掉了什么? - 0fnt
尝试在调用上下文当前后放置“args”。 - guy mograbi

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

AND USE LIKE

@if(isOk) {
   <div>OK</div>
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接