最近在學習Kotlin,看到了Kotlin Koans上面有一個HTML構造器的例子很有趣。今天來為大家介紹一下。最後實作的效果類似Groovy 标記模闆或者Gradle腳本,就像下面(這是一個Groovy标記模闆)這樣的。
html(lang:'en') {
head {
meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"')
title('My page')
}
body {
p('This is an example of HTML contents')
}
}
基礎文法
HTML構造器主要依靠Kotlin靈活的lambda文法。是以我們先來學習一下Kotlin的lambda表達式。如果學習過函數式程式設計的話,對lambda表達式應該很熟悉了。
首先,Kotlin中的lambda表達式可以賦給一個變量,然後我們可以“調用”該變量。這時候lambda表達式需要大括号包圍起來。
val lambda = { a: String -> println(a) }
lambda("lambda表達式")
lambda表達式還可以用作函數參數。
fun doSomething(name: String, func: (e: String) -> Unit) {
func(name)
}
Kotlin的lambda表達式還有一項特性,指定接收器。文法就是在lambda表達式的括号前添加接收器和點号
.
。在指定了接收器的lambda表達式内部,我們可以直接調用接收器對象上的任意方法,不需要額外的字首。
fun buildString(build: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.build()
return sb.toString()
}
然後我們就可以用非常簡潔的文法來建立字元串了。需要注意這裡的大括号中包圍起來的是lambda表達式,它是buildString函數的參數而非函數體。這一點非常重要,在後面了解HTML構造器的時候,我們需要明确這一點。
val str = buildString {
for (i in 1..9) append("$i ")
toString()
}
Kotlin提供了一個apply函數,它的作用是直接調用給定的lambda表達式。上面這個例子使用apply方法改寫如下。
fun buildStringWithApply() {
val str = StringBuilder().apply {
for (i in 1..9) append(i)
toString()
}
println("字元串構造結果是:$str")
}
構造HTML
在了解了Kotlin的lambda文法之後,我們就可以建立HTML構造器了。
首先我們建立屬性類、标簽類和文本類。屬性類包含屬性名稱和值,并重寫了toString方法以便輸出類似
name="value"
這樣的字元串。标簽類則是HTML标簽的抽象,包括一組屬性和子标簽。這裡屬性和子标簽都聲明為了
MutableList
類型,它是Kotlin類庫中的可變清單,存儲内容是可以修改的。最後的文本類非常簡單,直接傳回文本。
class Attribute(var name: String, var value: String) {
override fun toString(): String {
return """$name="$value" """
}
}
open class Tag(var name: String) {
val children: MutableList<Tag> = ArrayList()
val attributes: MutableList<Attribute> = ArrayList()
override fun toString(): String {
return """<$name${if (attributes.isEmpty()) "" else attributes.joinToString(prefix = " ", separator = " ")}>
${if (children.isEmpty()) "" else children.joinToString(separator = "\n")}
</$name> """
}
}
class Text(val text: String) : Tag("") {
override fun toString(): String = text
}
僅僅有這幾個類并不夠。我們還需要針對HTML實作一些具體的類。這些類非常簡單,繼承Tag類即可。這些類裡面有一個類比較特殊,它就是TableElement。這個類同時是Thead和Tbody的父類。它的作用在下面會提到。
class Html : Tag("html")
class Body : Tag("body")
class Head : Tag("head")
class Script : Tag("script")
class H1 : Tag("h1")
class Table : Tag("table")
open class TableElement(name: String) : Tag(name)
class Thead : TableElement("thead")
class Tbody : TableElement("tbody")
class Th : Tag("th")
class Tr : Tag("tr")
class Td : Tag("td")
class P : Tag("p")
然後我們需要幾個工具函數。doInit函數接受一個标簽和一個lambda表達式,作用是調用該lambda表達式并将給定的标簽添加到子标簽清單中,傳回的仍然是這個标簽,友善後面鍊式調用。set函數更簡單了,直接使用參數給定的名稱和值設定标簽的屬性,傳回值也是标簽以便鍊式調用。這兩個工具方法這麼寫的原因,等到我們完成了這個例子,實際顯示效果的時候就可以看到了。
fun <T : Tag> Tag.doInit(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
fun <T : Tag> T.set(name: String, value: String?): T {
if (value != null) {
attributes.add(Attribute(name, value))
}
return this
}
最後是一組擴充方法。大部分方法都相同,我們先看看html方法 。它接受一個額外參數lang,作為html标簽的屬性;另一個參數是lambda表達式,由apply方法調用來初始化。由于我們的工具方法傳回标簽本身,是以這裡可以鍊式調用多個方法。
剩下的方法基本一樣,我們以table方法為例。table方法是Body上的擴充方法,也就是說table方法隻能在Body上調用。table方法上的lambda表達式使用Table類作為接收器
init: Table.() -> Unit
。這裡接收器的類型實際上就是init參數lambda表達式的上下文。doInit工具方法中,子元素被添加到的标簽正是這裡定義的上下文。因為tr标簽既可以在thead标簽中使用,也可以在tbody标簽中使用。是以我們需要添加一個TableElement類,讓這兩個類繼承它。這樣HTML标簽才能正常生成。
fun html(lang: String = "en", init: Html.() -> Unit): Html = Html().apply(init).set("lang", lang)
fun Html.head(init: Head.() -> Unit) = doInit(Head(), init)
fun Html.body(init: Body.() -> Unit) = doInit(Body(), init)
fun Body.h1(init: H1.() -> Unit) = doInit(H1(), init)
fun Head.script(init: Script.() -> Unit) = doInit(Script(), init)
fun Body.p(init: P.() -> Unit) = doInit(P(), init)
fun Table.thead(init: Thead.() -> Unit) = doInit(Thead(), init)
fun Table.tbody(init: Tbody.() -> Unit) = doInit(Tbody(), init)
fun Body.table(init: Table.() -> Unit) = doInit(Table(), init)
fun TableElement.tr(init: Tr.() -> Unit) = doInit(Tr(), init)
fun Tr.th(init: Th.() -> Unit) = doInit(Th(), init)
fun Tr.td(init: Td.() -> Unit) = doInit(Td(), init)
fun Tag.text(s: Any?) = doInit(Text(s.toString()), {})
到此為止HTML構造器已經準備就緒了。我們來實際看看效果。可以看到這裡的文法非常奇怪,甚至都不像代碼,但是它确确實實是标準的Kotlin代碼。
val text = html(lang = "zh") {
head {
script {
text("alert('123')")
}
}
body {
h1 {
text("Hello")
}
table {
thead {
tr {
th { text("name") }
th { text("age") }
}
}
tbody {
tr {
td { text("yitian") }
td { text("24") }
}
tr {
td { text("liu6") }
td { text("16") }
}
}
}
p {
text("This is some words")
}
}
}
println("html構造器使用執行個體:\n$text")
其實也很好了解,隻要我們為這些方法添加小括号即可。
html({
head({.......)}
body({.......)}
})
這隻是一個小例子。如果技術夠硬的話,你甚至可以自己做一個腳本語言或者其他什麼東西。當然現在已經有項目開始使用這種文法了,例如
Kara Web架構視圖以及
用Kotlin寫Gradle腳本。