示例项目地址: https://github.com/wangyuheng/kotlin-dsl-html
全栈不能保证一定能够解决复杂的问题, 但却能帮你打开解决复杂问题的大门.
近些年,前端技术变得愈发复杂。这一趋势除了导致全球变暖,也让全栈开发的难度越来越大。
但是,阻碍一个后端开发去写页面的根本原因到底是什么呢?
我认为是开发环境的搭建,如果环境变量准备好,可以用自己平时使用的 IDE 直接写代码,刷新就能看效果,这事貌似可行。
那么如何解决这个问题呢?
我觉得终极解决方案是用一门编程语言同时写前后端代码。
Kotlin 配合 DSL 就可以。
DSL
DSL(Domain Specific Language),领域特定语言。专注于一个方面而特殊设计的语言。
比如 SQL
是数据库领域的 DSL。
特点
- 只描述和解决特定领域
优势
- 语义更明确,直观
- 可以屏蔽数据结构和技术细节,由领域专家编写
缺点
- 额外的理解、学习成本
- 抽象设计难度高,需要平衡表现力和实现成本。
比如描述 2 天前的时间,你可以定义为
2 days ago
也可以是
new Date().before(2, DAY)
这又引出了 DSL 的 2 种分类
- 内部(Internal)DSL,借助宿主语言(如:Scala、Kotlin)实现。和提取函数方法不同,提供了一套更接近自然语言的语法表现形式
- 外部(External)DSL,语言无关,需要自定义语法并实现解析器。比如
XMl
、YAML
kotlin DSL
Kotlin
借助 Lambda
+ Extensions扩展
来实现内部 DSL
fun main(args: Array<String>) {
expression {
source = "a"
target = "b"
operator = Operator.ADD
onBefore { println("before $source") }
}
}
enum class Operator {
ADD,
SUBTRACT,
MULTIPLY,
DIVIDE
}
class Expression {
var source: String? = null
var target: String? = null
var operator: Operator? = null
internal var before: () -> Unit = { }
fun onBefore(onBefore: () -> Unit) {
before = onBefore
}
fun execute(): String {
this.before()
val result = "$source $operator $target"
println(result)
return result
}
}
fun expression(init: Expression.() -> Unit) {
val wrap = Expression()
wrap.init()
wrap.execute()
}
expression
函数的入参是一个 Expression.() -> Unit
类型的 lambda
。未简化的代码为
val lambdaExpression:Expression.() -> Unit = {
source = "a"
target = "b"
operator = Operator.ADD
onBefore {
println("before $source")
}
}
expression(lambdaExpression)
简化后借助 Lambda argument should be moved out of parentheses
, 变为
expression {
source = "a"
target = "b"
operator = Operator.ADD
onBefore {
println("before $source")
}
}
是不是有 DSL 内味了
kotlinx
kotlinx.html
是一个通过 DSL 构造 HTML 的 kotlin 扩展库。
本质上是通过 kotlin 定义好了一套 htmlTag 类。
DSL 定义
html {
head {
script {
src = "https://code.jquery.com/jquery-3.5.1.slim.min.js"
}
}
body {
div {
+"Hello $name"
}
}
}
生成的 HTML
<html>
<head>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
</head>
<body>
<div>Hello DSL</div>
</body>
</html>
在此基础上扩展并封装 UI 组件,达到简化开发成本的目的。比如
- 封装 bootstrap V4 的 header 资源引用
fun HEAD.b4() {
link {
href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
rel = "stylesheet"
}
script {
src = "https://code.jquery.com/jquery-3.5.1.slim.min.js"
}
script {
src = "https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"
}
script {
src = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"
}
}
- 封装基于 bootstrap4 的 dropdown
fun DIV.b4dropdown(btn: String, block: DIV.() -> Unit) {
div("dropdown") {
a("#") {
classes = setOf("btn", "btn-secondary", "dropdown-toggle")
role = "button"
attributes["aria-expanded"] = "false"
attributes["data-toggle"] = "dropdown"
+btn
}
div("dropdown-menu") {
attributes["aria-labelledby"] = "dropdownMenuLink"
block()
}
}
}
fun DIV.b4dropdownItem(content: String, href: String? = "#", target: String? = null) {
a(href, target, "dropdown-item") { +content }
}
- 封装基于 bootstrap4 的 table
fun HtmlBlockTag.b4table(headers: List<String>, rows: List<List<() -> Unit>>, showIndex: Boolean = false) {
table("table") {
thead {
tr {
if (showIndex) th(ThScope.col) { +"#" }
headers.forEach { th(ThScope.col) { +it } }
}
}
tbody {
for ((index, row) in rows.withIndex()) {
tr {
if (showIndex) td { +"${index + 1}" }
row.forEach { td { it() } }
}
}
}
}
}
则 View 层代码为
val dropdownList = arrayListOf("Action", "Another action", "Something else here")
val tableHeaders = arrayListOf("First", "Last", "Handle")
val tableRows = arrayListOf(
arrayListOf("Mark", "Otto", "@mdo"),
arrayListOf("Jacob", "Thornton", "@fat"),
arrayListOf("Larry", "the Bird", "@twitter")
)
createHTML()
.html {
head {
b4()
}
body {
b4table(tableHeaders, tableRowsUnit, false)
div {
b4dropdown("Dropdown link") {
dropdownList.forEach { b4dropdownItem(it) }
}
}
}
}
浏览器效果
通常 table 的最后一列为 action 列,所以 table rows 类型为 List<List<() -> Unit>>
,可以传入 html 元素
createHTML()
.html {
val tableRowsUnit = tableRows.map { r ->
r.map {
{ +it }
}
}
head {
b4()
}
body {
b4table(tableHeaders, tableRowsUnit, false)
div {
b4dropdown("Dropdown link") {
list.forEach { b4dropdownItem(it) }
}
}
div {
b4table(
tableHeaders.toList().plus("action"),
tableRowsUnit.map { r ->
r.plus {
ul {
lia("#") { +"Edit" }
lia("#") { +"Delete" }
}
}
},
true
)
}
}
}
其他
Javalin
示例项目使用了 Javalin
Javalin is more of a library than a framework. Some key points:
- You don't need to extend anything
- There are no @Annotations
- There is no reflection
- There is no other magic; just code.
想必你也听得出是在影射哪个框架
结论
- kotlinx.html 可以作为 jsp、FreeMarker 这类模板方案的替代,优势是无需在两种语法之间切换。
- 如果没想清楚如何解决问题,那 DSL 不是一个好的选择
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于