Kotlin DSL for HTML实例解析
Kotlin DSL for HTML实例解析
Kotlin DSL, 指用Kotlin写的Domain Specific Language.
本文通过解析官方的Kotlin DSL写html的例子, 来说明Kotlin DSL是什么.
首先是一些基础知识, 包括什么是DSL, 实现DSL利用了那些Kotlin的语法, 常用的情形和流行的库.
对html实例的解析, 没有一冲上来就展示正确答案, 而是按照分析需求, 设计, 和实现细化的步骤来逐步让解决方案变得明朗清晰.
本文被收录在: https://github.com/mengdd/KotlinTutorials
理论基础
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 { }
}
就能输出这样的文本:
<html>
<head>
</head>
<body>
</body>
</html>
发现1: 调用形式
仔细观察第一段Kotlin代码, html{}
应该是一个方法调用, 只不过这个方法只有一个lambda表达式作为参数, 所以省略了()
.
里面的head{}
和body{}
也是同理, 都是两个以lambda作为唯一参数的方法.
发现2: 层级关系
因为标签的层级关系, 可以理解为每个标签都负责自己包含的内容, 父标签只负责按顺序显示子标签的内容.
发现3: 调用限制
由于<head>
和<body>
等标签只在<html>
标签中才有意义, 所以应该限制外部只能调用html{}
方法, head{}
和body{}
方法只有在html{}
的方法体中才能调用.
发现4: 应该需要完成的
- 如何加入和显示文字.
- 标签可能有自己的属性.
- 标签应该有正确的缩进.
2 设计
标签基类
因为标签看起来都是类似的, 为了代码复用, 首先设计一个抽象的标签类Tag
, 包含:
- 标签名称.
- 一个子标签的list.
- 一个属性列表.
- 一个渲染方法, 负责输出本标签内容(包含标签名, 子标签和所有属性).
怎么加文字
文字比较特殊, 它不带标签符号<>
, 就输出自己.
所以它的渲染方法就是输出文字本身.
可以提取出一个更加基类的接口Element
, 只包含渲染方法. 这个接口的子类是Tag
和TextElement
.
有文字的标签, 如<title>
, 它的输出结果:
<title>
HTML encoding with Kotlin
</title>
文字元素是作为标签的一个子标签的.
这里的实现不容易自己想到, 直接看后面的实现部分揭晓答案吧.
3 实现
有了前面的心路历程, 再来看实现就能容易一些.
基类实现
首先是最基本的接口, 只包含了渲染方法:
interface Element {
fun render(builder: StringBuilder, indent: String)
}
它的直接子类标签类:
abstract class Tag(val name: String) : Element {
val children = arrayListOf<Element>()
val attributes = hashMapOf<String, String>()
protected fun <T : Element> 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</$name>\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" }
}
}
得到结果:
<html>
<head>
<title>
HTML encoding with Kotlin
</title>
</html>
其中用到的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
内部可以通过调用head
和body
方法创建子标签, 也可以用+
来添加字符串.
这两个方法本来可以是这样:
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 <T : Element> 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.
是可以省略的.
这段代码输出的是:
<html>
<head>
haha
</head>
<head>
<title>
HTML encoding with Kotlin
</title>
</head>
</html>
最内层的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通过精心的定义, 主要的目的是为了让使用者更加方便, 代码更加清晰直观.
参考
More resources:
- Kotlin之美——DSL篇
- From Java Builders to Kotlin DSLs
- Oversimplified network call using Retrofit, LiveData, Kotlin Coroutines and DSL
Kotlin DSL for HTML实例解析的更多相关文章
- exec函数族实例解析
exec函数族实例解析 fork()函数通过系统调用创建一个与原来进程(父进程)几乎完全相同的进程(子进程是父进程的副本,它将获得父进程数据空间.堆.栈等资源的副本.注意,子进程持有的是上述存储空间的 ...
- [Reprint] C++函数模板与类模板实例解析
这篇文章主要介绍了C++函数模板与类模板,需要的朋友可以参考下 本文针对C++函数模板与类模板进行了较为详尽的实例解析,有助于帮助读者加深对C++函数模板与类模板的理解.具体内容如下: 泛型编程( ...
- [Reprint]C++普通函数指针与成员函数指针实例解析
这篇文章主要介绍了C++普通函数指针与成员函数指针,很重要的知识点,需要的朋友可以参考下 C++的函数指针(function pointer)是通过指向函数的指针间接调用函数.相信很多人对指向一般 ...
- JavaWeb实现文件上传下载功能实例解析
转:http://www.cnblogs.com/xdp-gacl/p/4200090.html JavaWeb实现文件上传下载功能实例解析 在Web应用系统开发中,文件上传和下载功能是非常常用的功能 ...
- Android实例-Delphi开发蓝牙官方实例解析(XE10+小米2+小米5)
相关资料:1.http://blog.csdn.net/laorenshen/article/details/411498032.http://www.cnblogs.com/findumars/p/ ...
- Android开发之IPC进程间通信-AIDL介绍及实例解析
一.IPC进程间通信 IPC是进程间通信方法的统称,Linux IPC包括以下方法,Android的进程间通信主要采用是哪些方法呢? 1. 管道(Pipe)及有名管道(named pipe):管道可用 ...
- easyUI:ComboTree and comselector使用实例解析
ComboTree 使用场景:故名思意,ComboTree是combox和Tree的结合体,在需要通过选择得到某一个node值的时候触发. 栗子: 定义: 使用标签创建树形下拉框. Comselect ...
- Maven--多模块依赖实例解析(五)
<Maven--搭建开发环境(一)> <Maven--构建企业级仓库(二)> <Maven—几个需要补充的问题(三)> <Maven—生命周期和插件(四)&g ...
- SoapUI简介和入门实例解析
SoapUI简介 SoapUI是一个开源测试工具,通过soap/http来检查.调用.实现Web Service的功能/负载/符合性测试.该工具既可作为一个单独的测试软件使用,也可利用插件集成到Ecl ...
随机推荐
- JNDI数据源的使用
有时候我们数据库的连接会使用jndi的方式 try { InitialContext ic = new InitialContext(); dataSource = (DataSource) ic.l ...
- 【一起学源码-微服务】Nexflix Eureka 源码九:服务续约源码分析
前言 前情回顾 上一讲 我们讲解了服务发现的相关逻辑,所谓服务发现 其实就是注册表抓取,服务实例默认每隔30s去注册中心抓取一下注册表增量数据,然后合并本地注册表数据,最后有个hash对比的操作. 本 ...
- 0009 CSS基础选择器( 标签、类、id、通配符)
typora-copy-images-to: media 第01阶段.前端基础.CSS基础选择器 CSS选择器(重点) 学习目标: 理解 能说出选择器的作用 id选择器和类选择器的区别 应用 能够使用 ...
- $CF888G\ Xor-MST$ 最小生成树
正解:最小生成树 解题报告: 传送门$QwQ$ 发现$Kruskal$和$Prime$都不太可做,于是考虑$B$算法. 先大概港下$B$算法的流程趴$QwQ$.大概就,每次对每个联通块找到最近的联通块 ...
- Linux常用命令大全(四)
Linux常用命令大全(四) shell的特点 ☆组合新命令 ☆提供了文件名扩展字符 ☆直接使用shell的内置命令 ☆灵活地使用数据流 ☆结构化的程序模块 ☆在后台执行命令 ☆可配置的环境 ☆高级的 ...
- 如何在Go项目中输出版本信息?
我们经常在使用CLI工具的时候,都会有这样的参数输出: ``` ➜ ~ docker version Client: Docker Engine - Community Version: 18.09. ...
- JavaScript 继承小记
面向对象编程很重要的一个方面,就是对象的继承.A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法.这对于代码的复用是非常有用的. 大部分面向对象的编程语言,都是通过“类”(class) ...
- WPF 添加提示动画
下面放一张效果图: 那么具体是怎么实现呢: 前端XAML中: <Image Source="/Images/tips.png" HorizontalAlignment=&qu ...
- Scala与Mongodb实践3-----运算环境的搭建
目的:使的在IDEA中编辑代码,令代码实现mongodb运算,且转换较为便捷 由实验2可知,运算环境的搭建亦需要对数据进行存储和计算,故需要实现类型转换,所以在实验2的基础上搭建环境. 由菜鸟教程可得 ...
- BFC 是什么东西?
以下是本人理解的 BFC 和 官方文档BFC资料 . BFC 是页面元素的隐藏属性,全称 : Block Formatting Context 作用: 可以清除子元素浮动后不良效果在线效果地址:ht ...