Freemarker 教程(一)-模板開發手冊

本文是Freemarker系列的第一篇,面向模板開發人員,主要介紹 FreeMarker 所使用的 FTL(FreeMarker Template Language) 語法,瞭解 Freemarker 的基本概念,介紹基本的 FTL 術語 及內置函數,內置指令,方便做爲開發手冊速查(文中演示所用版本爲 2.3.30,實際使用中請根據本身項目版本自查官網)。html

本文不會羅列官網API,只在必要時演示其語法,代碼工程中有課表明整理的 freemarker api 思惟導圖,配合此文食用可以使功力大增!請到 課表明的 github自取。java

1.FreeMarker 是什麼

Freemarker是一款純 Java編寫的模板引擎軟件,能夠用來生成各類文本,包括但不限於:HTMLE-Mail以及各類源代碼等等。python

它的主要任務就是:把模板和數據組裝在一塊兒,生成文檔,這個過程又叫渲染(Render)。流程如圖:git

freemarker-overview.png

因爲大部分模板開發人員都是用它來生成HTML頁面,因此本文將基於 SpringBoot(2.4.1)+Freemarker(2.3.30)+SpringWeb演示 HTML 頁面的渲染github

2.最簡單的模板

假設我想要一個簡單頁面用來歡迎當前用戶,模板代碼:spring

<html>
<head>
  <title>index</title>
</head>
<body>
    <p>你好,${userName}</p>
</body>
</html>

${userName}是 FTL 的插值語法,他會把userName的值替換到生成的 HTML 中,從而根據當前登陸者,顯示不一樣的用戶名,這個值由後端代碼放到Model中,對應的 Controlelr 代碼:apache

@Controller
public class HelloWorld {
    @GetMapping("hello")
    public String hello(Model model) {
        model.addAttribute("userName","Java 課表明");
        // 返回模板名稱
        return "index";
    }
}

訪問頁面:json

freemarker-hello.png
數據由後端代碼經過數據模型(Model)傳遞,模板只關心數據如何展現(View),兩者的關聯關係由 Controller 來控制,這就是 MVC。segmentfault

3.數據模型(data-model)

Controller中添加到 model 中的數據是如何組織的呢?這就須要瞭解一下FTL的數據模型(data-model)。後端

FTL 的數據模型在結構上是一個樹形:

(root)
  |
  +- animals
  |   |
  |   +- mouse
  |   |   |
  |   |   +- size = "small"
  |   |   |
  |   |   +- price = 50
  |   |
  |   +- elephant
  |   |   |
  |   |   +- size = "large"
  |   |   |
  |   |   +- price = 5000
  |   |
  |   +- python
  |       |
  |       +- size = "medium"
  |       |
  |       +- price = 4999
  |
  +- message = "It is a test"
  |
  +- misc
      |
      +- foo = "Something"

其中的root 能夠理解爲 Controller 中的 model ,經過 model.addAttribute("userName","Java 課表明");就能夠往數據模型中添加數據。

數據模型中能夠像目錄同樣展開的變量,如:root, animals, mouse, elephant, python, misc稱之爲哈希(hash)。哈希的 key 就是變量名,value 就是變量存儲的值,經過.分隔的路徑能夠訪問變量值,好比訪問 mouse 的 price :animals.mouse.price.

animals.mouse.price這樣存儲單個值的變量叫作標量(scalar),標量有四種具體類型:string,boolean,date-like,number;

還有一種變量叫作序列(sequence),能夠類比爲 Java 中的數組,序列中的每一個項沒有名字,能夠經過遍歷,或者下標的方式訪問(後面會演示序列的訪問),它的數據結構看起來是這樣的:

(root)
  |
  +- animals
  |   |
  |   +- (1st)
  |   |   |
  |   |   +- name = "mouse"
  |   |   |
  |   |   +- size = "small"
  |   |   |
  |   |   +- price = 50
  |   |
  |   +- (2nd)
  |   |   |
  |   |   +- name = "elephant"
  |   |   |
  |   |   +- size = "large"
  |   |   |
  |   |   +- price = 5000
  |   |
  |   +- (3rd)
  |       |
  |       +- name = "python"
  |       |
  |       +- size = "medium"
  |       |
  |       +- price = 4999
  |
  +- misc
      |
      +- fruits
          |
          +- (1st) = "orange"
          |
          +- (2nd) = "banana"

FTL 裏經常使用的數據類型就這三類:哈希(hashe),標量(scalar),序列(sequence)。

有了數據,還要有語法來組織這些數據,下面介紹 FTL 中的經常使用語法。

4.FTL 語法

FreeMarker 只認以下三種語法:

  1. 插值:${...} ,Freemarker 會將裏面的變量替換爲實際值
  2. FTL 標籤(tags):結構上相似HTML的標籤,都是用<>包裹起來,普通標籤以<#開頭,用戶自定義標籤以<@開頭,如<#if true>true thing<#/if><@myDirect></@myDirect>
你會看到兩種叫法,1:標籤(tags),2:指令(directive)。舉個例子: <#if></#if>叫標籤; 標籤裏面的 if是指令,能夠類比於html中的標籤(如: <table></table>)和元素(如: table)。不過,把標籤和指令認爲是 同義詞也沒有問題。
  1. 註釋(Comments):FTL 中的註釋是這樣的:<#-- 被註釋掉的內容 -->,對於註釋,FTL會自動跳過,因此不會顯示在生成的文本中(這點有別於 HTML 的註釋)
注意:除以上三種語法以外的全部內容,皆被 FreeMarker 視爲普通文本,普通文本會被原樣輸出

插值就是單純的替換變量的值,註釋更沒啥好說的,下面主要介紹幾個最經常使用的 FTL 標籤(指令)並結合代碼演示其用法。

if 指令

if 能夠根據條件跳過模板中的某塊代碼,之前文爲例,當 userName 值爲 "Java課表明" 或zhengxl5566時,用特殊樣式展現,相關模板代碼以下:

<p>你好,
    <#if userName == "Java 課表明">
        <strong>${userName}</strong>
    <#elseif userName == "zhengxl5566">
        <h1>${userName}</h1>
    <#else>
        ${userName}
    </#if>
</p>

list 指令

list用來遍歷序列,其語法爲:

<#list sequence as loopVariable>
    repeatThis
</#list>

好比後臺往 model 裏放入一個 allUsers 的集合

model.addAttribute("allUsers",userService.getAllUser());

能夠直接使用下標訪問集合中的某個元素:${allUsers[0].name}

也能夠在模板中直接遍歷展現:

<ol>
<#list allUsers as user>
    <li>
        姓名:${user.name},年齡:${user.age}
    </li>
</#list>
</ol>

實際渲染出來的 HTML:

<ol>
  <li>
    姓名:zxl,年齡:18
  </li>
  <li>
    姓名:ls,年齡:19
  </li>
  <li>
    姓名:zs,年齡:16
  </li>
</ol>
注意:假設 allUsers 是空的,渲染出來的頁面會是 <ol></ol>,若是須要規避這個狀況,可使用 items 標籤
<#list allUsers>
    <ol>
        <#items as user>
            <li>
                姓名:${user.name},年齡:${user.age}
            </li>
        </#items>
    </ol>
</#list>

此時,假設 allUsers是空的,list 標籤中的 html 內容就不會被渲染出來。

include 指令

include 指令能夠把一個模板的內容插入到另外一個模板中(官方建議使用 import 代替,參見下文的最佳實踐)。
假設咱們每一個頁面都須要一個 footer,能夠寫一個公共的footer.ftlh模板,其他須要footer的頁面只須要引用footer.ftlh模板便可:

<#include "footer.ftlh">

import 指令

import 能夠將模板中定義的變量引入當前模板,並在當前模板中使用。它和 include 的主要區別就是 import 能夠將變量封裝到新的命名空間中(後文會介紹 import 和 include 的對比)。

例如:模板 /libs/commons.ftl 裏面寫了不少公共方法,想在其餘模板裏引用,只須要在其餘模板的開頭寫上:

<#import "/libs/commons.ftl" as com>

後續想使用/libs/commons.ftl 中的 copyright 方法,能夠直接使用:

<@com.copyright date="1999-2002"/>

assign 指令

assign 能夠用來建立新的變量併爲其賦值,語法以下:

<#assign name1=value1 name2=value2 ... nameN=valueN>
or
<#assign name1=value1 name2=value2 ... nameN=valueN in namespacehash>
or
<#assign name>
  capture this
</#assign>
or
<#assign name in namespacehash>
  capture this
</#assign>

舉例:

<#--建立字符串-->
<#assign myStr = "Java 課表明">
<#--使用插值語法顯示字符串-->
myStr:${myStr}

macro 指令

macro 用來從模板上建立用戶自定義指令(Java後端能夠經過實現TemplateDirectiveModel接口自定義指令,將在下一篇:《Freemarker 教程(二)-後端開發指南》中介紹)

macro 建立的也是變量,該變量能夠作爲用戶自定義指令使用,好比下面的模板定義了 greet指令:

<#macro greet>
  <h1>hello 課表明</h1>
</#macro>

使用 greet 指令

<@greet></@greet>
或者
<@greet/>

指令還能夠附帶參數:

<#macro greet person>
  <h1>hello ${person}</h1>
</#macro>

使用時傳入 person 變量:

<@greet person="Java課表明"/> and <@greet person="zhengxl5566"/>

5.內置函數(Built-ins)

所謂內置函數,就是 FreeMarker 針對不一樣數 據類型爲咱們提供的一些內置方法,有了這些方法,可讓數據在模板中的展現更加方便。使用內置函數時,只須要在變量後面使用?加相應函數名便可。篇幅有限,這裏不打算羅列全部內置函數,只挑幾個簡單例子展現其語法。

例子一:字符串的內置函數

<#--建立字符串-->
<#assign myStr = "Java 課表明">
<#--首字母小寫-->
${myStr?uncap_first}
<#--保留指定字符後面的字符串-->
${myStr?keep_after("Java")}
<#--替換指定字符-->
${myStr?replace("Java","Freemarker")}

例子二:時間類型內置函數

<#--獲取當前時間(若是是後端將時間傳入data-model,只須要傳Date類型便可)-->
<#assign currentDateTime = .now>
<#--展現日期部分-->
${currentDateTime?date}<br>
<#--展現時間部分-->
${currentDateTime?time}<br>
<#--展現日期和時間部分-->
${currentDateTime?datetime}<br>
<#--按指定格式展現時間日期-->
${currentDateTime?string("yyyy-MM-dd HH:mm a")}<br>

例子三:序列的內置函數

<#--序列類型內置函數樣例-->
<#assign mySequence = ["Java 課表明","張三","李四","王五"]>
<#--將全部元素以指定分隔符分割,輸出字符串-->
${mySequence?join(",")}<br>
<#--取序列中的第一個元素-->
${mySequence?first}<br>
<#--將序列排序後使用逗號分割輸出爲字符串-->
${mySequence?sort?join(",")}<br>

經過以上三個例子的簡單演示,相信你已經能掌握內置函數的使用技巧了,就是在變量後面用 ?加變量數據類型所支持的函數。FTL 的內置函數極其豐富,官網按數據類型詳細羅列了各自支持的內置函數及其用法,可自行查看官網的內置函數參考

爲了方便你們快速查閱相關內置函數(built-ins)和指令(directives),課表明從官網翻譯,並使用 xmind 作了個思惟導圖,每一個函數(指令)均可以點進去查看功能描述和樣例,能夠極大提升模板開發效率:

freemarker-xmind.png

原始 xmind 文件放在 課表明的github上,讀者能夠按需自取。

課表明劃重點!這個思惟導圖是全文精華,必定要下載下來看看!

6.命名空間(Namespaces)

所謂命名空間,就是在同一個模板裏,全部使用 assign,macro,function 指令所建立的變量集合,它的主要做用就是惟一標識一個變量。

有兩種方式能夠建立命名空間:

一、同一個模板中的變量在同一個命名空間中。

以以下的index.ftlh爲例,這裏面建立的變量都在一個命名空間下,同名變量的值會相符覆蓋

<#assign myName = "Java 課表明">
<#assign myName = "課表明">
<#--實際輸出的是「課表明」-->
${myName}

二、不一樣模板中的變量能夠經過 import 指令來區分不一樣的命名空間變量

模板A中想使用模板B中的變量,可使用 import 指令,給引入的模板定義一個新的命名空間,經過 as 後面指定的 key 訪問該新命名空間。

好比模板 lib/example.ftlh中定義了copyright

<#macro copyright date>
  <p>Copyright (C) ${date} Someone. All rights reserved.</p>
</#macro>

<#assign mail = "user@example.com">

在另外一個模板index.ftlh中使用copyright

<#import "lib/example.ftlh" as e>
<@e.copyright date="1999-2002"/>
${e.mail}

命名空間的生命週期(The life-cycle of namespaces)

命名空間由 import 指令中的 path 肯定(絕對路徑),若是相同的 path 引入了屢次,只有第一次調用 import 的時候纔會觸發相應命名空間的建立。後面對於相同模板路徑的 import,指代的都是同一個命名空間,舉例:

<#import "/lib/example.ftl" as e>
<#import "/lib/example.ftl" as e2>
<#import "/lib/example.ftl" as e3>
${e.mail}, ${e2.mail}, ${e3.mail}
<#assign mail="other@example.com" in e>
${e.mail}, ${e2.mail}, ${e3.mail}

/lib/example.ftl import 後賦給 e,e2,e3三個命名空間,修改e中的 mail, 對e2,e3一樣生效

輸出:

user@example.com, user@example.com, user@example.com
other@example.com, other@example.com, other@example.com

import 和 include 的區別

<#import "lib/example.ftlh" as e> 會建立一個全新的命名空間,並把lib/example.ftlh中定義的變量封裝到新命名空間中,提供訪問,如:<@e.copyright date="1999-2002"/>

<#include "lib/example.ftlh"> 只是單純把example.ftlh的內容插入到當前模板,並不會對命名空間產生影響

Freemarker官方建議:

全部使用 include 的地方都應該被 import 替代

使用 import 好處以下:

  • import 引入的模板只會被執行一次,重複引用屢次並不會重複執行模板。而對於 include 而言,每次 include 都會執行一次模板內容;
  • import 能夠建立該模板的命名空間,引用變量時能夠清晰表達出變量來源,減小命名衝突機率;
  • 若是定義了不少通用方法,能夠將auto-import配置爲懶加載,用到哪一個加載哪一個。而auto-include沒法實現懶加載,必須全量加載;
  • import 指令不會有任何輸出,而 include 可能會根據模板內容輸出相應字符。

7.最佳實踐

1.空值處理

變量空值的處理

對於不存在的變量和值爲 null 的變量,freemarker 統一認爲是不存在的值,對其調用將會報錯。爲了不這種狀況,能夠設置默認值,示例:<h1>Welcome ${user!"visitor"}!</h1>,當user不存在時,將顯示visitor字符串。

另外一種方式是在變量後面使用 ??表達式,如:user??若是user存在,則返回 true,不然返回 false,示例:<#if user??><h1>Welcome ${user}!</h1></#if>,當user不存在時,就不展現歡迎標語了。

list中的空值處理

遍歷序列的時候,假設序列中有空值,freemarker並不會直接報錯或顯示空值,而是像更上一級做用域去搜索同名變量值,這種行爲可能會致使錯誤的輸出,舉例:

<#assign x = 20>
<#list xs as x>
  ${x!'Missing'}
</#list>

本意是當遇到序列 xs 中的空元素是顯示「missing」 字符串,但因爲 freemarker 向上查找的特性, 這裏的空值將會顯示爲 20。要關閉這個特性,能夠在服務端配置:configuration.setFallbackOnNullLoopVariable(false);

2.使用 import 代替 include

前文在介紹到 include 的時候提到過,官方建議應該將全部用到 include 的地方都用 import 實現

咱們平時用到 include 指令,主要就是用來把一段內容插入到當前模板,那如何用 import 實現 include 的功能呢?

很簡單,把須要插入的內容封裝成 自定義指令就行了。

好比咱們從common.ftlh 裏定義一個自定義指令 myFooter

<#macro myFooter>
    <hr>
    <p>這裏是 footer</p>
</#macro>

在須要使用的地方,引入 common.ftlh 並調用 myFooter 指令

<#import "lib/common.ftlh" as common>
<#--將include使用import代替-->
<#--<#include "footer.ftlh">-->
<@common.myFooter/>

3.隔行變色

數據展現的時候常常遇到使用表格展現數據的狀況,爲了增長辨識度,通常會讓奇數行和偶數行顏色不一樣以區分,這就是隔行變色

下面以遍歷序列爲例:

<#assign mySequence = ["Java 課表明","張三","李四","王五"]>
<#list mySequence as name>
    <span style="color:  ${name?item_cycle("red","blue")}">   ${name}<br/> </span>
</#list>

這裏的 item_cycle 是循環變量的內置函數,至於他的詳細用法,再次推薦你去看一下課表明整理的思惟導圖。

8.總結

本文介紹了 Freemarker 的基本概念和基礎語法,意在讓剛接觸的萌新能對 Freemarker 有個全局性認識,瞭解Freemarker的數據模型、內置函數、指令。只要能區分這幾個概念,實際開發中現用現查便可,不要一開始就迷失在海量的API中。

整體來講,Freemarker 是一個比較簡單,容易上手的模板引擎,只要掌握了本文所說起的基本概念,直接上手開發是徹底沒問題的。

在本文寫做過程當中,課表明意識到純文字表達的侷限性,又不能全文羅列和翻譯API,因而整理了一個思惟導圖,將Freemarker 的各種指令,內置函數及其官網示例全都翻譯整理了進去。在平時開發過程當中極大提高了開發效率,須要的同窗請到 課表明的 github自取。


👇關注 Java課表明,獲取最新 Java 乾貨👇
image

相關文章
相關標籤/搜索