热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

KotlinDSLforHTML实例解析

KotlinDSL,指用Kotlin写的DomainSpecificLanguage.本文通过解析官方的KotlinDSL写html的例子,来说明KotlinDSL是什么.首

Kotlin DSL for HTML实例解析

Kotlin DSL, 指用Kotlin写的Domain Specific Language.
本文通过解析官方的Kotlin DSL写html的例子, 来说明Kotlin DSL是什么.

首先是一些基础知识, 包括什么是DSL, 实现DSL利用了那些Kotlin的语法, 常用的情形和流行的库.

对html实例的解析, 没有一冲上来就展示正确答案, 而是按照分析需求, 设计, 和实现细化的步骤来逐步让解决方案变得明朗清晰.

理论基础

DSL: 领域特定语言

DSL: Domain Specific Language.
专注于一个方面而特殊设计的语言.

可以看做是封装了一套东西, 用于特定的功能, 优势是复用性和可读性的增强. -> 意思是提取了一套库吗?

不是.

DSL和简单的方法提取不同, 有可能代码的形式或者语法变了, 更接近自然语言, 更容易让人看懂.

Kotlin语言基础

做一个DSL, 改变语法, 在Kotlin中主要依靠:

  • lambda表达式.
  • 扩展方法.

三个lambda语法:

  • 如果只有一个参数, 可以用it直接表示.
  • 如果lambda表达式是函数的最后一个参数, 可以移到小括号()外面. 如果lambda是唯一的参数, 可以省略小括号().
  • lambda可以带receiver.

扩展方法.

流行的DSL使用场景

Gradle的build文件就是用DSL写的.
之前是Groovy DSL, 现在也有Kotlin DSL了.

还有Anko.
这个库包含了很多功能, UI组件, 网络, 后台任务, 数据库等.

和服务器端用的: Ktor

应用场景: Type-Safe Builders
type-safe builders指类型安全, 静态类型的builders.

这种builders就比较适合创建Kotlin DSL, 用于构建复杂的层级结构数据, 用半陈述式的方式.

官方文档举的是html的例子.
后面就对这个例子进行一个梳理和解析.

html实例解析

1 需求分析

首先明确一下我们的目标.

做一个最简单的假设, 我们期待的结果是在Kotlin代码中类似这样写:

html {
    head { }
    body { }
}

就能输出这样的文本:


  
  
  
  

发现1: 调用形式

仔细观察第一段Kotlin代码, html{}应该是一个方法调用, 只不过这个方法只有一个lambda表达式作为参数, 所以省略了().

里面的head{}body{}也是同理, 都是两个以lambda作为唯一参数的方法.

发现2: 层级关系

因为标签的层级关系, 可以理解为每个标签都负责自己包含的内容, 父标签只负责按顺序显示子标签的内容.

发现3: 调用限制

由于等标签只在标签中才有意义, 所以应该限制外部只能调用html{}方法, head{}body{}方法只有在html{}的方法体中才能调用.

发现4: 应该需要完成的

  • 如何加入和显示文字.
  • 标签可能有自己的属性.
  • 标签应该有正确的缩进.

2 设计

标签基类

因为标签看起来都是类似的, 为了代码复用, 首先设计一个抽象的标签类Tag, 包含:

  • 标签名称.
  • 一个子标签的list.
  • 一个属性列表.
  • 一个渲染方法, 负责输出本标签内容(包含标签名, 子标签和所有属性).

怎么加文字

文字比较特殊, 它不带标签符号<>, 就输出自己.
所以它的渲染方法就是输出文字本身.

可以提取出一个更加基类的接口Element, 只包含渲染方法. 这个接口的子类是TagTextElement.

有文字的标签, 如

文字元素是作为标签的一个子标签的.
这里的实现不容易自己想到, 直接看后面的实现部分揭晓答案吧.

3 实现

有了前面的心路历程, 再来看实现就能容易一些.

基类实现

首先是最基本的接口, 只包含了渲染方法:

interface Element {
    fun render(builder: StringBuilder, indent: String)
}

它的直接子类标签类:

abstract class Tag(val name: String) : Element {
    val children = arrayListOf()
    val attributes = hashMapOf()

    protected fun  initTag(tag: T, init: T.() -> Unit): T {
        tag.init()
        children.add(tag)
        return tag
    }

    override fun render(builder: StringBuilder, indent: String) {
        builder.append("$indent<$name${renderAttributes()}>\n")
        for (c in children) {
            c.render(builder, indent + "  ")
        }
        builder.append("$indent\n")
    }

    private fun renderAttributes(): String {
        val builder = StringBuilder()
        for ((attr, value) in attributes) {
            builder.append(" $attr=\"$value\"")
        }
        return builder.toString()
    }

    override fun toString(): String {
        val builder = StringBuilder()
        render(builder, "")
        return builder.toString()
    }
}

完成了自身标签名和属性的渲染, 接着遍历子标签渲染其内容. 注意这里为所有子标签加上了一层缩进.

initTag()这个方法是protected的, 供子类调用, 为自己加上子标签.

带文字的标签

带文字的标签有个抽象的基类:

abstract class TagWithText(name: String) : Tag(name) {
    operator fun String.unaryPlus() {
        children.add(TextElement(this))
    }
}

这是一个对+运算符的重载, 这个扩展方法把字符串包装成TextElement类对象, 然后加到当前标签的子标签中去.

TextElement做的事情就是渲染自己:

class TextElement(val text: String) : Element {
    override fun render(builder: StringBuilder, indent: String) {
        builder.append("$indent$text\n")
    }
}

所以, 当我们调用:

html {
    head {
        title { +"HTML encoding with Kotlin" }
    }
}

得到结果:


  
    

其中用到的Title类定义:

class Title : TagWithText("title")

通过‘+‘运算符的操作, 字符串: "HTML encoding with Kotlin"被包装成了TextElement, 他是title标签的child.

程序入口

对外的公开方法只有这一个:

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()
    html.init()
    return html
}

init参数是一个函数, 它的类型是HTML.() -> Unit. 这是一个带接收器的函数类型, 也就是说, 需要一个HTML类型的实例来调用这个函数.

这个方法实例化了一个HTML类对象, 在实例上调用传入的lambda参数, 然后返回该对象.

调用此lambda的实例会被作为this传入函数体内(this可以省略), 我们在函数体内就可以调用HTML类的成员方法了.

这样保证了外部的访问入口, 只有:

html {
    
}

通过成员函数创建内部标签.

HTML类

HTML类如下:

class HTML : TagWithText("html") {
    fun head(init: Head.() -> Unit) = initTag(Head(), init)

    fun body(init: Body.() -> Unit) = initTag(Body(), init)
}

可以看出html内部可以通过调用headbody方法创建子标签, 也可以用+来添加字符串.

这两个方法本来可以是这样:

fun head(init: Head.() -> Unit) : Head {
    val head = Head()
    head.init()
    children.add(head)
    return head
}

fun body(init: Body.() -> Unit) : Body {
    val body = Body()
    body.init()
    children.add(body)
    return body
}

由于形式类似, 所以做了泛型抽象, 被提取到了基类Tag中, 作为更加通用的方法:

protected fun  initTag(tag: T, init: T.() -> Unit): T {
    tag.init()
    children.add(tag)
    return tag
}

做的事情: 创建对象, 在其之上调用init lambda, 添加到子标签列表, 然后返回.

其他标签类的实现与之类似, 不作过多解释.

4 修Bug: 隐式receiver穿透问题

以上都写完了之后, 感觉大功告成, 但其实还有一个隐患.

我们居然可以这样写:

html {
    head {
        title { +"HTML encoding with Kotlin" }
        head { +"haha" }
    }
}

在head方法的lambda块中, html块的receiver仍然是可见的, 所以还可以调用head方法.
显式地调用是这样的:

this@html.head { +"haha" }

但是这里this@html.是可以省略的.

这段代码输出的是:


  
    haha
  
  
    
  

最内层的haha反倒是最先被加到html对象的孩子列表里.

这种穿透性太混乱了, 容易导致错误, 我们能不能限制每个大括号里只有当前的对象成员是可访问的呢? -> 可以.

为了解决这种问题, Kotlin 1.1推出了管理receiver scope的机制, 解决方法是使用@DslMarker.

html的例子, 定义注解类:

@DslMarker
annotation class HtmlTagMarker

这种被@DslMarker修饰的注解类叫做DSL marker.

然后我们只需要在基类上标注:

@HtmlTagMarker
abstract class Tag(val name: String)

所有的子类都会被认为也标记了这个marker.

加上注解之后隐式访问会编译报错:

html {
    head {
        head { } // error: a member of outer receiver
    }
    // ...
}

但是显式还是可以的:

html {
    head {
        this@html.head { } // possible
    }
    // ...
}

只有最近的receiver对象可以隐式访问.

总结

本文通过实例, 来逐步解析如何用Kotlin代码, 用半陈述式的方式写html结构, 从而看起来更加直观. 这种就叫做DSL.

Kotlin DSL通过精心的定义, 主要的目的是为了让使用者更加方便, 代码更加清晰直观.

参考

  • 官方文档: Type-Safe Builders
  • Domain-Specific Languages In Kotlin

More resources:

  • Kotlin之美——DSL篇
  • From Java Builders to Kotlin DSLs
  • Oversimplified network call using Retrofit, LiveData, Kotlin Coroutines and DSL

Kotlin DSL for HTML实例解析


推荐阅读
  • 本文将介绍网易NEC CSS框架的规范及其在实际项目中的应用。通过详细解析其分类和命名规则,探讨如何编写高效、可维护的CSS代码,并分享一些实用的学习心得。 ... [详细]
  • 通过Web界面管理Linux日志的解决方案
    本指南介绍了一种利用rsyslog、MariaDB和LogAnalyzer搭建集中式日志管理平台的方法,使用户可以通过Web界面查看和分析Linux系统的日志记录。此方案不仅适用于服务器环境,还提供了详细的步骤来确保系统的稳定性和安全性。 ... [详细]
  • 本文详细探讨了 Django 的 ORM(对象关系映射)机制,重点介绍了其如何通过 Python 元类技术实现数据库表与 Python 类的映射。此外,文章还分析了 Django 中各种字段类型的继承结构及其与数据库数据类型的对应关系。 ... [详细]
  • 创建项目:Visual Studio Online 入门指南
    本文介绍如何使用微软的 Visual Studio Online(VSO)创建和管理开发项目。作为一款基于云计算的开发平台,VSO 提供了丰富的工具和服务,简化了项目的配置和部署流程。 ... [详细]
  • 解决U盘安装系统后无法重启的问题
    本文详细探讨了运维新手常遇到的U盘安装系统后无法正常重启的问题,提供了从问题分析到具体解决方案的完整步骤。通过理解Boot Loader的工作原理和正确配置启动项,帮助用户顺利解决问题。 ... [详细]
  • 本文详细介绍超文本标记语言(HTML)的基本概念与语法结构。HTML是构建网页的核心语言,通过标记标签描述页面内容,帮助开发者创建结构化、语义化的Web页面。 ... [详细]
  • 哈密顿回路问题旨在寻找一个简单回路,该回路包含图中的每个顶点。本文将介绍如何判断给定的路径是否构成哈密顿回路。 ... [详细]
  • TechStride 网站
    TechStride 成立于2014年初,致力于互联网前沿技术、产品创意及创业内容的聚合、搜索、学习与展示。我们旨在为互联网从业者提供更高效的新技术搜索、学习、分享和产品推广平台。 ... [详细]
  • 本文将带领读者深入了解Android系统源码在手机中的实际表现,通过详细的步骤和专业的解释,帮助你更好地理解Android系统的底层运作机制。 ... [详细]
  • Qt中QSpinBox与QSlider的联动实现
    本文介绍如何在Qt框架下将QSpinBox和QSlider组件进行联动,使用户在拖动滑块或修改文本框中的数值时,两个组件能同步更新,从而提供更加直观和便捷的用户体验。 ... [详细]
  • 本文介绍了如何使用Java中的同步方法和同步代码块来实现两个线程的交替打印。一个线程负责打印1到52的数字,另一个线程负责打印A到Z的字母,确保输出顺序为12A34B...5152Z。 ... [详细]
  • 探讨了在有序数列中实现多种查询和修改操作的高效数据结构设计,主要使用线段树与平衡树(Treap)结合的方法。 ... [详细]
  • 深入理解T-SQL中的NULL与三值逻辑
    本文探讨了SQL Server中的三值逻辑,解释了谓词计算结果为TRUE、FALSE和UNKNOWN的规则。通过具体示例,详细说明了如何正确处理NULL值,并探讨了在不同约束条件下的行为。 ... [详细]
  • Redis Hash 数据结构详解
    本文详细介绍了 Redis 中的 Hash 数据类型及其常用命令。Hash 类型用于存储键值对集合,支持多种操作如插入、查询、更新和删除字段值。此外,文章还探讨了 Hash 类型在实际业务场景中的应用,并提供了优化建议。 ... [详细]
  • 本文介绍了 Winter-1-C A + B II 问题的详细解题思路和测试数据。该问题要求计算两个大整数的和,并输出结果。我们将深入探讨如何处理大整数运算,确保在给定的时间和内存限制下正确求解。 ... [详细]
author-avatar
手机用户2502858701
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有