Kotlin DSL for HTML實例解析

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

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

但是這裡[email protected]是可以省略的.

這段代碼輸出的是:

<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:


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

更多相關文章
  • 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 ...
一周排行
  • 微信公眾號dotnet跨平臺2020年初做的一個關於中國.NET開發者調查收到了開發者近 1400 條回覆。這份調查報告涵蓋了開發者工具鏈的所有部分,包括編程語言、應用架構、應用伺服器、運行時平臺、框架技術、框架配置、IDE、.NET/.NET Core 發行版部署模式、構建工具和Kubernete... ...
  • Winform控制項的雙緩衝。控制項的雙緩衝屬性是隱藏的,可以通過反射改變其屬性值。 lv.GetType().GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(lv, true, ...
  • 1. 需求 上圖這種包含多選(CheckBox)和單選(RadioButton)的菜單十分常見,可是在WPF中只提供了多選的MenuItem。順便一提,要使MenuItem可以多選,只需要將MenuItem的 屬性設置為True: 不知出於何種考慮,WPF沒有為MenuItem提供單選的功能。為了在 ...
  • gRPC的結構 在我們搭建gRPC通信系統之前,首先需要知道gRPC的結構組成。 首先,需要一個server(伺服器),它用來接收和處理請求,然後返迴響應。 既然有server,那麼肯定有client(客戶端),client的作用就是向server發送請求,具體就是生成一個請求,然後把它發送到ser ...
  • 區別 OpenId: Authentication :認證 Oauth: Aurhorize :授權 輸入賬號密碼,QQ確認輸入了正確的賬號密碼可以登錄 認證 下麵需要勾選的覆選框(獲取昵稱、頭像、性別) 授權 OpenID 當你需要訪問A網站的時候,A網站要求你輸入你的OpenId,即可跳轉到你的 ...
  • 前言 預計是通過三篇來將清楚asp.net core 3.x中的授權:1、基本概念介紹;2、asp.net core 3.x中授權的預設流程;3、擴展。 在完全沒有概念的情況下無論是看官方文檔還是源碼都暈乎乎的,希望本文能幫到你。不過我也是看源碼結合官方文檔看的,可能有些地方理解不對,所以只作為參考 ...
  • 簡介 基於生產者消費者模式,我們可以開發出線程安全的非同步消息隊列。 知識儲備 什麼是生產者消費者模式? 為了方便理解,我們暫時將它理解為垃圾的產生到結束的過程。 簡單來說,多住戶產生垃圾(生產者)將垃圾投遞到全小區唯一一個垃圾桶(單隊列),環衛將垃圾桶中的垃圾進行處理(消費者)。就是一個生產者消費者 ...
  • 很多時候,需要對類中的方法進行一些測試,來判斷是否能按要求輸出預期的結果。 C#提供了快速創建單元測試的方法,但單元測試不僅速度慢不方便,大量的單元測試還會拖慢項目的啟動速度。 所以決定自己搞個方便的測試用例。 控制台一句話調用。 測試用例.註冊並Print(EnumEx.Name); 結果畫面: ...
  • 常成員函數不能改變數據成員的值,例如定義坐標類Coordinate,成員函數changeX():void Coordinate::changeX(){ x = 10;}雖然changeX()沒有參數,但是它隱含一個參數——this指針:void Coordinate::changeX(Coordin... ...
  • 因為新冠肺炎疫情,診所還沒復工。這是在家用手機敲的,代碼顯示有問題。等復工以後在電腦上改,各位先湊和看吧。 支持向量機(Support Vector Machine, SVM)是一種基於統計學習的模式識別的分類方法,主要用於模式識別。所謂支持向量指的是在分割區域邊緣的訓練樣本點,機是指演算法。就是要找 ...
x