天天看點

使用Kotlin做一個簡單的HTML構造器

最近在學習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腳本