grails2 跨域_Grails设计注意事项2 –偶尔将视图模型投入– Ted Vinke的博客

grails2 跨域

这是我对如何设计特定用户界面以使其可重用,可测试以及整体软件更易于维护的看法。 是的,使用了MVVM模式中的一些视图模型。

背景

最近,我们开始与两个团队合作开发新的Grails应用程序。

作为其中的一部分,我回顾了团队早期应用程序的一些代码,以得出一些通用的编码标准和约定,两个团队都将遵循此新应用程序 在这些代码审查会议中,我还将就一些我希望使用的体系结构选择和设计风格提供建议。

用例:任务

新应用程序的主页显示已登录的用户,按月分组任务概述以及一些不相关的滑块。

例如这样的事情:

任务和东西

任务和东西

我们确实已经创建了一个具有标题,内容块和页脚的良好主布局(grails-app / views / layouts / main.gsp)。 在sprint的前几天,当最初的设计和资产也放入Grails应用程序中时,一个叫John的团队成员正在实施一个概述。

为此,John创建了一个控制器(HomeController)和GSP(home / index.gsp)。页眉和页脚已提取到主布局中,因此初始为空的GSP…

<%@ page contentType="text/html;charset=UTF-8"%>
<html>
 <head>
  <meta name="layout" content="main" />
  <title>Tasks</title>
 </head>
 <body>
 </body>
</html>

…很快开始填充非常复杂HTML。 尽管上面的抽象屏幕快照告诉您什么,但是实际任务概述包含了相当多的SVG图像- 占用了大量空间

超过1000行HTML的小片段:Hello SVG

超过1000行HTML的小片段:Hello SVG

任务概述结束后,将出现一些滑块。

如果您曾经从设计机构那里获得过静态HTML,那么您已经经历过了,但是在某些时候,您必须使内容适用于实际内容,而不是常规的lorem ipsum

任务概述显示了当年每月(一月,二月,比赛等)分组的任务。 但是,视图不应该总是从一月开始,而应该在当前月份开始-因此,最紧迫的任务放在首位。 至少目前是这样。

John的第二个步骤是将硬编码的值从HTML移到应用程序本身,并将其提供给GSP的Grails方式。 通过模型中的控制器。

HomeController看起来像这样:

class HomeController {

  def securityService
  def taskService

  def index() {
    int showPercentage = 3

    Map<String, List> tasks = retrieveTasks()
    Map<String, Integer> notifications = retrieveNotifications()

    def nowDate = new Date()
    def currentMonth = nowDate[Calendar.MONTH] + 1

    return [
      username: securityService.user.name,
      tasks : tasks,
      showPercentage: showPercentage,
      months : getMonths(),
      startMonth : currentMonth,
      notifications : notifications
    ]
  }

  private Map<String, List> retrieveTasks() {
    taskService.retrieveTasks()
  }

  private Map<String, Integer> retrieveNotifications() {
    taskService.retrieveNotifications()
  }

  List<String> getMonths() {
    return [
      'January',
      'February',
      'March',
      'April',
      'May',
      'June',
      'July',
      'August',
      'September',
      'October',
      'November',
      'December'
    ]
  }
}

该模型具有用户名(显示“ Hi John”)和底部的任务概览和滑块所需的各种变量( tasksshowPercentagemonthsstartMonthnotifications )。

因此,index.gsp已更新为从模型中获取值。

<body>
  <div class="container">
    <div class="pat-well">
      <div class="row month-${startMonth}" id="task-browser">
        <div class="six columns">
        <g:if test="${notifications}">
        ...

因此,这是初始版本,并且可以正常工作。

模型-视图-视图模型

在查看控制器的代码以及模型中的最终内容时, 无法轻易分辨出什么是相关的,什么不是相关的。

return [ 
      username: securityService.user.name,
      tasks : tasks,
      showPercentage: showPercentage,
      months : getMonths(),
      startMonth : currentMonth,
      notifications : notifications
    ]

同样,变量按照看似随机的顺序放入模型中-可能是按照创建顺序,GSP或其他方式所需的顺序。 对于用户界面的各个部分,这些批量模型并不罕见-只是“仅”返回了6件事。

也许我们可以清理一下。

第1部分–视图模型

让我们介绍一个视图模型,一个普通的Groovy对象(PO​​GO),仅用于保存视图“组件”的相关数据。 嘿,这是我第一次听到您提到组件 。 是的,这是那些重载的术语之一,可以表示任何含义。 幸运的是,因为我们还可以为任何事物创建POGO ;-)

在HomeController.groovy的底部创建一个新类 ,并根据您的UI小部件或组件表示的名称对其进行命名。 直到现在,我一直在说“任务概述”,并且可以调用新类: TasksOverview

在这种情况下,我们从外部网页设计机构获取布局,他们通常还会考虑命名对象,以用作CSS选择器或JavaScript对象。 资料来源显示:

<div class="row.." id="task-browser">

任务浏览器!

特别是在使用大量UI元素的项目中,在设计和开发团队之间尽早命名事物很重要。 您只是不能不说在右上角有沙漏

TaskBrowser类也可以是一个单独的TaskBrowser.groovysrc文件夹中,但现在它只是在主页所以只是把它的下面使用HomeControllerHomeController.groovy会为现在要做的。

...
}

/**
 * Task browser.
 */
class TaskBrowser {
 
}

将保存数据的每个关联属性从控制器的index()操作移动到此新类,例如tasksnotifications集合。

class TaskBrowser {
  Map<String, List> tasks = [:]
  Map<String, List> notifications = [:]
}

在操作本身中或通过帮助器方法创建TaskBrowser的实例,例如

private TaskBrowser createTaskBrowser() {
  return new TaskBrowser(
    tasks: taskService.retrieveTasks(), 
    notifications: taskService.retrieveNotifications()
  )
}

我们仍然需要一些日期/时间处理,以建立月份列表并确定任务浏览器的开始月份。

即使此类似乎只是一堆相关数据, 也请不要忘记正确的OO原则,并尽可能将责任转移到组件类上,例如

class TaskBrowser {
  Map<String, List> tasks = [:]
  Map<String, List> notifications = [:]

  int getStartMonth() {
    def nowDate = new Date()
    nowDate[Calendar.MONTH] + 1
  }  

  List<String> getMonths() {
    return [
      'January',
      'February',
      'March',
      'April',
      'May',
      'June',
      'July',
      'August',
      'September',
      'October',
      'November',
      'December'
    ]
  }
}

控制器动作现在可以在模型中返回TaskBrowser ,因此,不仅将其缩减为“仅三样”,而且更清楚了相关的内容。

def index() { 
  int showPercentage = 3
  return [ 
    username: securityService.user.name,
    showPercentage: showPercentage,
    taskBrowser: createTaskBrowser()
  ]
}

private TaskBrowser createTaskBrowser() {
  ...
}
第2部分–视图

我们会的,那很好,但是我们还没有到那儿:来自TaskBrowser东西仍然在home / index.gsp的各个部分中使用–基本上仍然需要在整个地方进行更改。

请记住,我们的index.gsp是将常规HTML混入特定组件(例如任务浏览器)的混合物:

<body>
  <div class="container">
    <div class="content">
      <div class="row month-${startMonth}" id="task-browser">
        <div class="six columns">
        <g:if test="${notifications}">
        ...

在我们当前的设置中,至关重要的是

  • 在任务浏览器上工作的人,使其与系统中的真实数据一起工作
  • 谁在Web浏览器中进行调整就在任务浏览器中实现UI更改的人
  • 从事主页其他功能的人

不要冲突太多

这意味着,如果我们可以避免的话,它们在应用程序的同一行代码上不应做太多工作。 而且我们可以。

模板

人们最容易想到的就是从GSP中提取大量HTML到单独的代码段或模板中

从index.gsp中提取HTML到较小的GSP中,例如_taskBrowser.gsp ,最终可以像<g:render>一样包含在index.gsp中。

<body>
  <div class="container">
    <div class="content">
      <g:render template="taskBrowser" />

这样可以在模板中呈现美观且干净利落HTML,例如

<div class="row month-${startMonth}" id="task-browser">
  <div class="six columns">
    <g:if test="${notifications}">

看起来还可以,直到您意识到仍然必须将模型传递给模板,然后模板才能访问所有需要的变量。 在那之前,不是很有用。

<body>
  <div class="container">
    <div class="content">
      <g:render template="taskBrowser" model="[
          tasks : taskBrowser.tasks,
          months : taskBrowser.months,
          startMonth : taskBrowser.startMonth,
          notifications : taskBrowser.notifications
        ]" />

有点冗长。 较短的版本将使用bean属性。

<g:render template="taskBrowser" bean="${taskBrowser}" />

在这种情况下,我们不能从访问属性taskBrowser直到我们改变模板从隐性得到的一切it

<div class="row month-${it.startMonth}" id="task-browser">
  <div class="six columns">
    <g:if test="${it.notifications}">

这是一个折衷,但是我们只需要两件事就可以使index.gsp的作者的工作变得简单一些: 模板名称数据

仍包含的模板名称仍可以视为实现细节。 重命名或完全替换它涉及更新所有GSP,这些GSP可能会按名称呈现此特定模板,因此-如果您有更多这样的模板-可能会成为维护负担。

让我们看看是否可以通过…做得更好。

标签库

Grails应用程序的View层不仅是grails-app / views-directory中的Groovy服务器页面(GSP),而且还包括Grails的标记库机制。 标记库是模型视图控制器模式(MVC)中的一种“视图助手”。

在grails-app / taglib中创建一个名为LayoutTagLib的标签库,该标签库有助于将“布局”内容放置在页面上。 非常通用的名称,但是如果名称太大,则可以将其拆分为更多特定的选项卡库。

/**
 * Layout tags.
 */
class LayoutTagLib {
  static defaultEncodeAs = [taglib:'html']

}

实现一个名为taskBrowser标记 ,该标记接受单个属性taskBrowser这是必需的。 这个不需要任何身体。

/**
 * Layout tags.
 */
class LayoutTagLib {
  static defaultEncodeAs = [taglib:'html']

  /**
   * Renders the task browser.
   * 
   * @attr taskBrowser REQUIRED a task browser instance
   */
   def taskBrowser = { attrs ->
     if (!attrs.taskBrowser) {
       throwTagError("Tag [taskBrowser] is missing 
                           required attribute [taskBrowser]")
     }
 
  }
}

throwTagError将引发GrailsTagException说明如果标记的用户忘记传递必需的属性,则会缺少什么。 “ @attr必需”部分用于IDE支持和/或文档。

渲染较早的模板,为其提供正确的模型。 由于_taskBrowser.gsp现在成为该标签库的责任,因此将其移动到新的,更中性的位置,例如grails-app / views / layouts / components。

/**
 * Layout tags.
 */
class LayoutTagLib {
  static defaultEncodeAs = [taglib:'none']

  /**
   * Renders the task browser.
   * 
   * @attr taskBrowser REQUIRED a task browser instance
   */
   def taskBrowser = { attrs ->
     if (!attrs.taskBrowser) {
       throwTagError("Tag [taskBrowser] is missing " +
                       "required attribute [taskBrowser]")
     }
 
     TaskBrowser browser = attrs.taskBrowser
     out << render(template: '/layouts/components/taskBrowser', 
       model: [tasks : browser.tasks,
         months : browser.months,
         startMonth : browser.startMonth,
         notifications : browser.notifications
       ])
  }
}

defaultEncodeAs = [taglib:'html'][taglib:'none'] –以防止我们对呈现的GSP代码段进行HTML编码。

Jip,我们仍然将每个单独的部分传递给模型,但是如果您愿意,当然也可以提供bean变体。

索引页面现在变为:

<body>
  <div class="container">
    <div class="content">
      <g:taskBrowser taskBrowser="${taskBrowser}"/>

可以将TaskBrowser视为taskBrowser标记的输入 。 由于我们现在处于标签库世界中,因此我们可能会基于此输入执行一些其他逻辑或处理(例如,首先过滤要由特定任务类型显示的任务),然后将处理后的数据进一步传递给模型中的模板。

如果此类附加输入(例如,用于过滤器的输入)是可选的,并且取决于应用程序中使用标记的位置,则可以将其接受为可选属性,而不是将其放置在TaskBrowser对象中。

例如,额外的filter属性可以像

/**
 * Renders the task browser.
 * 
 * @attr taskBrowser REQUIRED a task browser instance
 * @attr filter Optionally a {@link Task.Type} to show only those tasks
 */
def taskBrowser = { attrs ->
  if (!attrs.taskBrowser) {
    throwTagError("Tag [taskBrowser] is missing " +
                    "required attribute [taskBrowser]")
  }
 
  TaskBrowser browser = attrs.taskBrowser
   
  // filter tasks by type
  def tasks = browser.tasks
  if (attrs.filter && attrs.filter instanceof Task.Type) {
    tasks = browser.tasks.findAll { task -> task.type == attrs.filter }
  } 

  out << render(template: '/layouts/components/taskBrowser', 
    model: [tasks : tasks,
      months : browser.months,
      startMonth : browser.startMonth,
      notifications : browser.notifications
  ])
}

页面可以忽略该属性:

<g:taskBrowser taskBrowser="${taskBrowser}" />

另一个页面可以使用过滤器:

<g:taskBrowser taskBrowser="${taskBrowser}" filter="${TaskType.PERSONAL}" />

同样,这只是一种方式。 当然,另一种有效的方法是,如果不同的控制器出于不同的目的创建不同的实例,则将此类内容(例如,过滤器选项)仍放置在原始视图模型( TaskBrowser类)中。 一如既往,最有效的方法取决于用例。

Grails任务浏览器

瞧!

是否应该为所有内容创建特定的视图模型?

当然不是。 以滑块为例。

Grails-Slider

如果只需要一些属性,例如下面的value ...

<g:slider value="${showPercentage}" />

showPercentage包装在特殊对象中或从特殊对象中获取showPercentage似乎过于showPercentage 。 只需将这些值直接传递给GSP。

class HomeController {

  def index() { 
    ...

    return [..., showPercentage: 3]
  }

还记得我们从一个控制器开始,该控制器返回了一个杂乱无章模型,里面放着各种各样的东西吗?

return [ 
      username: securityService.user.name,
      tasks : tasks,
      showPercentage: showPercentage,
      months : getMonths(),
      startMonth : currentMonth,
      notifications : notifications
    ]

之后,我们将相关内容归为View模型,以查看彼此之间的归属。 如果您发现自己仍然以模型中的12个“简单值”作为结尾,则可能表明仍然可以在其中找到一些关系,将候选者归为一组。

我的域类还不是视图模型吗?

当然。 对于CRUD屏幕,您需要显示的信息基本上与域类中的信息完全相同。 大多数时候。 好的,对于更复杂的管理前端或后端而言,这并不完全正确,但是如果您查看一些适用于简单方案的脚手架控制器,那就似乎是这样。

我写了一篇较早的文章,讲述了即使在简单的情况下也要注意的一些以域类为中心的事情;-)

我的标签应该始终呈现某种模板吗?

当然不是。

如您所见

def taskBrowser = { attrs ->
  ...
 
  TaskBrowser browser = attrs.taskBrowser
   
  // filter tasks by type
  def tasks = browser.tasks
  if (attrs.filter && attrs.filter instanceof Task.Type) {
    tasks = browser.tasks.findAll { task -> task.type == attrs.filter }
  } 

  out << render(template: '/layouts/components/taskBrowser', model: [...])

过滤逻辑是在标记本身中完成的,但是显示1000行以上HTML被委托给_taskBrowser.gsp 。 这自然适用于大块HTML(即GSP)。

可以在标签本身中做一些小HTML工作,例如

def importantMonth = { attrs ->
  out << "<strong>${attrs.month}</strong>"
}
HTML与以代码为中心

这里肯定有一个带有标记库的灰色区域

下一个示例标记更加以代码为中心 ,而HTML介于两者之间。

def showMonths = { attrs ->
  out << "<ol>"
  months.each { month ->
    out << "<li>${month}</li>"
  }
  out << "</ol>"
}

如您所见,由于代码充斥着控制流和HTML输出,因此很难看到发生了什么。 如果您采用更以HTML为中心的方法,则最终会得到这样的GSP代码段:

<ol>
<g:each var="month" in="${months}">
    <li>${month}</li>
</g:each>
</ol>

在我们的情况下,如果我们需要一些复杂的Task Browser UI组件的1000多行HTML,那么最好将该HTML放到模板中;-)

结论

使用视图模型和标记库成功分离关注点可以进行组件思考,因为组件可以一起工作并且应该一起更改,所以各个组件位于代码库中清晰,隔离和可区分的位置,因此可以使代码更易于维护。

希望如果您发现自己处在与我们相似的情况下,那么思考过程至少可以对您有所帮助。

考虑不时抛出视图模型;-)

你说Testabillity怎么样? 在以后的文章中,我将描述如何测试我们的新标签库。

进一步阅读

翻译自: https://www.javacodegeeks.com/2016/02/grails-design-consideration-2-throw-view-model-ted-vinkes-blog.html

grails2 跨域

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值