範本引擎

1. 簡介

Groovy 支援多種動態產生文字的方式,包括 GStringsprintfMarkupBuilder,僅舉幾例。除了這些之外,還有一個專用的範本架構,非常適合文字要遵循靜態範本形式的應用程式。

2. 範本架構

Groovy 中的範本架構包含 TemplateEngine 抽象基底類別(引擎必須實作)和 Template 介面(它們產生的範本必須實作)。

Groovy 中包含多個範本引擎

  • SimpleTemplateEngine - 適用於基本範本

  • StreamingTemplateEngine - 功能上等同於 SimpleTemplateEngine,但可處理大於 64k 的字串

  • GStringTemplateEngine - 將範本儲存為可寫閉包(適用於串流場景)

  • XmlTemplateEngine - 當範本和輸出為有效 XML 時,運作良好

  • MarkupTemplateEngine - 非常完整且最佳化的範本引擎

3. SimpleTemplateEngine

這裡顯示的 SimpleTemplateEngine 允許您在範本中使用類似 JSP 的指令碼小程式(請參閱以下範例)、指令碼和 EL 表達式,以產生參數化文字。以下是使用此系統的範例

def text = 'Dear "$firstname $lastname",\nSo nice to meet you in <% print city %>.\nSee you in ${month},\n${signed}'

def binding = ["firstname":"Sam", "lastname":"Pullara", "city":"San Francisco", "month":"December", "signed":"Groovy-Dev"]

def engine = new groovy.text.SimpleTemplateEngine()
def template = engine.createTemplate(text).make(binding)

def result = 'Dear "Sam Pullara",\nSo nice to meet you in San Francisco.\nSee you in December,\nGroovy-Dev'

assert result == template.toString()

雖然通常不建議在範本(或檢視)中混用處理邏輯,但有時非常簡單的邏輯會很有用。例如,在上述範例中,我們可以將此

$firstname

變更為此(假設我們已在範本內部設定大寫靜態匯入)

${firstname.capitalize()}

或此

<% print city %>

變更為此

<% print city == "New York" ? "The Big Apple" : city %>

3.1. 進階使用注意事項

如果您碰巧將範本直接嵌入在指令碼中(如我們在上面所做),您必須小心反斜線跳脫。由於範本字串本身會在傳遞到範本架構之前由 Groovy 分析,因此您必須跳脫 Groovy 程式中輸入的任何 GString 表達式或指令碼小程式「程式碼」中的任何反斜線。例如,如果我們想要在上述的「The Big Apple」周圍加上引號,我們會使用

<% print city == "New York" ? "\\"The Big Apple\\"" : city %>

類似地,如果我們想要換行,我們會使用

\\n

在 Groovy 指令碼中出現的任何 GString 表達式或指令碼小程式「程式碼」中。在靜態範本文字本身或整個範本本身位於外部範本檔案中的情況下,正常的「\n」是沒問題的。類似地,要在文字中表示實際的反斜線,您需要

\\\\

在外部檔案中或

\\\\

在任何 GString 表達式或指令碼小程式「程式碼」中。(注意:如果我們能找到支援此類變更的簡單方法,在 Groovy 的未來版本中可能不需要這個額外的斜線。)

4. StreamingTemplateEngine

StreamingTemplateEngine 引擎在功能上等同於 SimpleTemplateEngine,但會使用可寫入的封閉函數來建立範本,使其更適合用於大型範本。具體來說,這個範本引擎可以處理大於 64k 的字串。

它使用 JSP 風格 <% %> 指令碼和 <%= %> 運算式語法或 GString 風格運算式。變數 'out' 會繫結到範本寫入的寫入器。

通常,範本來源會是一個檔案,但我們在此展示一個簡單的範例,提供範本作為字串

def text = '''\
Dear <% out.print firstname %> ${lastname},

We <% if (accepted) out.print 'are pleased' else out.print 'regret' %> \
to inform you that your paper entitled
'$title' was ${ accepted ? 'accepted' : 'rejected' }.

The conference committee.'''

def template = new groovy.text.StreamingTemplateEngine().createTemplate(text)

def binding = [
    firstname : "Grace",
    lastname  : "Hopper",
    accepted  : true,
    title     : 'Groovy for COBOL programmers'
]

String response = template.make(binding)

assert response == '''Dear Grace Hopper,

We are pleased to inform you that your paper entitled
'Groovy for COBOL programmers' was accepted.

The conference committee.'''

5. GStringTemplateEngine

以下為使用 GStringTemplateEngine 的範例,這是上述範例的再次示範(稍作修改以顯示其他選項)。首先,我們這次會將範本儲存在檔案中

test.template
Dear "$firstname $lastname",
So nice to meet you in <% out << (city == "New York" ? "\\"The Big Apple\\"" : city) %>.
See you in ${month},
${signed}

請注意,我們使用 out 而不是 print 來支援 GStringTemplateEngine 的串流特性。由於範本在一個獨立的檔案中,因此不需要跳脫反斜線。以下是我們呼叫它的方式

def f = new File('test.template')
def engine = new groovy.text.GStringTemplateEngine()
def template = engine.createTemplate(f).make(binding)
println template.toString()

以下是輸出結果

Dear "Sam Pullara",
So nice to meet you in "The Big Apple".
See you in December,
Groovy-Dev

6. XmlTemplateEngine

XmlTemplateEngine 用於範本產生情境,其中範本來源和預期的輸出都應為 XML。範本可以使用一般的 ${expression}$variable 符號將任意運算式插入範本中。此外,還支援特殊標籤:<gsp:scriptlet>(用於插入程式碼片段)和 <gsp:expression>(用於產生輸出的程式碼片段)。

註解和處理指令會在處理過程中移除,而特殊 XML 字元(例如 <>"')會使用各自的 XML 符號進行跳脫。輸出也會使用標準 XML 美化列印進行縮排。

gsp: 標籤的 xmlns 名稱空間定義會被移除,但其他名稱空間定義會保留(但可能會變更為 XML 樹中的等效位置)。

通常,範本來源會在一個檔案中,但我們在此展示一個簡單的範例,提供 XML 範本作為字串

def binding = [firstname: 'Jochen', lastname: 'Theodorou', nickname: 'blackdrag', salutation: 'Dear']
def engine = new groovy.text.XmlTemplateEngine()
def text = '''\
    <document xmlns:gsp='http://groovy.codehaus.org/2005/gsp' xmlns:foo='baz' type='letter'>
        <gsp:scriptlet>def greeting = "${salutation}est"</gsp:scriptlet>
        <gsp:expression>greeting</gsp:expression>
        <foo:to>$firstname "$nickname" $lastname</foo:to>
        How are you today?
    </document>
'''
def template = engine.createTemplate(text).make(binding)
println template.toString()

此範例將產生此輸出

<document type='letter'>
  Dearest
  <foo:to xmlns:foo='baz'>
    Jochen &quot;blackdrag&quot; Theodorou
  </foo:to>
  How are you today?
</document>

7. MarkupTemplateEngine

此範本引擎主要用於產生 XML 類型的標記 (XML、XHTML、HTML5、…​),但可用於產生任何基於文字的內容。與傳統範本引擎不同,此引擎依賴使用建構函式語法的 DSL。以下是範本範例

xmlDeclaration()
cars {
   cars.each {
       car(make: it.make, model: it.model)
   }
}

如果您提供以下模型

model = [cars: [new Car(make: 'Peugeot', model: '508'), new Car(make: 'Toyota', model: 'Prius')]]

將會呈現為

<?xml version='1.0'?>
<cars><car make='Peugeot' model='508'/><car make='Toyota' model='Prius'/></cars>

此範本引擎的主要功能有

  • 類似標記建構函式的語法

  • 範本會編譯成位元組碼

  • 快速呈現

  • 模型的選用類型檢查

  • 包含

  • 國際化支援

  • 片段/配置

7.1. 範本格式

7.1.1. 基礎

範本包含 Groovy 程式碼。讓我們更深入探討第一個範例

xmlDeclaration()                                (1)
cars {                                          (2)
   cars.each {                                  (3)
       car(make: it.make, model: it.model)      (4)
   }                                            (5)
}
1 呈現 XML 宣告字串。
2 開啟 cars 標籤
3 cars範本模型 中的變數,為 Car 實例的清單
4 針對每個項目,我們使用 Car 實例中的屬性建立 car 標籤
5 關閉 cars 標籤

如您所見,可以在範本中使用常規 Groovy 程式碼。在此,我們在清單 (從模型中擷取) 上呼叫 each,讓我們可以針對每個項目呈現一個 car 標籤。

類似地,呈現 HTML 程式碼就像這樣

yieldUnescaped '<!DOCTYPE html>'                                                    (1)
html(lang:'en') {                                                                   (2)
    head {                                                                          (3)
        meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"')      (4)
        title('My page')                                                            (5)
    }                                                                               (6)
    body {                                                                          (7)
        p('This is an example of HTML contents')                                    (8)
    }                                                                               (9)
}                                                                                   (10)
1 呈現 HTML doctype 特殊標籤
2 開啟具有屬性的 html 標籤
3 開啟 head 標籤
4 呈現一個具有 http-equiv 屬性的 meta 標籤
5 呈現 title 標籤
6 關閉 head 標籤
7 開啟 body 標籤
8 呈現 p 標籤
9 關閉 body 標籤
10 關閉 html 標籤

輸出很簡單

<!DOCTYPE html><html lang='en'><head><meta http-equiv='"Content-Type" content="text/html; charset=utf-8"'/><title>My page</title></head><body><p>This is an example of HTML contents</p></body></html>
透過一些 設定,您可以讓輸出格式化,並自動加入換行符號和縮排。

7.1.2. 支援方法

在先前的範例中,doctype 宣告是使用 yieldUnescaped 方法呈現的。我們也看過 xmlDeclaration 方法。範本引擎提供多種支援方法,可協助您適當地呈現內容

方法 說明 範例

yield

呈現內容,但在呈現前先跳脫。

範本:

yield 'Some text with <angle brackets>'

輸出:

Some text with &lt;angle brackets&gt;

yieldUnescaped

呈現原始內容。參數會原樣呈現,不跳脫。

範本:

yieldUnescaped 'Some text with <angle brackets>'

輸出:

Some text with <angle brackets>

xmlDeclaration

呈現 XML 宣告字串。如果組態中指定編碼,則會寫入宣告中。

範本:

xmlDeclaration()

輸出:

<?xml version='1.0'?>

如果 TemplateConfiguration#getDeclarationEncoding 不為 null

輸出:

<?xml version='1.0' encoding='UTF-8'?>

comment

在 XML 註解中呈現原始內容

範本:

comment 'This is <a href='https://docs.groovy-lang.org/latest/html/documentation/foo.html'>commented out</a>'

輸出:

<!--This is <a href='https://docs.groovy-lang.org/latest/html/documentation/foo.html'>commented out</a>-->

newLine

呈現新行。另請參閱 TemplateConfiguration#setAutoNewLineTemplateConfiguration#setNewLineString

範本:

p('text')
newLine()
p('text on new line')

輸出:

<p>text</p>
<p>text on new line</p>

pi

呈現 XML 處理指令。

範本:

pi("xml-stylesheet":[href:"mystyle.css", type:"text/css"])

輸出:

<?xml-stylesheet href='mystyle.css' type='text/css'?>

tryEscape

如果物件是 String (或衍生自 CharSequence 的任何類型),則傳回物件的跳脫字串。否則傳回物件本身。

範本:

yieldUnescaped tryEscape('Some text with <angle brackets>')

輸出:

Some text with &lt;angle brackets&gt;

7.1.3. 包含

MarkupTemplateEngine 支援從其他檔案包含內容。包含的內容可能是

  • 另一個範本

  • 原始內容

  • 要跳脫的內容

可以使用下列方式包含另一個範本

include template: 'other_template.tpl'

可以使用下列方式包含檔案作為原始內容,不跳脫

include unescaped: 'raw.txt'

最後,可以使用下列方式包含要在呈現前跳脫的文字

include escaped: 'to_be_escaped.txt'

或者,您可以改用下列輔助方法

  • includeGroovy(<name>) 包含另一個範本

  • includeEscaped(<name>) 包含另一個檔案並跳脫

  • includeUnescaped(<name>) 包含另一個檔案而不跳脫

如果要包含的檔案名稱是動態的 (例如儲存在變數中),則呼叫這些方法會比使用 include xxx: 語法更實用。要包含的檔案 (不論類型,範本或文字) 都會在 classpath 中找到。這是 MarkupTemplateEngine 將選擇性 ClassLoader 作為建構函式參數的原因之一 (另一個原因是您可以在範本中包含參照其他類別的程式碼)。

如果您不希望範本在 classpath 中,MarkupTemplateEngine 接受一個方便的建構函式,讓您可以定義要尋找範本的目錄。

7.1.4. 片段

片段是巢狀範本。它們可用於在單一範本中提供更佳的組合。片段包含字串、內部範本和用於呈現此範本的模型。請考慮下列範本

ul {
    pages.each {
        fragment "li(line)", line:it
    }
}

fragment 元素會建立巢狀範本,並使用特定於此範本的模型來呈現它。在此,我們有 li(line) 片段,其中 line 繫結至 it。由於 it 對應於 pages 的反覆運算,因此我們將為模型中的每個頁面產生單一 li 元素

<ul><li>Page 1</li><li>Page 2</li></ul>

片段對於分解範本元素很有用。它們以每個範本編譯一個片段的代價而來,而且無法外置化。

7.1.5. 版面

版面與片段不同,它們會參考其他範本。它們可用於組合範本並共用常見結構。如果您有例如常見的 HTML 頁面設定,而且只想替換主體時,這通常很有用。這可以用版面輕鬆完成。首先,您需要建立一個版面範本

layout-main.tpl
html {
    head {
        title(title)                (1)
    }
    body {
        bodyContents()              (2)
    }
}
1 title 變數(在標題標籤內)是版面變數
2 bodyContents 呼叫會呈現主體

然後您需要的是包含版面的範本

layout 'layout-main.tpl',                                   (1)
    title: 'Layout example',                                (2)
    bodyContents: contents { p('This is the body') }        (3)
1 使用 main-layout.tpl 版面檔案
2 設定 title 變數
3 設定 bodyContents

如您所見,bodyContents 會在版面內呈現,這要歸功於版面檔案中的 bodyContents() 呼叫。因此,範本會呈現為這樣

<html><head><title>Layout example</title></head><body><p>This is the body</p></body></html>

呼叫 contents 方法用於告訴範本引擎,程式碼區塊實際上是範本的明細,而不是要直接呈現的輔助函式。如果您沒有在明細前加入 contents,那麼主體會被呈現,但您也會看到一個隨機產生的字串,對應於區塊的結果值。

版面是跨多個範本共用常見元素的強大方式,而無需重寫所有內容或使用包含。

版面預設使用一個模型,這個模型獨立於使用版面的頁面模型。然而,可以讓它們繼承自父模型。假設模型定義如下

model = new HashMap<String,Object>();
model.put('title','Title from main model');

以及下列範本

layout 'layout-main.tpl', true,                             (1)
    bodyContents: contents { p('This is the body') }
1 請注意使用 true 來啟用模型繼承

那麼就不需要將 title 值傳遞給版面,就像在 前一個範例 中一樣。結果會是

<html><head><title>Title from main model</title></head><body><p>This is the body</p></body></html>

但也可以覆寫父模型中的值

layout 'layout-main.tpl', true,                             (1)
    title: 'overridden title',                               (2)
    bodyContents: contents { p('This is the body') }
1 true 表示繼承自父模型
2 title 已被覆寫

那麼輸出會是

<html><head><title>overridden title</title></head><body><p>This is the body</p></body></html>

7.2. 呈現主體

7.2.1. 建立範本引擎

在伺服器端,呈現範本需要 groovy.text.markup.MarkupTemplateEngine 的執行個體和 groovy.text.markup.TemplateConfiguration

TemplateConfiguration config = new TemplateConfiguration();         (1)
MarkupTemplateEngine engine = new MarkupTemplateEngine(config);     (2)
Template template = engine.createTemplate("p('test template')");    (3)
Map<String, Object> model = new HashMap<>();                        (4)
Writable output = template.make(model);                             (5)
output.writeTo(writer);                                             (6)
1 建立範本組態
2 使用此組態建立範本引擎
3 String 建立範本實例
4 建立模型以用於範本
5 將模型繫結至範本實例
6 呈現輸出

有數種可能的選項可供解析範本

  • String,使用 createTemplate(String)

  • Reader,使用 createTemplate(Reader)

  • URL,使用 createTemplate(URL)

  • 給定範本名稱,使用 createTemplateByPath(String)

最後一個版本通常較為優先

Template template = engine.createTemplateByPath("main.tpl");
Writable output = template.make(model);
output.writeTo(writer);

7.2.2. 組態選項

引擎的行為可以使用透過 TemplateConfiguration 類別存取的數個組態選項進行調整

選項 預設值 說明 範例

declarationEncoding

null

在呼叫 xmlDeclaration 時,決定要寫入的編碼值。它不會影響您用作輸出的寫入器。

範本:

xmlDeclaration()

輸出:

<?xml version='1.0'?>

如果 TemplateConfiguration#getDeclarationEncoding 不為 null

輸出:

<?xml version='1.0' encoding='UTF-8'?>

expandEmptyElements

false

如果為 true,則會以展開形式呈現空標籤。

範本:

p()

輸出:

<p/>

如果 expandEmptyElements 為 true

輸出:

<p></p>

useDoubleQuotes

false

如果為 true,則對屬性使用雙引號,而非單引號

範本:

tag(attr:'value')

輸出:

<tag attr='value'/>

如果 useDoubleQuotes 為 true

輸出:

<tag attr="value"/>

newLineString

系統預設值(系統屬性 line.separator

允許選擇在呈現新行時使用的字串

範本:

p('foo')
newLine()
p('baz')

如果 newLineString='BAR'

輸出:

<p>foo</p>BAR<p>baz</p>

autoEscape

false

如果為 true,則在呈現之前會自動跳脫模型中的變數。

請參閱 自動跳脫區段

autoIndent

false

如果為 true,則會在新行後自動縮排

autoIndentString

四 (4) 個空格

用作縮排的字串。

autoNewLine

false

如果為 true,則會自動插入新行

baseTemplateClass

groovy.text.markup.BaseTemplate

設定已編譯範本的超類別。這可以用於提供特定於應用程式的範本。

請參閱 自訂範本區段

locale

預設語言環境

設定範本的預設語言環境。

請參閱 國際化區段

建立範本引擎後,變更組態是不安全的

7.2.3. 自動格式化

預設情況下,範本引擎會在沒有任何特定格式化的情況下呈現輸出。某些 組態選項 可以改善這個情況

  • autoIndent 負責在插入新行後自動縮排

  • autoNewLine 負責根據範本來源的原始格式自動插入新行

一般來說,如果你想要人類可讀、美觀列印的輸出,建議將 autoIndentautoNewLine 都設定為 true

config.setAutoNewLine(true);
config.setAutoIndent(true);

使用以下範本

html {
    head {
        title('Title')
    }
}

輸出現在會是

<html>
    <head>
        <title>Title</title>
    </head>
</html>

我們可以稍微變更範本,讓 title 指令出現在與 head 相同的行

html {
    head { title('Title')
    }
}

輸出也會反映這一點

<html>
    <head><title>Title</title>
    </head>
</html>

新行會插入在找到標籤的大括號處,而且插入會對應到找到巢狀內容的位置。這表示另一個標籤主體中的標籤不會觸發新行,除非它們自己使用大括號

html {
    head {
        meta(attr:'value')          (1)
        title('Title')              (2)
        newLine()                   (3)
        meta(attr:'value2')         (4)
    }
}
1 插入新行是因為 meta 不在與 head 相同的行
2 沒有插入新行,因為我們與前一個標籤在相同深度
3 我們可以透過明確呼叫 newLine 來強制呈現新行
4 而且這個標籤會呈現在個別行上

這次,輸出會是

<html>
    <head>
        <meta attr='value'/><title>Title</title>
        <meta attr='value2'/>
    </head>
</html>

預設情況下,渲染器使用四 (4) 個空格作為縮排,但你可以透過設定 TemplateConfiguration#autoIndentString 屬性來變更

7.2.4. 自動跳脫

預設情況下,從模型讀取的內容會原樣呈現。如果這個內容來自使用者輸入,這可能是合理的,而且你可能想要預設跳脫它,例如避免 XSS 注入。為了做到這一點,範本組態提供一個選項,只要物件從 CharSequence 繼承 (通常是 `String`s),就會自動跳脫模型中的物件。

讓我們想像以下設定

config.setAutoEscape(false);
model = new HashMap<String,Object>();
model.put("unsafeContents", "I am an <html> hacker.");

以及下列範本

html {
    body {
        div(unsafeContents)
    }
}

那麼你不會想要將 unsafeContents 中的 HTML 原樣呈現,因為有潛在的安全問題

<html><body><div>I am an <html> hacker.</div></body></html>

自動跳脫會修正這個問題

config.setAutoEscape(true);

現在輸出已經正確跳脫

<html><body><div>I am an &lt;html&gt; hacker.</div></body></html>

請注意,使用自動跳脫並不會阻止你包含模型中未跳脫的內容。為此,你的範本應該明確提到不應該跳脫模型變數,方法是在前面加上 unescaped.,就像這個範例

明確繞過自動跳脫
html {
    body {
        div(unescaped.unsafeContents)
    }
}

7.2.5. 常見陷阱

包含標記的字串

假設您要產生一個包含標記字串的 <p> 標籤

p {
    yield "This is a "
    a(href:'target.html', "link")
    yield " to another page"
}

並產生

<p>This is a <a href='target.html'>link</a> to another page</p>

這不能寫得更簡短嗎?一個天真的替代方案會是

p {
    yield "This is a ${a(href:'target.html', "link")} to another page"
}

但結果看起來不會如預期

<p><a href='target.html'>link</a>This is a  to another page</p>

原因是標記範本引擎是一個串流引擎。在原始版本中,第一個 yield 呼叫產生一個串流到輸出的字串,然後產生 a 連結並串流,最後一個 yield 呼叫串流,導致執行順序。但使用上述字串版本,執行順序不同

  • yield 呼叫需要一個參數,一個字串

  • 這些參數需要在產生yield 呼叫之前評估

因此評估字串會導致在呼叫 yield 之前執行 a(href:…​) 呼叫。這不是您要執行的動作。相反地,您要產生一個包含標記的字串,然後傳遞給 yield 呼叫。這可以用這種方式執行

p("This is a ${stringOf {a(href:'target.html', "link")}} to another page")

請注意 stringOf 呼叫,它基本上告訴標記範本引擎底層標記需要個別呈現並輸出為一個字串。請注意,對於簡單的表達式,stringOf 可以替換為以美元符號開頭的替代標籤符號

p("This is a ${$a(href:'target.html', "link")} to another page")
值得注意的是,使用 stringOf 或特殊 $tag 符號會觸發產生一個不同的字串寫入器,然後用於呈現標記。它比使用直接串流標記的 yield 呼叫版本慢。

7.2.6. 國際化

範本引擎原生支援國際化。因此,當您建立 TemplateConfiguration 時,您可以提供一個 Locale,它是範本要使用的預設語言環境。每個範本可能有多個版本,每個語言環境一個。範本名稱會有所不同

  • file.tpl:預設範本檔案

  • file_fr_FR.tpl:範本的法文版本

  • file_en_US.tpl:範本的美國英語版本

  • …​

當範本被渲染或包含時,

  • 如果範本名稱或包含名稱明確設定區域設定,則包含特定版本,或在找不到時包含預設版本

  • 如果範本名稱不包含區域設定,則使用 TemplateConfiguration 區域設定的版本,或在找不到時使用預設版本

例如,假設預設區域設定設為 Locale.ENGLISH,且主範本包含

在包含中使用明確區域設定
include template: 'locale_include_fr_FR.tpl'

然後使用特定範本渲染範本

略過範本設定
Texte en français

使用包含而不指定區域設定,將使範本引擎尋找具有已設定區域設定的範本,如果找不到,則像這裡一樣,退回預設值

在包含中不使用區域設定
include template: 'locale_include.tpl'
退回預設範本
Default text

然而,將範本引擎的預設區域設定變更為 Locale.FRANCE 會變更輸出,因為範本引擎現在會尋找具有 fr_FR 區域設定的檔案

不退回預設範本,因為找到特定區域設定的範本
Texte en français

此策略讓您可以逐一翻譯範本,方法是依賴預設範本,其檔案名稱中未設定區域設定。

7.2.7. 自訂範本類別

預設情況下,建立的範本繼承 groovy.text.markup.BaseTemplate 類別。應用程式提供不同的範本類別可能會很有趣,例如提供知道應用程式的其他輔助方法,或自訂渲染原語 (例如,針對 HTML)。

範本引擎透過在 TemplateConfiguration 中設定替代 baseTemplateClass 來提供此功能

config.setBaseTemplateClass(MyTemplate.class);

自訂基礎類別必須像此範例一樣延伸 BaseClass

public abstract class MyTemplate extends BaseTemplate {
    private List<Module> modules
    public MyTemplate(
            final MarkupTemplateEngine templateEngine,
            final Map model,
            final Map<String, String> modelTypes,
            final TemplateConfiguration configuration) {
        super(templateEngine, model, modelTypes, configuration)
    }

    List<Module> getModules() {
        return modules
    }

    void setModules(final List<Module> modules) {
        this.modules = modules
    }

    boolean hasModule(String name) {
        modules?.any { it.name == name }
    }
}

此範例顯示一個類別,提供名為 hasModule 的其他方法,然後可以在範本中直接使用

if (hasModule('foo')) {
    p 'Found module [foo]'
} else {
    p 'Module [foo] not found'
}

7.3. 類型檢查範本

7.3.1. 選擇性類型檢查

即使範本未進行類型檢查,它們仍會進行靜態編譯。這表示一旦編譯範本,效能應該非常好。對於某些應用程式,在實際渲染範本之前,確定範本有效可能會很好。這表示如果模型變數的方法不存在,則範本編譯失敗。

MarkupTemplateEngine 提供此類功能。範本可以選擇性進行類型檢查。為此,開發人員必須在建立範本時提供其他資訊,也就是模型中找到的變數類型。假設一個模型公開一頁清單,其中一頁定義為

Page.groovy
public class Page {

    Long id
    String title
    String body
}

然後可以在模型中公開一頁清單,如下所示

Page p = new Page();
p.setTitle("Sample page");
p.setBody("Page body");
List<Page> pages = new LinkedList<>();
pages.add(p);
model = new HashMap<String,Object>();
model.put("pages", pages);

範本可以輕鬆使用它

pages.each { page ->                    (1)
    p("Page title: $page.title")        (2)
    p(page.text)                        (3)
}
1 從模型中反覆運算頁面
2 page.title 有效
3 page.text 無效(應為 page.body

在沒有類型檢查的情況下,範本編譯會成功,因為範本引擎在實際呈現頁面之前並不知道模型。這表示問題只會在執行階段浮現,也就是在頁面呈現之後

執行階段錯誤
No such property: text

在某些情況下,這可能會很複雜,難以解決,甚至難以察覺。透過向範本引擎宣告 pages 的類型,我們現在可以在編譯階段失敗

modelTypes = new HashMap<String,String>();                                          (1)
modelTypes.put("pages", "List<Page>");                                              (2)
Template template = engine.createTypeCheckedModelTemplate("main.tpl", modelTypes)   (3)
1 建立一個用來儲存模型類型的映射
2 宣告 pages 變數的類型(注意類型使用字串)
3 使用 createTypeCheckedModelTemplate 取代 createTemplate

這時,當範本在最後一行編譯時,會發生錯誤

範本編譯階段錯誤
[Static type checking] - No such property: text for class: Page

這表示您不需要等到頁面呈現才能看到錯誤。使用 createTypeCheckedModelTemplate 是必要的。

7.3.2. 類型的替代宣告

或者,如果開發人員也是撰寫範本的人,則可以直接在範本中宣告預期變數的類型。在這種情況下,即使您呼叫 createTemplate,它也會進行類型檢查

類型的內嵌宣告
modelTypes = {                          (1)
    List<Page> pages                    (2)
}

pages.each { page ->
    p("Page title: $page.title")
    p(page.text)
}
1 類型需要在 modelTypes 標頭中宣告
2 為模型中的每個物件宣告一個變數

7.3.3. 已類型檢查範本的效能

使用已類型檢查模型的另一個好處是效能應該會提升。透過告訴類型檢查器預期的類型,您也讓編譯器為此產生最佳化的程式碼,因此如果您正在尋找最佳效能,請考慮使用已類型檢查範本。

8. 其他解決方案

此外,還有其他可以與 Groovy 搭配使用的範本解決方案,例如 FreeMarkerVelocityStringTemplate 等。