Kotlin DSL for HTML實例解析

来源:https://www.cnblogs.com/mengdd/archive/2020/02/05/kotlin-dsl-for-html.html
-Advertisement-
Play Games

Kotlin DSL, 指用Kotlin寫的Domain Specific Language. 本文通過解析官方的Kotlin DSL寫html的例子, 來說明Kotlin DSL是什麼. 首先是一些基礎知識, 包括什麼是DSL, 實現DSL利用了那些Kotlin的語法, 常用的情形和流行的庫. ... ...


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

就能輸出這樣的文本:

<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, 只包含渲染方法. 這個介面的子類是TagTextElement.

有文字的標簽, 如<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內部可以通過調用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 <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方法.
顯式地調用是這樣的:

[email protected] { +"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 {
        [email protected] { } // possible
    }
    // ...
}

只有最近的receiver對象可以隱式訪問.

總結

本文通過實例, 來逐步解析如何用Kotlin代碼, 用半陳述式的方式寫html結構, 從而看起來更加直觀. 這種就叫做DSL.

Kotlin DSL通過精心的定義, 主要的目的是為了讓使用者更加方便, 代碼更加清晰直觀.

參考

More resources:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 0. 本blog 簡單說明一下 Linux測試環境尤其是 CentOS測試環境的開發測試使用, 教程可能不會很長, 主要是入門. 0.1 Linux簡介: Linux 的歷史基本上不用闡述, linus作為自己的興趣愛好進行編碼實現的一種開源的操作系統. Linux很好的切合了GNU裡面一直沒有可用 ...
  • 1.flink運行時的組件 ​ Flink 運行時架構主要包括四個不同的組件,它們會在運行流處理應用程式時協同工作: 作業管理器(JobManager)、資源管理器(ResourceManager)、任務管理器(TaskManager), 以及分發器(Dispatcher)。因為 Flink 是用 ...
  • 在消費Kafka中分區的數據時,我們需要跟蹤哪些消息是讀取過的、哪些是沒有讀取過的。這是讀取消息不丟失的關鍵所在。Kafka是通過offset順序讀取事件的。如果一個消費者退出,再重啟的時候,它知道從哪兒繼續讀取消息進行處理。所以,消費者需要「提交」屬於它們自己的偏移量。如果消費者已經提交了偏移量,... ...
  • 首先看一下我的基本的開發環境: 操作系統:MacOS 10.13.5 編輯器:IDEA 2018.3 其他:MySQL8.0.15、Maven 3.3.9、JDK 1.8 好,下麵就正式開始: 第一步:在IDEA中新建一個maven項目 1.使用骨架創建maven項目,此處選擇:maven-arch ...
  • 2020-02-05 mysqli擴展 phpl連接Mysql mysqli_connect($servername,$username,$password,$database,$port); //參數1:連接的主機 參數2:資料庫登錄賬號 參數3:資料庫登錄密碼 參數4:將要連接的資料庫 參數5: ...
  • redis中整數集合intset相關的文件為:intset.h與intset.c intset的所有操作與操作一個排序整形數組 int a[N]類似,只是根據類型做了記憶體上的優化。 一、數據結構 1 typedef struct intset { 2 uint32_t encoding; 3 uin ...
  • android sdk manager 無法更新,解決連不上dl.google.com的問題 ...
  • Android開發中,Binder是一種跨進程通信方式,而使用AIDL可以實現Binder的工作。 如何使用它是瞭解它的第一步,本文章主要記錄使用Binder的一些步驟。(代碼思路參考《Android開發藝術探索》任玉剛 著) 1.創建兩個activity 兩個activity(OneActivit ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...