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

2023-11-15

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用Kotlin做一个简单的HTML构造器 的相关文章

随机推荐

  • 移动端点击(click)事件延迟问题的解决方法

    移动端 click 事件会有 300ms 的延时 原因是移动端屏幕双击会缩放 double tap to zoom 页面 解决方案 1 禁用缩放 浏览器禁用默认的双击缩放行为并且去掉300ms 的点击延迟 2 利用touch事件自己封装这个
  • (mac)配置vue

    安装参考 https www jianshu com p cc722eba1f46 1 安装brew 一个安装 卸载软件的程序 https blog csdn net poppy rain article details 88406390
  • Java面试题第一季学习笔记

    Java面试题第一季 1 自增变量 2 单例设计 2 1 什么是Singleton 2 2 代码示例 3 类初始化 3 1 代码 3 2 考点 3 3 Override 重写 和Overload 重载 区别 4 方法的传递机制 4 1 代码
  • java 反射中的method.invoke()方法详解

    public class TestReflect public static void main String args String names tom tim allen alice Class
  • java关于ArrayList,Vector,LinkedList,Set及其面试题+LeetCode136两种方式实现

    ArrayList ArrayList的遍历补充 将list转换为数组 使用toArray 方法将列表转换为数组 再对数组进行遍历 Test void test01 List
  • vue3实现鼠标左键拖拽画矩形框框选功能

    vue3 elementuiPlus 实现鼠标左键拖拽画矩形框 框选列表功能 仿照桌面框选功能 效果如图 vue3鼠标框选 代码
  • Hibernate的核心配置

    Hibernate的设计思路 Hibernate是一种全自动化管理持久化对象的ORM框架 既提供了完全面向对象的封装完整的对象持久化接口 屏蔽db层的差异化 提升代码可移植性 也提供了操作HQL和SQL的半自动化DB访问接口 提供复杂查询的
  • JSVC的配置与使用详解

    JSVC是apache出的所谓common daemon的一个工具套件 他利用一个daemon程序 从而使tomcat这样的程序能在开机的时候自动启动 而且能使tomcat被 chkconfig这样的工具所管理 在之前的一篇文章中对jsvc
  • 算法岗面试问题总结(二)

    文章目录 1 SVM的loss是啥 2 kmeans聚类如何选择初始点 3 RF和GBDT谁更容易过拟合 偏差和方差 4 xgb的分类树也是用残差吗 不是的话是什么 5 讲讲数据倾斜怎么处理 6 请你说说SVM的优缺点 7 LR和SVM的联
  • C++中的typeInfo用法总结

    最近在做测试 在大型程序中 模板类型加上继承关系搞得我混乱 还好有tpyeinfo帮助捋顺关系 typeInfo与typeid简单总结说明 和sizeof这类的操作符一样 typeid是C 的关键字之一 typeid操作符的返回结果是名为t
  • jenkins学习笔记第十六篇 jenkins权限控制

    创建用户 对用户进行权限控制 在实际项目中 根据不同的用户 大致可分为 测试用户 开发用户 运维用户等 这时就需要给不同的用户赋予不能的权限 首选需要安装插件 Role based Authorization Strategy 这个插件主要
  • ctfshow-WEB-web14( 利用数据库读写功能读取网站敏感文件)

    ctf show WEB模块第14关是一个SQL注入漏洞 绕过switch循环后可以拿到一个登录界面 登录界面存在SQL注入 脱库以后会提示flag在另一个文件中 利用数据库的文件读写功能读取文件内容即可拿到flag 开局是一个switch
  • 智能优化算法之遗传算法(GA)的实现(基于二进制编码,Python附源码)

    文章目录 一 遗传算法的实现思路 二 基于二进制编码方式的遗传算法的实现 1 库的导入 2 目标函数 3 个体编码函数 4 个体解码函数 5 选择函数 6 交叉函数 7 变异函数 8 算法主流程 一 遗传算法的实现思路 遗传算法 Genet
  • Android4种网络连接方式HttpClient、HttpURLConnection、OKHttp和Volley优缺点和性能对比

    比较的指标 1 cpu 2 流量 3 电量 4 内存占用 5 联网时间 功能点 1 重试机制 2 提供的扩展功能 3 易用性 4 是否https 5 是否支持reflect api OkHttp有配套方法 6 缓存 重试 7 cookie支
  • Python音视频开发:消除抖音短视频Logo和去电视台标的实现详解

    前往老猿Python博文目录 一 引言 对于带Logo 如抖音Logo 电视台标 的视频 有三种方案进行Logo消除 直接将对应区域用对应图像替换 直接将对应区域模糊化 通过变换将要去除部分进行填充 其中 方法1又可以使用三种方法 一是使用
  • C++之vector深度剖析

    C 之vector深度剖析 1 vector的介绍及使用 1 1 vector的介绍 1 2 vector的使用 1 2 1 vector的定义 1 2 2 vector iterator 的使用 1 2 3 vector 空间增长问题 1
  • python3-常用方法和函数总结(字符串)

    方法与函数的差别 调用方式 作用域 方法 对象 方法名 不释放空间 函数 函数名 自动释放空间 字符串常用函数与方法 函数 方法 说明 举例 capitalize 方法 将首字母变为大写 非首字母变为小写 str hEllo print s
  • UE4 DDC共享

    本人用的是源码引擎编译的 内网使用DDC 先创建一个共享文件夹 这个文件夹来保存共享资源 修改引擎的baseengine ini cpp DerivedDataBackendGraph Shared Type FileSystem Read
  • 栈内存与堆内存的区别

    栈内存和堆内存都是计算机中用于存储数据的内存区域 它们之间的主要区别在于 存储方式 栈内存采用 先进后出 的存储方式 而堆内存采用随机存储的方式 空间大小 栈内存的空间大小通常比堆内存的空间大小要小得多 生命周期 栈内存的生命周期比堆内存的
  • 使用Kotlin做一个简单的HTML构造器

    最近在学习Kotlin 看到了Kotlin Koans上面有一个HTML构造器的例子很有趣 今天来为大家介绍一下 最后实现的效果类似Groovy 标记模板或者Gradle脚本 就像下面 这是一个Groovy标记模板 这样的 html lan