5.3.1 内置标签#
由于Play采用的是MVC模型,这就要求表现层从请求处理和数据存储中分离开来。但是静态页面的功能毕竟是有限的,不能满足复杂的显示的需要,Play对一些常用的模板标签进行了封装,不仅实现了表现层和控制层的业务分离,还大大提升了模板功能的重用性。
1. #{a}
Play需要通过路由器(逆向)生成URL,以此来调用指定的Action。框架封装了HTML中的超链接标签<a>,可以方便地调用控制器中的Action,如:
#{a @Application.index()} 首页 #{/a}
该实例使用了内置标签#{a},可以生成指向控制器Application中index方法的HTML链接元素。在模板执行时,以上代码会被会解析为如下HTML代码:
<a href="/application/index ">首页</a>
2.#{authenticityToken /}
使用#{authenticityToken /}标签可以防止跨站请求伪造,同时也能消除刷新提交和后退提交所带来的困扰。#{authenticityToken /}首先会为服务器和客户端表单生成相同的随机令牌,并以隐藏的input输入域的形式嵌入表单。当用户点击submit按钮提交时,令牌会伴随着表单信息一并发送至控制器中的Action,在Action对提交的表单进行处理前,会先进行令牌的比对,只有令牌信息一致时,才进行后续的表单处理操作。在模板中可以直接使用#{authenticityToken /}标签:
#{authenticityToken /}
当为form表单添加了#{authenticityToken /}标签后,HTML代码将会被解析为:
<input type="hidden" name="authenticityToken" value="1c6d92fed96200347f06b7c5e1a3a28fa258ef7c">
3.#{cache}
Play的设计理念是无状态架构,不建议开发人员记录用户的状态。缓存是很好的解决方案,在开发中会被大量的用到,但缓存也有它自身的缺点,因为缓存只是存在于内存中,适用于存放暂时性的数据,时间一到就会过期。框架对缓存进行了封装,可以在模板中以标签的形式对缓存进行操作:
#{cache 'startTime'} ${new java.util.Date()} #{/cache}
上例中使用#{cache}标签,将标签体内的日期进行缓存。还可以为#{cache}标签添加for参数,设置缓存的过期时间:
#{cache 'currentTime', for:'3s'} ${new java.util.Date()} #{/cache}
上例为#{cache}标签添加了for参数,设置缓存的过期时间为3秒。
4.#{doLayout /}
Play引入了模板继承机制,假设定义装饰模板main.html,代码如下:
<!-- common header here --> <h1>Main template</h1> <div id="content"> #{doLayout /} </div> <!-- common footer here -->
#{doLayout /}标签可以包含自身的页面内容,即表示在标签处可以插入子模板的内容。
5.#{extends}
子模板可以通过#{extends}标签继承已经定义好的装饰模板。
#{extends 'main.html'/} <h1>Some code</h1>
当子模板使用#{extends}标签继承了main.html,就会包含父模板的内容。需要注意的是:在指定模板中别漏掉 #{doLayout /}标签声明。
6.#{if},#{ifnot},#{ifError},#{ifErrors}
在模板中使用#{if}标签可以进行条件判断:
#{if user.countryCode == 'en' } Connected user is ${user} #{/if}
同时,#{if}标签还支持复杂的条件判断:
#{if ( request.actionMethod == 'administer' && user.isAdmin() ) } You are admin, allowed to administer. #{/if}
#{ifnot}标签顾名思义,就是ifnot的意思,其执行时具体的作用与#{if !condition}等价:
#{ifnot tasks} No tasks today #{/ifnot}
可能经常会遇到这种情况,经过控制器中Action的处理,向模板中注入验证错误的提示消息。#{ifError}标签可以判断指定的作用域中是否有验证错误的信息:
#{ifError 'user.name'} <p> User name is invalid: #{error 'user.name' /} <p> #{/ifError}
只要模板中有任何的验证错误信息,#{ifErrors}标签都可以将其输出:
#{ifErrors} <p>Error(s) found!</p> #{/ifErrors}
7.#{else},#{elseif}
#{else}标签通常与#{if}标签配合使用:
#{if user} Connected user is ${user} #{/if} #{else} Please log in #{/else}
在Play的模板中,#{else}标签也可以与#{list}标签一起使用,当使用#{list }标签进行迭代的集合为空时,可以执行#{else}标签中的内容:
#{list items:task, as:'task'} <li>${task}</li> #{/list} #{else} Nothing to do... #{/else}
#{elseif}标签的作用同样是进行条件判断,用法与#{else}标签非常类似,也可以与#{list}标签配合使用:
#{if tasks.size() > 1} Busy tasklist #{/if} #{elseif tasks} One task on the list #{/elseif} #{else} Nothing to do #{/else}
8. #{error},#{errorClass},#{errors}
#{error}标签的作用是输出验证后的错误消息。如果指定字段的错误消息确实存在,则直接将其打印出来:
#{error 'user.name'/}
#{errorClass}标签的作用是如果存在指定的验证错误消息,则标签在解析时将被替换为文本hasError,以此可以用于设置出现错误后的HTML元素的样式。
<input name="name" class="#{errorClass 'name'/}">
在模板执行时,以上代码将会按照如下规则解析:
<input name="name" class="${errors.forKey('name') ? 'hasError' : ''}">
#{errors}标签可以遍历当前的验证错误对象集合,其使用方式与 #{list}非常类似,循环体中使用的对象变量名称是${error}:
<ul> #{errors} <li>${error}</li> #{/errors} </ul>
#{errors}的标签体内参数变量有以下几种:
- error:表示error对象。
- error_index:表示当前错误在集合中的序号。
- error_isLast:表示是否是集合中的最后一个错误。
- error_isFirst:表示是否是集合中的第一个错误。
- error_parity:表示当前错误在集合中序号的奇偶值,可能是even或odd。
<table> <tr> <th>#</th> <th>Error</th> </tr> #{errors} <tr class="${error_parity}"> <td>${error_index}</td> <td>${error}</td> </tr> #{/errors} </table>
由于很多应用中表单的奇偶行的颜色是不同的,使用#{errors}标签的error_parity参数就可以很方便的解决这个问题,这样有利于用户查看表单,用户体验性更强。
9.#{field}
#{field}标签的作用非常大,本着Play的“简约”理念,#{field}标签可以帮助开发者减少同一变量名的重复使用:
<p> <label>&{'user.name'}</label> <input type="text" id="user_name" name="user.name" value="${user.name}" class="${errors.forKey('user.name') ? 'has_error' : ''}"> <span class="error">${errors.forKey('user.name')}</span> </p>
以上实例中,表单input输入中需要大量用到user.name。大量重复操作不仅给开发人员带来了不便,也使得页面的代码显得非常混乱。使用#{field}标签很大程度上可以缓解这个问题:
#{field 'user.name'} <p> <label>&{field.name}</label> <input type="text" id="${field.id}" name="${field.name}" value="${field.value}" class="${field.errorClass}"> <span class="error">${field.error}</span> </p> #{/}
Play提倡在页面开发中尽量使用#{field}标签,而不是重复地编写变量名。
10.#{form}
Play的#{form}标签封装了HTML中的<form>元素,在模板中插入#{form}标签就等价于插入了表单元素。当在模板中使用了#{form}标签时,Play会从路由配置中自动匹配已经定义好的HTTP方法:如果在路由配置中没有定义HTTP方法类型,则默认以POST方式请求;如果在路由配置中GET与POST都定义了,则默认以最先定义的HTTP方法执行。#{form}标签中有以下三种参数:
- method(可选):定义表单提交的HTTP方法类型,可以是POST或 GET。
- id(可选):用于定义表单的id属性。
- enctype(可选):设置表单数据编码方式,默认的编码方式为application/x-www-form-urlencoded。
注意:
Play中字符编码方式只能是utf-8。
在Play模板中使用#{form}标签的具体示例如下:
#{form @Client.details(), method:'GET', id:'detailsForm'} ... #{/form}
在模板执行时,以上代码会被会解析为如下HTML代码:
<form action="/client/details" id="detailsForm" method="GET" accept-charset="utf-8"> ... </form>
也可以指定目标实体,作为Action方法的一部分:
#{form @Client.details(client.id)} ... #{/form}
假设在控制器中,按照如下定义details方法。该方法中声明了String类型的参数,参数名为clientId。之后HTTP请求参数的名称会从声明的Action方法中匹配:
public static void details(String clientId){ // ... }
在模板代码解析时,Play会自动生成带clientId参数的URL地址:
<form action="/client/details?clientId=3442" method="GET" accept-charset="utf-8"> ... </form>
需要着重介绍的一点是,在使用#{form}标签以POST方式提交时,自带了令牌保护机制,也就是说#{form}标签包含了#{authenticityToken /}的功能:
#{form @Client.create(), method:'POST', id:'creationForm', enctype:'multipart/form-data' } ... #{/form}
在模板执行时,以上代码会被会解析为如下HTML代码:
<form action="/client/create" id="creationForm" method="POST" accept-charset="utf-8" enctype="multipart/form-data"> <input type="hidden" name="authenticityToken" value="1c6d92fed96200347f06b7c5e1a3a28fa258ef7c"> ... </form>
11.#{i18n}
i18n在模板中的用法比较特殊,不仅可以直接以标签的形式用于模板语言,也可以直接在JavaScript中调用。可以在JavaScript代码中使用i18n()方法来访问本地化消息文件,如i18n('app_title')。
首先,需要在conf/messages中进行国际化定义:
hello_world=Hello, World ! hello_someone=Hello %s !
之后,在模板中就可以插入#{i18n /}标签来开启国际化支持了:
#{i18n /}
在JavaScript中可以调用i18n()方法来进行国际化操作,通过conf/messages中的key获取当前key的国际化值:
alert(i18n('hello_world')); alert(i18n('hello_someone', 'John'));
还可以通过设置key的值来限制允许使用的国际化值,并且允许使用通配符来设置范围:
#{i18n keys:['title', 'menu.*'] /}
12.#{include}
#{include}标签的作用是在当前模板中导入另一个模板,并且当前模板中的所有变量对导入的模板透明。其具体使用方法如下:
<div id="tree"> #{include 'tree.html' /} </div>
上例代码中,直接在当前模板中导入了tree.html模板。
13.#{render}
#{render}标签的作用是指定模板进行渲染。#{render}标签的参数是指定渲染模板的路径,可以是绝对路径,也可以是相对于/app/views的路径:
#{render 'Application/other.html'/}
14.#{jsAction}
#{jsAction}标签会生成一个返回服务端Action的URL地址的JavaScript函数,它不会自动执行Ajax请求,需要通过返回的URL地址手动执行。具体的应用示例如下:
首先在conf/routes文件中定义路由:
GET /users/{id} Users.show
之后,就可以在客户端导入定义好的路由:
<script type="text/javascript"> var showUserAction = #{jsAction @Users.show(':id') /} var displayUserDetail = function(userId) { $('userDetail').load( showUserAction({id: userId}) ) } </script>
有关Ajax的具体用法,会在第10.12节进行详细介绍。
15.#{list}
#{list}标签提供非常简便的操作遍历对象集合:
<ul> #{list items:products, as:'product'} <li>${product}</li> #{/list} </ul>
其中#{list items:products, as:'product'}用来遍历对象集合products,循环体中的每个对象用变量product来引用。在循环体的代码中可以引用一些预定义的变量,这些变量有固定的名称,但是会以循环体中的对象变量名作为前缀,在上面的例子中就是product_Xxx。#{list}标签的预定义的变量有以下几种:
- note_index:表示当前对象在集合中的序号。
- note_isFirst:表示是否是集合中的第一个对象。
- note_isLast:表示是否是集合中的最后一个对象。
- note_parity:表示当前对象在集合中序号的奇偶值,可能是even或odd。
<ul> #{list items:products, as:'product'} <span class="${product_parity}">${product_index}. ${product}</span> ${product_isLast ? '' : '-'} #{/list} </ul>
#{list}标签中的items参数是可选的,也可以无需声明参数类型直接写参数,这样可以简化#{list}标签的用法:
#{list products, as:'product'} <li>${product}</li> #{/list}
在#{list}标签中可以使用Groovy中的range对象,用法与for循环非常类似:
#{list items:0..10, as:'i'} ${i} #{/list}
#{list items:'a'..'z', as:'letter'} ${letter} ${letter_isLast ? '' : '|' } #{/list}
#{list}标签中as参数也不是必须的,可以用约定的下划线符“ _ ”作为默认的变量名:
#{list users} <li>${_}</li> #{/list}
16.#{option}
#{option}标签是HTML中<option>的封装,其作用是在当前模板位置中插入<option>元素。#{option}标签的参数只有一个:
- value:选项的值。
#{option}标签的具体使用方法如下:
#{option user.id} ${user.name} #{/option}
在模板执行时,以上代码会被会解析为如下HTML代码:
<option value="42">jto</option>
#{option}标签一般与#{select}标签配合使用。
17.#{select}
#{select}标签是HTML中<select>的封装,其作用是在当前模板位置中插入<select>元素。#{select}标签有一个必选的参数:
- name(必须):设置select元素的name属性。
其具体使用方法如下:
#{select 'booking.beds', value:2, id:'select1'} #{option 1}One king-size bed#{/option} #{option 2}Two double beds#{/option} #{option 3}Three beds#{/option} #{/select}
在模板执行时,以上代码会被会解析为如下HTML代码:
<select name="booking.beds" size="1" id="select1" > <option value="1">One king-size bed</option> <option value="2" selected="selected">Two double beds</option> <option value="3">Three beds</option> </select>
在#{select}标签中使用items属性可以自动生成所需的option选择项:
- items(可选):设置集合对象。
- value(可选):设置默认选中项。
- labelProperty(可选):设置每一项的lable值对应对象的哪个属性。
- valueProperty(可选):设置每一项的value值对应对象的哪个属性,默认为对象的id属性。
注意:
labelProperty和valueProperty属性值不支持Java基本类型,需使用封装类型如Integer或Long来代替int或long类型 。
以下代码为#{select}标签中使用items属性的例子。首先由控制器Action向页面模板注入用户集合List<User> users,且每一个用户user都有name属性,id是User的主键:
#{select 'users', items:users, valueProperty:'id', labelProperty:'name', value:5, class:'test', id:'select2' /}
在模板执行时,以上代码会被会解析为如下HTML代码:
<select name="users" size="1" class="test" id="select2" > <option value="0" >User-0</option> <option value="1" >User-1</option> <option value="2" >User-2</option> <option value="3" >User-3</option> <option value="4" >User-4</option> <option value="5" selected="selected">User-5</option> </select>
18.#{set},#{get}
#{set}标签用于设置可以在模板中使用的变量。如#{set email:'china@oopsplay.org'},就设置了模板变量email的值,可以通过 #{get 'email'} 来获取。#{set}标签的定义中可以引用其他变量:
#{set title:'Profile of ' + user.login /}
还可以在#{set}标签体内定义变量的值:
#{set 'title'} Profile of ${user.login} #{/set}
#{get}标签的作用是获取由#{set}标签定义的值,在Play的模板中,通过get/set机制,可以使父模板和子模板间进行通信。
<head> <title>#{get 'title' /} </head>
可以在#{get}标签体内设置当变量不存在时的缺省值,如下例当title变量不存在时,会显示Homepage。
<head> <title>#{get 'title'}Homepage #{/} </head>
19.#{script}
#{script}标签用于生成一个<script>元素,可以引入/public/javascripts目录下的JavaScript文件。如 #{script 'jquery.js'}就引用了/public/javascripts/jquery.js 文件。
#{script src:'jquery-1.5.1.min.js ', id:'jquery' , charset:'utf-8' /}
#{script}标签有以下几类参数:
- src(必须):设置脚本文件的地址,无需在地址前加上默认父目录/public/javascripts。
- id(可选):设置生成的<script>元素的id。
- charset(可选):设置脚本文件的字符编码,默认为UTF-8。
如果只需要在#{script}标签中设置src的参数值,则可以省略src参数名,直接写上需要引入的JavaScript文件即可:
#{script 'jquery-1.5.1.min.js' /}
20.#{stylesheet}
#{stylesheet}标签的使用方法与#{script}类似,不同的是#{stylesheet}标签使用<link>元素来引入/public/stylesheets目录下的CSS文件。如#{stylesheet id:'main', media:'print', src:'print.css', title:'Print stylesheet' /}就引用了/public/stylesheets/print.js 文件。
#{stylesheet}标签有以下几类参数:
- src(必须):设置样式文件的地址,无需在地址前加上默认父目录/public/stylesheets。
- id(可选):设置生成的<link>标签的id。
- media(可选):设置 media 属性: screen,print,aural,projection……
- title(可选):设置标签title属性。
如果只需要在#{stylesheet}标签中设置src的参数值,则可以省略src参数名,直接写上需要引入的CSS文件即可:
#{stylesheet 'default.css' /}
21.#{verbatim}
使用#{verbatim}标签可以禁用模板中的HTML转义功能。如果直接按照如下方式在模板中输出表达式:
${'&'}
经模板解析后,会默认转义输出“& ;”。如果使用了#{verbatim}标签:
#{verbatim}${'&'}#{/verbatim}
模板解析时将不会进行转义,直接输出“&”。
5.3.2 自定义tag#
Play提供了模板标签的自定义功能,可以非常方便地为应用创建特定的标签。这不仅提高了代码重用性,也使模板中的代码更加整洁。在这里提到的标签,指的就是简单的模板文件(存放在app/views/tags目录中),在使用时标签的名称需要与模板的文件名保持一致。
具体使用方法如下。在app/views/tags目录中新建hello.html文件,即成功创建了hello标签,之后编辑hello.html中的内容:
Hello from tag!
并没有进行任何配置,就完成了标签的自定义,之后就可以在其他模板中直接使用该标签了:
#{hello /}
获取标签参数
在标签中可以包含参数,框架会自动将参数传递给模板变量。在使用中,标签的参数名与模板变量名并不是完全一致的,模板变量声明时需要增加下划线_。例如,在hello.html标签模板文件中进行如下声明:
Hello ${_name}!
在其他模板中,就可以通过如下语句将name参数传递给标签了:
#{hello name:'Bob' /}
如果标签中只有一个参数,就可以采用默认的参数名称arg。由于该名称是内置的,在使用时可以不显式地写出参数名。例如,在hello.html标签模板文件中声明:
Hello ${_arg}!
在其他模板中,简单地使用如下语句就可以将Bob传递给标签:
#{hello 'Bob' /}
调用标签体
起始标签和结束标签之间包含的内容称为标签体,标签体可以是文本,HTML等。在Play中调用#{doBody /}标签可以很方便地完成以上内容。例如,在hello.html标签模板文件中声明:
Hello #{doBody /}!
之后Ben将会作为标签体进行传递:
#{hello} Ben #{/hello}
自动选择最适标签
根据content type设置,不同的页面可能会由不同的格式进行渲染(HTML,XML,JSON等)。这一点在Play中完全不用担心,框架会选取最适合的形式来显示。例如,当请求希望以HTML格式渲染时,Play会自动调用app/views/tags/hello.html模板;当请求希望以XML格式渲染时,Play则会选择app/views/tags/hello.xml模板。
在Play中可以定义后缀为.tag的通用模板标签,但他的优先级是最低的。只有当规定的模板标签格式是无效时,才会调用它。例如,在程序中只定义了两个模板标签,分别是app/views/tags/hello.xml和app/views/tags/hello.tag,当请求希望以HTML格式渲染时,框架会优先寻找hello.html模板,当发现该文件不存在时,才会调用hello.tag模板。
5.3.3 自定义Play标签#
也可以在Java代码中自定义标签,封装一些更为复杂的功能。如果希望自定义Java标签,需要先创建继承于play.templates.FastTags的类。
package utils; public class MyFastTag extends FastTags { ... }
每个想要作为标签(tag)运行的方法都具有固定的声明方式,具体如下:
public static void _tagName(Map<?, ?> args, Closure body, PrintWriter out, ExecutableTemplate template, int fromLine)
注意:
别漏了标签名前面的下划线。
为了进一步了解如何创建标签,先来看看两个Play内置标签的例子(Play封装了这两个标签,在play/framework/src/play/templates/FastTags.java文件中可以查看)。
#{verbatim}标签只是简单的调用了从JavaExtensions传递过来的toString()方法,直接在模板上打印了标签体中的内容:
public static void _verbatim(Map<?, ?> args, Closure body, PrintWriter out, ExecutableTemplate template, int fromLine) { out.println(JavaExtensions.toString(body)); }
该标签的具体使用方法如下,从标签的开始到结束都是标签体内容:
#{verbatim}My verbatim #{/verbatim}
模板中将会打印出标签体内容:
My verbatim
第二个Play内置标签例子是#{option},它要稍微复杂一点,因为它的实现依赖于父标签#{select}(Play也封装了#{select}标签,在play/framework/templates/tags/select.tag文件中可以查看具体定义)。
public static void _option(Map<?, ?> args, Closure body, PrintWriter out, ExecutableTemplate template, int fromLine) { Object value = args.get("arg"); Object selection = TagContext.parent("select").data.get("selected"); boolean selected = selection != null && value != null && selection.equals(value); out.print("<option value=\"" + (value == null ? "" : value) + "\" " + (selected ? "selected=\"selected\"" : "") + "" + serialize(args, "selected", "value") + ">"); out.println(JavaExtensions.toString(body)); out.print("</option>"); }
该Java标签的原理是封装了HTML的<option>标签,用于实现下拉列表实例,并且从父标签#{select}中获取被默认选中(selected)的值。方法体最开始的三行用于设定输出的变量,最后的三行则输出了标签的结果。该标签的具体使用方法如下:
#{select name:"country",value:"中国"} #{option arg:"英国"}英国#{/option} #{option arg:"中国"}中国#{/option} #{option arg:"美国"}美国#{/option} #{option arg:"日本"}日本#{/option} #{/select}
这里需要对父标签#{select}的使用做一些简单说明:country是#{select}标签的name属性,是必选的;中国是#{select}标签的value属性,用于标识selected元素,是可选的。
标签的命名空间
实际项目中可能会自定义大量的标签,标签命名不规范可能会造成一些不必要的麻烦(比如与已定义的标签重名,或是与Play框架的核心标签冲突)。为了解决这个问题,Play可以使用@FastTags.Namespace注解来设定命名空间。对于my.tags命名空间中的#{hello}标签需要做以下工作:
@FastTags.Namespace("my.tags") public class MyFastTag extends FastTags { public static void _hello (Map<?, ?> args, Closure body, PrintWriter out, ExecutableTemplate template, int fromLine) { ... } }
然后在模板中可以通过以下方式来调用#{hello}标签:
#{my.tags.hello/}