在我的主视图模板中,我想显示动态数据(来自数据库),例如网站的导航项。
当我将模型作为参数添加到模板时,需要使用我的主模板的每个视图来提供主模板的模型。因此,每个控制器中的每个操作首先需要获取主模板的导航模型。
这种方法会导致代码重复和违反单一责任原则,因为每个操作都需要知道如何检索主模板模型。有没有办法以隔离的方式提供所描述的功能而无需代码重复,同时保持代码可测试?
示例
以下内容可用于模拟模型和服务类:
package services
import scala.concurrent.Future
case class HeaderItem(title: String, url: String)
case class User(name: String, email: String)
class HeaderItemService {
val all: Future[Seq[HeaderItem]] = Future.successful(HeaderItem("Home", "/") :: Nil)
}
class UserService {
val all: Future[Seq[User]] = Future.successful(User("Test", "test@test") :: Nil)
}
主视图模板显示标题项:
@import services.HeaderItem
@(headerItems: Seq[HeaderItem])(content: Html)
<!DOCTYPE html>
<html lang="en">
<body>
<div id="header">
<ul>
@for(item <- headerItems) {
<li>@item.title</li>
}
</ul>
</div>
@content
</body>
</html>
子视图显示特定数据(用户)的视图,并且必须将主模板特定数据传递给模板:
@import services.HeaderItem
@import services.User
@(headerItems: Seq[HeaderItem], users: Seq[User])
@main(headerItems) {
<ul>
@for(user <- users) {
<li>@user.name</li>
}
</ul>
}
这是控制器必须关心导航项目以及用户:
package controllers
import javax.inject._
import play.api.mvc._
import services.{HeaderItemService, UserService}
import scala.concurrent.ExecutionContext.Implicits.global
@Singleton
class HomeController @Inject()(headerItemService: HeaderItemService, userService: UserService) extends Controller {
def index = Action.async {
for {
headerItems <- headerItemService.all
users <- userService.all
} yield Ok(views.html.index(headerItems, users))
}
}
首次尝试
在ASP MVC中,可以通过使用Html.RenderAction方法(https://msdn.microsoft.com/en-us/library/ee839451(v=vs.100).aspx)在视图内渲染动作来解决问题。据我所知,play框架(2.4)无法实现类似的方法。
答案 0 :(得分:1)
有几种方法可以重新组织代码以减少重复。要记住的是,模板只是从某些指定参数到Html
的函数。考虑到这一点,您可以像这样组织控制器:
@Singleton
class Renderer @Inject() (headerItemService: HeaderItemService) {
// wrap some content html with a layout with a menu
private def renderWithMenu (content: Html): Future[Html] = {
for {
headerItems <- headerItemService.all
} yield views.html.layoutWithMenu(headerItems, content)
}
}
@Singleton
class HomeController @Inject()(userService: UserService, renderer: Renderer) extends Controller with ControllerOps {
def index = Action.async {
for {
users <- userService.all
// views.html.index now only contains the "content" html
rendered <- renderer.renderWithMenu(views.html.index(users))
} yield Ok(rendered)
}
}
虽然此代码仍然负责“触发”菜单的呈现,但获取项目和生成Html
的责任已移至可以重复使用的特征。
关于Action
组合,我认为模板UI的东西有点过分。我通常保留用于身份验证或执行更复杂逻辑的其他代码(自定义请求对象,修改参数,授权等)。
答案 1 :(得分:0)
使用函数合成创建自定义动作。
请注意getHeadersFromDB
是一个db调用,如果用户不必等待太长时间,则应立即返回。优化它或使用一些缓存层。
def withHeadersAction(f: Headers => Request[AnyContent] => Future[Result]) = {
Action.async { req =>
getHeadersFromDB.map { headers =>
f(headers)(req)
}.recover { case th => Ok(s"oops error occurred ${th.getMessage}")}
}
}
如何使用此自定义操作
ApplicationController @Inject() () extends Controller {
def foo = withHeadersAction { implicit headers => req =>
Ok(views.html.something) //headers is implicitly passed to the view
}
}
请注意,implicit
参数可用于明确删除传递的参数
something.scala.html
@(implicit headers: List[Headers])
@main("something") {
//doSomething()
}
其他方式
只在动作中创建内容,并且内部为您提供的所有内容都将由您处理
定义类似views.html.something(headers)(content)
def withHeadersAction(f: Request[AnyContent] => Future[Html]) = {
Action.async { req =>
getHeadersFromDB.flatMap { headers =>
f(req).map { content => Ok(views.html.something(headers)(content)}
}.recover { case th => Ok(s"oops error occurred ${th.getMessage}")}
}
}
ApplicationController @Inject() () extends Controller {
def bar = withHeadersAction { req =>
Future.successful(views.html.someContent())
}
}
答案 2 :(得分:0)
基于pamu的方法,我使用自定义动作构建器制定了类似的方法:
package controllers
import javax.inject._
import play.api.mvc.{BodyParser, _}
import play.twirl.api.Html
import services.{HeaderItemService, UserService}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
class MainAction @Inject()(headerItemService: HeaderItemService) extends Results {
def apply(block: Request[AnyContent] => (Status, Future[Html])) = Action.async { request =>
execute(request, block)
}
def apply[A](bodyParser: BodyParser[A])(block: Request[A] => (Status, Future[Html])) = Action.async(bodyParser) { request =>
execute(request, block)
}
def execute[A](request: Request[A], block: Request[A] => (Status, Future[Html])) = {
val (status, futureContent) = block(request)
for {
content <- futureContent
headerItems <- headerItemService.all
} yield status(views.html.main(headerItems)(content))
}
}
@Singleton
class HomeController @Inject()(mainAction: MainAction, userService: UserService) extends Controller {
def index = mainAction { request =>
val content = userService.all.map(users => views.html.index(users))
(mainAction.Ok, content)
}
}
此方法包括将依赖项注入到负责呈现主视图模板的单独类中,以及从呈现子视图的自定义操作传递状态的可能性。