且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

使用Kotlin做一个简单的HTML构造器

更新时间:2022-07-02 15:16:40

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