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图像- 占用了大量空间 。
任务概述结束后,将出现一些滑块。
如果您曾经从设计机构那里获得过静态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”)和底部的任务概览和滑块所需的各种变量( tasks
, showPercentage
, months
, startMonth
, notifications
)。
因此,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对象(POGO),仅用于保存视图“组件”的相关数据。 嘿,这是我第一次听到您提到组件 。 是的,这是那些重载的术语之一,可以表示任何含义。 幸运的是,因为我们还可以为任何事物创建POGO ;-)
在HomeController.groovy的底部创建一个新类 ,并根据您的UI小部件或组件表示的名称对其进行命名。 直到现在,我一直在说“任务概述”,并且可以调用新类: TasksOverview
。
在这种情况下,我们从外部网页设计机构获取布局,他们通常还会考虑命名对象,以用作CSS选择器或JavaScript对象。 资料来源显示:
<div class="row.." id="task-browser">
任务浏览器!
特别是在使用大量UI元素的项目中,在设计和开发团队之间尽早命名事物很重要。 您只是不能不说在右上角有沙漏 。
该TaskBrowser
类也可以是一个单独的TaskBrowser.groovy
在src
文件夹中,但现在它只是在主页所以只是把它的下面使用HomeController
类HomeController.groovy
会为现在要做的。
...
}
/**
* Task browser.
*/
class TaskBrowser {
}
将保存数据的每个关联属性从控制器的index()操作移动到此新类,例如tasks
和notifications
集合。
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
类)中。 一如既往,最有效的方法取决于用例。
瞧!
是否应该为所有内容创建特定的视图模型?
当然不是。 以滑块为例。
如果只需要一些属性,例如下面的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怎么样? 在以后的文章中,我将描述如何测试我们的新标签库。
进一步阅读
- 标签库 – Grails文档
- Grails域类和特殊表示要求 –
泰德·温克(Ted Vinke)的博客 - 模型–视图–视图模型 –维基百科
grails2 跨域