距离全栈 你只差一个 kotlinx

本贴最后更新于 1315 天前,其中的信息可能已经物是人非

示例项目地址: https://github.com/wangyuheng/kotlin-dsl-html

全栈不能保证一定能够解决复杂的问题, 但却能帮你打开解决复杂问题的大门.

dsl_0

近些年,前端技术变得愈发复杂。这一趋势除了导致全球变暖,也让全栈开发的难度越来越大。

但是,阻碍一个后端开发去写页面的根本原因到底是什么呢?
我认为是开发环境的搭建,如果环境变量准备好,可以用自己平时使用的 IDE 直接写代码,刷新就能看效果,这事貌似可行。

那么如何解决这个问题呢?
我觉得终极解决方案是用一门编程语言同时写前后端代码。

Kotlin 配合 DSL 就可以。

DSL

DSL(Domain Specific Language),领域特定语言。专注于一个方面而特殊设计的语言。

比如 SQL 是数据库领域的 DSL。

特点

  1. 只描述和解决特定领域

优势

  1. 语义更明确,直观
  2. 可以屏蔽数据结构和技术细节,由领域专家编写

缺点

  1. 额外的理解、学习成本
  2. 抽象设计难度高,需要平衡表现力和实现成本。

比如描述 2 天前的时间,你可以定义为
2 days ago
也可以是
new Date().before(2, DAY)

这又引出了 DSL 的 2 种分类

  • 内部(Internal)DSL,借助宿主语言(如:Scala、Kotlin)实现。和提取函数方法不同,提供了一套更接近自然语言的语法表现形式
  • 外部(External)DSL,语言无关,需要自定义语法并实现解析器。比如 XMlYAML

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

dsl_1

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) }
                    }
                }
            }
        }

浏览器效果

dsl_2

通常 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.

想必你也听得出是在影射哪个框架

结论

  1. kotlinx.html 可以作为 jsp、FreeMarker 这类模板方案的替代,优势是无需在两种语法之间切换。
  2. 如果没想清楚如何解决问题,那 DSL 不是一个好的选择
  • Kotlin

    Kotlin 是一种在 Java 虚拟机上运行的静态类型编程语言,由 JetBrains 设计开发并开源。Kotlin 可以编译成 Java 字节码,也可以编译成 JavaScript,方便在没有 JVM 的设备上运行。在 Google I/O 2017 中,Google 宣布 Kotlin 成为 Android 官方开发语言。

    19 引用 • 33 回帖 • 26 关注
  • HTML

    HTML5 是 HTML 下一个的主要修订版本,现在仍处于发展阶段。广义论及 HTML5 时,实际指的是包括 HTML、CSS 和 JavaScript 在内的一套技术组合。

    103 引用 • 294 回帖
2 操作
crick77 在 2020-09-19 16:27:14 置顶了该帖
crick77 在 2020-09-19 16:27:03 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...