最近在学习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脚本。