自定义指令

本章将介绍怎样自定义指令。

可以使用 macro 指令来自定义指令,Java 程序员还可以通过扩展 freemarker.template.TemplateDirectiveModel 类来自定义指令。

基本内容

宏是有一个变量名的模板片段。可以在模板中使用宏作为自定义指令, 这样就能进行重复性的工作。例如,创建一个宏变量来输出大字号的“Hello Joe!”:

<#macro greet>
  <font>Hello Joe!</font>
</#macro>

macro 指令自身不输出任何内容,它只是用来创建宏变量,所以就会有一个名为 greet 的宏变量。在 <#macro greet> 和 </#macro> 之间的内容 (称为宏定义体) 将会在使用该宏变量作为指令时执行。可以在 FTL 标记中通过 <@宏变量名></@宏变量名> 的方式来使用自定义指令。因此,就可以这样来使用 greet 宏变量:

<@greet></@greet>

因为 <anything></anything> 和 <anything/> 是相同的,也可以使用单标记形式:

<@greet/>

将会输出:

  <font>Hello Joe!</font>

宏能做的事情还有很多,因为在 <#macro ...> 和 </#macro> 之间的东西是模板片段,也就是说它可以包含插值 (${...}) 和 FTL 标签 (如 <#if ...>...</#if>)。

注意:程序员通常将使用 <@宏变量名></@宏变量名> 称为宏调用。

参数

我们来改进 greet 宏使之可以使用任意的名字, 而不仅仅是“Joe”。为了实现这个目的,就要使用到参数。在 macro 指令中,宏名称的后面位置是用来定义参数的。这里我们仅在 greet 宏中定义一个参数,person:

<#macro greet person>
  <font>Hello ${person}!</font>
</#macro>

那么就可以这样来使用这个宏:

<@greet person="Fred"/>
<@greet person="Batman"/>

这和HTML的语法是很相似的,将会输出:

<font>Hello Fred!</font>
<font>Hello Batman!</font>

那么我们就看到了,宏参数的真实值是可以作为变量(person)放在宏定义体中的。使用预定义指令时,参数的值 (=号后边的值) 可以是 FTL 表达式。如果参数的值是表达式或变量,“Fred”和“Batman”引号就可以不用要了。如:

<@greet person=Fred/>

这意味着使用变量的值 Fred 作为 person 参数,而不是字符串“Fred”。当然参数值并不一定是字符串类型,也可以是数字,布尔值,哈希表,序列等。也可以在 = 号左边使用复杂表达式,比如:

<@greet person=(price + 50) * 1.25) />

自定义指令可以有多个参数。如下所示,再添加一个新的参数 color:

<#macro greet person color>
  <font color="${color}">Hello ${person}!</font>
</#macro>

那么,这个宏就可以这样来使用:

<@greet person="Fred" color="black"/>

参数的顺序不重要,下面的这个和上面的含义也是相同的:

<@greet color="black" person="Fred"/>

当调用这个宏的时候,只能使用在 macro 指令中定义的参数(本例中是:person 和 color)。 那么当你尝试使用没有定义的参数时就会发生错误,如下:

<@greet person="Fred" color="black" background="green"/>

因为我们并没有在 <#macro...> 中声明参数 background。

同时也必须给出在宏中定义所有参数的值,如果忘记指定某个参数,也会发生错误。如下:

<@greet person="Fred"/>

上面因为忘记指定 color的值了,因此发生错误。 

很多情况下,有些参数我们并不一定是必须要填写的。但是,从语法上说如果我们忘记填写某些参数,则会发生错误。此时,我们可以通过给不必填的参数添加默认值,来解决不填写问题。

在 macro 指令中必须这么来指定变量:

param_name=usual_value

其中,param_name 为参数名称,usual_value 为参数默认值。例如:当没有特定值的时候,我们想要给 color 赋值为 "black",那么 greet 指令就要这么来写:

<#macro greet person color="black">
  <font size="+2" color="${color}">Hello ${person}!</font>
</#macro>

现在,我们这么使用宏就可以了: 

<@greet person="Fred"/>
<#-- 两则等价 -->
<@greet person="Fred" color="black"/>
<#-- 如果想给 color 设置为 "red",macro 指令就会使用 red 来覆盖之前设置的通用值, 参数 color 的值就会是 "red" 了 -->
<@greet person="Fred" color="red"/>

根据已知的 FTL 表达式规则,明白下面这一点是至关重要的。someParam=foo 和 someParam="${foo}" 是不同的。

  • someParam=foo 是把变量 foo 的值作为参数的值来使用。

  • someParam="${foo}" 是使用插值形式的字符串, 那么参数值就是字符串了,这个时候, foo 的值呈现为文本, 而不管 foo 是什么类型的 (数字,日期等)。

看下面这个例子: someParam=3/4 和 someParam="${3/4}" 是不同的。 如果指令需要 someParam 是一个数字值, 那么就不要用第二种方式。

宏参数的另外一个重要的方面是它们是。 更多局部变量的信息可以阅读:模板中定义变量

嵌套内容

自定义指令可以嵌套内容(使用 <#nested> 指令引用宏指令开始和结束标签中间的内容),和预定义指令相似:<#if ...>nested content</#if>。 例如,下面这个例子中是创建了一个可以为嵌套的内容画出边框的宏:

<#macro border>
  <table border=4 cellspacing=0 cellpadding=4>
      <tr>
          <td><#nested></td>
      </tr>
  </table>
</#macro>

<#nested> 指令执行位于开始和结束标记指令之间的模板代码段。 如果这样写:

<@border>The bordered text</@border>

将会输出:

  <table border=4 cellspacing=0 cellpadding=4>
      <tr>
          <td>
              The bordered text
          </td>
      </tr>
  </table>

nested 指令也可以多次被调用,例如:

<#macro do_thrice>
  <#nested>
  <#nested>
  <#nested>
</#macro>

<#-- 宏调用 -->
<@do_thrice>
  Anything.
</@do_thrice>

将会输出:

  Anything.
  Anything.
  Anything.

如果不使用 nested 指令, 那么嵌套的内容就不会被执行,如果不小心将 greet 指令写成了这样:

<@greet person="Joe">
  Anything.
</@greet>

FreeMarker 不会把它视为错误,只是输出:

<font size="+2">Hello Joe!</font>

嵌套的内容被忽略了,因为 greet 宏没有使用 nested 指令。

嵌套的内容可以是任意有效的 FTL,包含其他用户的自定义指令,也是对的:

<@border>
    <ul>
        <@do_thrice>
            <li><@greet person="Joe"/>
        </@do_thrice>
    </ul>
</@border>

将会输出:

  <table border=4 cellspacing=0 cellpadding=4>
      <tr>
          <td>
            <ul>
                <li><font size="+2">Hello Joe!</font>
                <li><font size="+2">Hello Joe!</font>
                <li><font size="+2">Hello Joe!</font>
            </ul>
        </td>
    </tr>
</table>

在嵌套的内容中,宏的局部变量是不可见的。为了说明这点,我们来看:

<#macro repeat count>
  <#local y = "test">
  <#list 1..count as x>
    ${y} ${count}/${x}: <#nested>
  </#list>
</#macro>

<@repeat count=3>${y!"?"} ${x!"?"} ${count!"?"}</@repeat>

将会输出:

    test 3/1: ? ? ?
    test 3/2: ? ? ?
    test 3/3: ? ? ?

因为 y,x 和 count 是宏的局部 (私有) 变量,从宏外部定义是不可见的。此外不同的局部变量的设置是为每个宏自己调用的,所以不会导致混乱:

<#macro test foo>
    ${foo} (<#nested>) ${foo}
</#macro>

<@test foo="A">
    <@test foo="B">
        <@test foo="C"/>
    </@test>
</@test>

将会输出:

A (B (C () C) B) A

宏和循环变量

像 list 这样的预定义指令可以使用循环变量,可以阅读 模板中定义变量 来理解循环变量。

自定义指令也可以有循环变量。比如我们来扩展先前例子中的 do_thrice 指令,就可以拿到当前的循环变量的值。而对于预定义指令 (如list),当调用指令时,循环变量的 name 是给定的,比如 <#list foos as foo>...</#list> 中的 foo,变量 value 的设置是由指令本身完成的。

<#macro do_thrice>
  <#nested 1>
  <#nested 2>
  <#nested 3>
</#macro>

<#-- 宏调用 -->
<#-- 用户定义指令使用“;”而不是“as” -->
<@do_thrice ; x>
    ${x} Anything.
</@do_thrice>

将会输出:

  1 Anything.
  2 Anything.
  3 Anything.

语法规则是给确定“循环”的循环变量传递真实值(比如重复嵌套内容)来作为 nested 指令的参数(当然参数可以是任意的表达式)。 循环变量的名称是在自定义指令的开始标记 (<@...>) 的参数后面通过分号确定的。

一个宏可以使用多个循环变量 (变量的顺序是很重要的):

<#macro repeat count>
  <#list 1..count as x>
    <#nested x, x/2, x==count>
  </#list>
</#macro>

<@repeat count=4 ; c, halfc, last>
  ${c}. ${halfc}<#if last> Last!</#if>
</@repeat>

将会输出:

  1. 0.5
  2. 1
  3. 1.5
  4. 2 Last!

如果你在用户定义的指令开始标签 (分号之后) 和嵌套指令中指定不同数量的循环变量,这不是问题。如果在分号之后指定的循环变量少, 那么就看不到 nested指令提供的最后的值, 因为没有循环变量来存储这些值,下面的这些都是可以的:

<@repeat count=4 ; c, halfc, last>
  ${c}. ${halfc}<#if last> Last!</#if>
</@repeat>

<@repeat count=4 ; c, halfc>
  ${c}. ${halfc}
</@repeat>

<@repeat count=4>
  Just repeat it...
</@repeat>

如果在分号后面指定了比 nested 指令还多的变量, 那么最后的循环变量将不会被创建 (在嵌套内容中不会被定义)。

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
公众号