模版引擎,對現在的 Web 開發極為重要,幾乎所有主流 Web 框架都會支援一種或多種模版引擎,模版引擎可以分離使用者介面和業務邏輯,工作原理主要是一種翻譯,後端對特定的標記、語法、變數等渲染後再輸送給瀏覽器,如今模版引擎已經非常強大,在相關領域可以幫助開發者節約很多時間精力,比如快取、設計分層、外掛化。不同的模版引擎千變萬化,各種語言也是層出不窮,比如 PHP 模版引擎中的老大哥 Smarty,Python 的 Jinja2,也是 Flask 中內建的模版引擎,如今前端也有新生模版引擎,依賴前端的效能提升,像後端一樣處理模版語言渲染資料。
Leaf 作為 Vapor 官方提供的元件之一原生整合在 Vapor 中,Leaf 模版檔案以 .leaf
結尾,模版語法夾雜在 HTML 之間,我們可以直接使用而不需要引入其他外部依賴。
渲染
我們可以在路由中進行模版的渲染,同時附帶給模版傳引數,引數以 Dict 的形式放在第二個位置,然後在 .leaf
模版檔案中就能拿到對應的資料。
drop.get { req in
return try drop.view.make("index.leaf", [`greeting`: "Hello world!"])
}複製程式碼
標記 (#)
Leaf 使用 #
作為模版語法的標記,筆者也很苦惱為啥 Vapor 的作者要用井號做語法標記,在 Github 也有人提出 #
會與 HTML/CSS
有衝突(issue #11)。
我們可以用 #()
來輸出 HTML, 這裡需要注意的是這樣輸出 Leaf 會對其中的 HTML 進行轉義,舉個例子,如下程式碼輸出到瀏覽器,HTML 原始碼真是內容其實是:hello
,並沒有被 A 標籤包起來。
#(<a>hello</a>)複製程式碼
如果想輸出原始 HTML 怎麼辦呢?我們可以用 raw() {}
來輸出不會被轉移的 HTML,也就是說到瀏覽器會被解析成對應的 HTML 程式碼,如下會在瀏覽器輸出:<a>hello</a>
。
#raw() { <a>hello</a> }複製程式碼
需要注意的是不要讓使用者輸入內容用
#raw(){ }
進行原始輸出,比如評論,以免造成惡意程式碼執行的漏洞!
變數
#(variable)複製程式碼
比較
#equal(leaf, leaf)複製程式碼
判斷
下面這段程式碼邏輯就是 if:elif:else
#if(entering) {
Hello, there!
} ##if(leaving) {
Goodbye!
} ##else() {
I`ve been here the whole time.
}複製程式碼
迴圈
#loop(users, "user") {
Hello, #(user.name)! </br >
}複製程式碼
基本迴圈用 #loop
,第一個引數傳入陣列,第二個引數寫出迴圈內部的例項變數名,假設我的 users
內容是 [“Objective-C”、”Swift”、”Vapor”],輸出內容在瀏覽器看起來就會是這個樣子:
Hello, Objective-C!
Hello, Swift!
Hello, Vapor!
模組化
我們構建一些多頁面 Web 程式,常常會有每個頁面或者一種頁面部分相似的內容,比如網站的頭部、腳部,頭部一般用來放導航欄,腳部放一些版權宣告、三方連結等等,那麼在整個網站任何頁面中這兩部分的內容基本都是一致的,那麼我們可以把這兩部分的內容抽出一個單獨的 .leaf
檔案存放,並在需要的地方引入進來就可以了。
Leaf 提供了四個方法來引入/包含其他模版:
- Import:
#import("template")
- Export:
#export("template") { Leaf/HTML }
- Extend:
#extend("template")
- Embed:
#embed("template")
這四個標籤都不需要補全
.leaf
字尾,Leaf 會自動查詢對應檔案。
#import()
用來宣告一個插入點在當前模版。
#extend()
用來繼承一個模版,使用模版的內容,然後就只能用 #export()
填充之前宣告的插入點。
#embed("body")
才是用來引入一個模版的內容到當前位置。
舉個例子 ?:
/// base.leaf
<html>#import("html-content")</html>
/// index.leaf
#extend("base")
#export("html-content") {
Hello
}複製程式碼
渲染 index.leaf
的內容如下: <html>Hello</html>
,index.leaf
頁繼承了 base.leaf
的內容, 並把 Hello
這個字串被插入到了 <html>
標記中間。注意之前的描述,繼承之後只能使用 #export()
填充,也就是在 index.leaf
檔案中其他地方任何內容將不會被渲染。
然後是 #embed()
的用法,可以直接引入另一個檔案:
/// html-content.leaf
Hello
/// index.leaf
<html>#embed("html-content")</html>複製程式碼
上面程式碼渲染 index.leaf
後的結果和之前是一樣的。
自定義標籤
有時候官方提供的標籤功能不夠我們使用,當然 Leaf 給我們留出了自定義的空間,我們可以宣告自己的標籤然後在 .leaf
模版種使用,就像 #equal()
、#import()
一樣。
我們可以通過宣告一個類來自定義一種標籤,借用一下官方的例子,定義在 Leaf/Tag/Models/Index.swift:,該程式碼定義了一個方法用來獲取陣列中指定下標的一個元素:
class Index: BasicTag {
let name = "index1"
func run(arguments: [Argument]) throws -> Node? {
guard
arguments.count == 2,
let array = arguments[0].value?.nodeArray,
let index = arguments[1].value?.int,
index < array.count
else { return nil }
return array[index]
}
}複製程式碼
然後再向 droplet
註冊它:
if let leaf = drop.view as? LeafRenderer {
leaf.stem.register(Index())
}複製程式碼
有了自定義標籤的功能,我們可以豐富很多自己的功能邏輯,甚至定義一個快速解析 Markdown 的標籤,用起來會相當方便。
語法高亮
根據 Vapor 官方提示,VSCode 和 Atom 有對應的語法高亮外掛,如有需要可以試試。
- Visual Studio Code – html-leaf
- Atom – language-leaf
- Xcode – 目前沒有很好支援的外掛,不過可以設定 Xcode 編輯器語法高亮為 HTML,稍會有改善
結語
另外如果你喜歡類似於 Django 和 Mustache 式的語法,可以看看 Stencil,也是一個純 Swift 寫的模版引擎。
這是 [Swift Web 開發之 Vapor] 系列的第三篇,說了說 Vapor 中自帶的 Leaf 模版引擎,按照筆者目前的使用情況來看其實 Leaf 還不太成熟,雖然還有太多需要優化改進的地方,不過我相信之後一定會越來越好的。所以不要害怕,趕緊來寫 Swift Server Side 吧!
之前開的坑在用 Vapor 寫一個部落格程式 NSPress,如果大家有興趣歡迎討論。