iT邦幫忙

2025 iThome 鐵人賽

DAY 22
0
Software Development

每天一點 Ktor 3.0:一個月學會 Kotlin 後端開發系列 第 22

Day 22:Ktor 善用 trailing lambda 設計的前端生成框架

  • 分享至 

  • xImage
  •  

上次看過怎麼生成路由之後

今天我們來看看 Ktor 產生前端畫面的流程是怎麼進行的

前端畫面

Ktor 裡面也支援 Java 常見的 HTML 樣板

比方說 Freemarker、Mustache、Thymeleaf、Velocity 等等

不過最值得特別提出來說的

應該是 Ktor 自己定義的 HTML DSL 和 CSS DSL

我們來看看生成 HTML 的程式碼怎麼寫

get("/more-rows") {
    call.respondHtml {
        body {
            table {
                tbody {
                    randomRows(random)
                }
            }
        }
    }
}

這邊我們可以看到,利用 Kotlin 的 trailing lambda 特性,我們可以將生成畫面的函式設計成互相包含的結構,
每個函式包含著生成其他元件的函式,並且將調整的參數寫在 lambda 裡面,幾乎就跟對應的 HTML 結構一樣了。

甚至我們可以定義自己的元件,像是這樣

get("/") {
    call.respondHtml {
        leaderboardPage(random)
    }
}

然後我們在另一個檔案內定義 leaderboardPage()

fun HTML.leaderboardPage(random: Random) {
    head {
        title("HTMX Example")
        script(src = "/web.js") {}
        link(rel = "stylesheet", href = "/leaderboard.css")
    }
    body {
        h1 {
            +"Leaderboard"
        }
        table {
            id = "leaderboard"
            thead {
                tr {
                    th { +"Alias" }
                    th { +"Score" }
                }
            }
            tbody {
                randomRows(random)
            }
        }
        h2 {
            id = "total-count"
            +"Total: 10"
        }
    }
}

要生成 CSS 的話,我們可以先用類似的語法生成

get("/styles.css") {
    call.respondCss {
        body {
            backgroundColor = Color.darkBlue
            margin = Margin(0.px)
        }
        rule("h1.page-title") {
            color = Color.white
        }
    }
}
suspend inline fun ApplicationCall.respondCss(builder: CssBuilder.() -> Unit) {
   this.respondText(CssBuilder().apply(builder).toString(), ContentType.Text.CSS)
}

接著在程式內引入 styles.css

get("/html-css-dsl") {
    call.respondHtml {
        head {
            link(rel = "stylesheet", href = "/styles.css", type = "text/css")
        }
        body {
            h1(classes = "page-title") {
                +"Hello from Ktor!"
            }
        }
    }
}

這背後的原理是什麼呢?首先我們挑 head 來看實作

@HtmlTagMarker
@OptIn(ExperimentalContracts::class)
inline fun HTML.head(crossinline block : HEAD.() -> Unit = {}) : Unit {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    HEAD(emptyMap, consumer).visit(block)
}

這個函數只接受一個參數,就是以 HEAD 為接收者的 Lambda。另外如果沒有傳入 block 的話,就傳入一個空的 lambda。

不管我們有沒有傳入 block,之後都會執行 HEAD(emptyMap, consumer).visit(block)

inline fun <T : Tag> T.visit(crossinline block: T.() -> Unit) = visitTag { block() }

visitTag 則是

inline fun <T : Tag> T.visitTag(block: T.() -> Unit) {
    consumer.onTagStart(this)
    this.block()
    consumer.onTagEnd(this)
}

到這邊我們就可以大致知道,利用函數一層一層的包起來,所以我們就可以先生成 HTML 開頭的標籤,運作完裡面的內容(也是生成許多 HTML 標籤),之後生成結尾的標籤。

這樣的實作方式,不僅不需要 template engine 也可以生成複雜的畫面,另外由於全部都是 Kotlin 的程式,所以不用擔心樣板內使用程式碼的耦合問題。

今天的部分就到這邊,我們明天見!


上一篇
Day 21:Ktor 的特點:從 fun main 設計開始的輕量化
系列文
每天一點 Ktor 3.0:一個月學會 Kotlin 後端開發22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言