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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...