如何在每个控制器中不重复代码的情况下将模型传递给我的主视图模板?
Posted
技术标签:
【中文标题】如何在每个控制器中不重复代码的情况下将模型传递给我的主视图模板?【英文标题】:How can I pass a model to my main view template without code duplication in every controller? 【发布时间】:2016-09-19 15:01:37 【问题描述】:在我的主视图模板中,我想显示动态数据(来自数据库),例如网站的导航项。
当我将模型作为参数添加到模板时,每个使用我的主模板的视图都需要为主模板提供模型。因此,每个控制器中的每个动作首先需要获取主模板的导航模型。
这种方法会导致代码重复和违反单一职责原则,因为每个动作都需要知道如何检索主模板模型。有没有办法以隔离的方式提供所描述的功能而无需重复代码,同时保持代码可测试?
示例
以下可用于模拟模型和服务类:
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) 无法使用类似的方法。
【问题讨论】:
您可以在主模板中使用 javascript 从单独的端点获取标题项 @Lukasz:使用 javascript,我确实可以构建一个类似于 ASP RenderAction-approach 的解决方案,但它也会带来新的依赖关系并使视图的可读性降低。我更喜欢仅基于 Scala 的解决方案。 【参考方案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
的责任已转移到可以重复使用的 trait。
关于Action
的组合,我认为这对于模板 UI 的东西来说有点矫枉过正。我通常将其保留用于身份验证或执行更复杂逻辑的其他代码(自定义请求对象、修改参数、授权等)。
【讨论】:
Alvaro,感谢您的回复,这正是我想要的,因为它将加载标题的代码与所有操作分开。与使用动作组合的方法相比,我更喜欢这种解决方案,因为我不必通过视图传递标题。尽管如此,最后一件事困扰着我:我仍然需要将 HeaderItemService 注入任何将使用 ControllerOps 特征的控制器。有没有办法通过将依赖移出控制器来减少耦合?我想我可以让 trait 成为一个抽象类,但我更喜欢另一种方式。 @Felix 我更新了代码。不确定这是否是您所指的抽象类。如果是这样,它有什么问题?归根结底,index
方法需要能够呈现“完整”页面。依靠另一个组件来寻求帮助是有意义的。【参考方案2】:
使用函数组合创建自定义操作。
请注意,getHeadersFromDB
是一个数据库调用,如果用户不必等待太久,它应该立即返回。优化它或使用一些缓存层。
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()
其他方式
只在动作中创建内容,一切都将由 withHeadersAction 在内部为您处理
定义这样的视图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())
【讨论】:
这种方法会将加载头文件的代码封装到一个单独的函数中,这很好。但是,我仍然需要将标题项传递到使用主视图的任何视图中,因此我更喜欢使用 Alvaro 提出的特征的方法。 @Felix .. 你可以使用implicit
参数来明确地摆脱传递的标题。
谢谢pamu,这确实可以按要求工作。我仍然更喜欢单独渲染主模板的方法(参见 Alvaro 的回答),因为它更好地隐藏了主类的参数。想象一下,我必须更改主视图模型的类型或添加一个,使用隐式的解决方案我需要更改每个操作。当将主模板的渲染封装到一个单独的函数中时,我可以传入任何我需要的东西。
看起来不错!是否也可以将状态从控制器传递给动作?此外,还需要以某种方式将 HeaderItemService 注入到 withHeadersAction 中。例如,通过将 withHeadersAction 包装在 MainAction 类中,该类将服务作为构造函数依赖项。然后可以向控制器注入 MainAction,其中 withHeadersAction 可以重命名为 apply 以省略方法名称。【参考方案3】:
基于 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)
这种方法包括将依赖项注入到负责呈现主视图模板的单独类中,以及从呈现子视图的自定义操作中传递状态的可能性。
【讨论】:
以上是关于如何在每个控制器中不重复代码的情况下将模型传递给我的主视图模板?的主要内容,如果未能解决你的问题,请参考以下文章
如何在不覆盖 TTY 的情况下将密码传递给 su/sudo/ssh?
如何在没有新服务器请求的情况下将下载的图像从父路由传递到子路由