mmap.page

少写些 HTML

HTML 是文本标记语言 #

HTML 全称是 HyperText Markup Language, 超文本标记语言。 顾名思义,HTML 是用来标记文本的,而不是用来描述用户界面。 虽然后来进行了扩展,但为了和原本的设计兼容,仍然不善于表达用户界面。

当然了,JavaScript 本来也是用来装饰页面的,用来写一些亮晶晶的小玩意。 但是不管怎么说,JavaScript 总还是一个通用编程语言,而且 JavaScript 更新的节奏也远远超过 HTML. 所以两害相权取其轻,宁愿写 JavaScript, 也不愿意写 HTML.

实际上,真正纯粹用 HTML 的框架并不常见。 因为 HTML 太不好用了,用 HTML 的框架多少需要加点料, 比如 *ng (angular) 和 v- (vue) 属性,再比如 JSX (React). 再不然就是搞一个和 HTML 类似的模板语言。

其实这种「缝缝补补又三年」的作风,还不如干脆直接写 JavaScript 爽气。

Kotlin 和 Ceylon 的做法 #

一些新冒出来的语言直接用原生的结构来表达 HTML.

比如,Kotlin 里,html 是这样写的:

fun result(args: Array<String>) =
    html {
        head {
            title {+"XML encoding with Kotlin"}
        }
        body {
            h1 {+"XML encoding with Kotlin"}
            a(href = "http://kotlinlang.org") {+"Kotlin"}
            p {
                for (arg in args)
                    +arg
            }
        }
    }

这可不是什么模板语言。这就是 Kotlin, 原生的。

Kotlin 提供这样一个语法糖:

f({ "一个匿名函数" })

可以写成

f { "一个匿名函数" }

所以 html { ... } 就是一个 Kotlin 函数调用, {} 里面的其他 html 元素同理。

html 函数的定义大概是这样的这样:

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

大致上是初始化一个 HTML 实例。

相应的 HTML 类的定义:

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

使用原生 Kotlin 语言表达 HTML,意味着我们可以使用所有的 Kotlin 原生语言特性, 再也不必为 HTML 模板语言表达力不足或者写法奇怪而烦恼。 同时,自然而然地保证了 HTML 模板的类型安全。

类似地,ceylon 里 html 是这样写的:

Html {
    doctype = html5;
    Head {
        title = "Ceylon: home page";
    };
    Body {
        H1 { "Hello `` req.queryParameter("name") else "World" `` !" }
    };
}

上面用到了 Ceylon 为命名参数提供的语法糖,等价于:

Html(doctype = html5, Head(...), Body(...))

Html 是标准库提供的类,Html { ... } 就是初始化, (Ceylon 直接通过 C() 构造类的实例,不用 new) 同样是原生的 Ceylon. 这和 Kotlin 的思路是类似的。

Mithril #

JavaScript 中,Mithril 框架直接使用 JavaScript 代替 HTML:

m("main", [
    m("h1", {class: "title"}, "My first app"),
    m("button", "A button"),
])

这就意味着 JavaScript 的各种特性都可以直接用, 虽然 JavaScript 并不是一门优雅的语言, 比起 HTML 以及魔改 HTML 的模板语言,不知道高到哪里去了。

同样,JavaScript 的各种基础设施全部可以直接用,包括 IDE 的错误提示、重构,flow 类型检查,等等。 不需要苦等工具专门支持。

迁移框架 #

有人说,直接写 JavaScript 的话,换框架的时候改起来会累死。 其实写 HTML 换框架不用怎么改,是因为大部分写的是普通 HTML. 如果真的用模板的特性,写的稍微酷炫一点,改起来一样累死。 而且一旦有什么地方不小心忘记改了,就变 bug 了。

而直接写 JavaScript 的话,换框架或者模板其实就是代码的转换, 也就是解析一种风格的 JavaScript,生成另一种风格的 JavaScript, 或者另一种模板语言。

这都可以由程序完成,相比手工修改,不容易出错。 如果有类型系统辅助的话就更好了,会比较容易发现转换的错误。

由于复杂的逻辑通常都抽出来包成函数, 所以实际转换的是一个高度简化的 JavaScript, 猜想工作量大概和写一个 json 到 yaml 的转换器差不多吧。 而且,各种 JavaScript 的 parser 库、AST 变换的库等也可以直接使用或参考, 不用自己从头开始写。

思考题 #

Mithril 提供了属性的简写形式。 以下两段代码是等价的。

长版:

m("input", {
    class: "name",
    type: "text",
    placeholder: "First name",
})

短版:

m("input.name[type=text][placeholder=First name]")

问题:

  1. 属性简写为 [name=value] 好不好?为什么?
  2. class 简写为 element.className 好不好?为什么?