看了怎麼處理靜態檔案之後,今天我們來看網頁後端服務必備的一個項目:處理網頁畫面
Ktor 允許一種處理方式,稱為 HTML DSL(Domain-Specific Language)
首先需要安裝套件
implementation("io.ktor:ktor-server-html-builder:$ktor_version")
實際看起來類似這樣
import io.ktor.server.application.*
import io.ktor.server.html.*
import io.ktor.http.*
import io.ktor.server.routing.*
import kotlinx.html.*
fun Application.module() {
routing {
get("/") {
val name = "Ktor"
call.respondHtml(HttpStatusCode.OK) {
head {
title {
+name
}
}
body {
h1 {
+"Hello from $name!"
}
}
}
}
}
}
這樣寫起來,最後會生成以下的 HTML
<html>
<head>
<title>Ktor</title>
</head>
<body>
<h1>Hello from Ktor!</h1>
</body>
</html>
今天,我們就來看看這是怎麼做到的。
首先來看看 call.respondHtml
的實作
public suspend fun ApplicationCall.respondHtml(status: HttpStatusCode = HttpStatusCode.OK, block: HTML.() -> Unit) {
val text = buildString {
append("<!DOCTYPE html>\n")
appendHTML().html(block = block)
}
respond(TextContent(text, ContentType.Text.Html.withCharset(Charsets.UTF_8), status))
}
可以看到這個結構非常簡短,先加上 <!DOCTYPE html>
接著裡面所有內容生成全部透過 appendHTML().html(block = block)
處理
然後就可以透過 respond
回傳了。
接著我們找一下 appendHTML
的實作
fun <O : Appendable> O.appendHTML(prettyPrint: Boolean = true, xhtmlCompatible: Boolean = false): TagConsumer<O> =
HTMLStreamBuilder(this, prettyPrint, xhtmlCompatible).delayed()
HTMLStreamBuilder
的宣告如下
class HTMLStreamBuilder<out O : Appendable>(val out: O, val prettyPrint: Boolean, val xhtmlCompatible: Boolean) :
TagConsumer<O>
我們看看 TagConsumer
的介面實作
interface TagConsumer<out R> {
fun onTagStart(tag: Tag)
fun onTagAttributeChange(tag: Tag, attribute: String, value: String?)
fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit)
fun onTagEnd(tag: Tag)
fun onTagContent(content: CharSequence)
fun onTagContentEntity(entity: Entities)
fun onTagContentUnsafe(block: Unsafe.() -> Unit)
fun onTagComment(content: CharSequence)
fun finalize(): R
}
這邊可以看到 out
的宣告語法,這種語法也被稱為協變(Covariance)。這讓實作 TagConsumer
的 HTMLStreamBuilder
裡面實作可以更加彈性。
接著我們呼叫 html()
inline fun <T, C : TagConsumer<T>> C.html(namespace : String? = null, crossinline block : HTML.() -> Unit = {}) : T = HTML(emptyMap, this, namespace).visitAndFinalize(this, block)
visitAndFinalize
裡面會訪問過我們所宣告的所有元素,並在最後 finalize
出我們要的結果。
這邊用到了 Kotlin 的 inline
和 crossinline
語法。簡單的說,inline
可以在編譯階段,將這段函數 inline 到物件內,進而減少匿名函數實作上的一些效能花費。crossinline
標記函數內的 block
函數只能夠在本地進行回傳。
理解之後,我們就可以看看
inline fun <T : Tag, R> T.visitTagAndFinalize(consumer: TagConsumer<R>, block: T.() -> Unit): R {
if (this.consumer !== consumer) {
throw IllegalArgumentException("Wrong exception")
}
visitTag(block)
return consumer.finalize()
}
visitTag
則是
inline fun <T : Tag> T.visitTag(block: T.() -> Unit) {
consumer.onTagStart(this)
this.block()
consumer.onTagEnd(this)
}
看到這裡,我們總算看出這段邏輯想做什麼了。
因為許多的標籤語法,包含 HTML 的結構,都是會用一個 TagStart
和 TagEnd
包起裡面的內容的
所以這邊首先宣告了 TagConsumer
介面,包含了遇到標籤該做的順序:開始標籤,運作內容,結束標籤。
然後 HTML 就可以依照這個順序去編撰標籤。然後最後透過 finalize
一次生成內容。
看到這邊,我們再去看 HTML
的宣告
open class HTML(initialAttributes : Map<String, String>, override val consumer : TagConsumer<*>, namespace : String? = null) : HTMLTag("html", consumer, initialAttributes, namespace, false, false), CommonAttributeGroupFacade
這邊宣告了一個 HTMLTag
物件, tagName
包含了 "html"
字串,讓後續 HTMLStreamBuilder
可以好好做 finalize
。
我們來看看 HTMLStreamBuilder.onTagStart
這段雖說比較多內容,但是邏輯是很單純的字串處理
private var level = 0
private var ln = true
override fun onTagStart(tag: Tag) {
if (prettyPrint && !tag.inlineTag) {
indent()
}
level++
out.append("<")
out.append(tag.tagName)
if (tag.namespace != null) {
out.append(" xmlns=\"")
out.append(tag.namespace)
out.append("\"")
}
if (tag.attributes.isNotEmpty()) {
tag.attributesEntries.forEachIndexed { _, e ->
if (!e.key.isValidXmlAttributeName()) {
throw IllegalArgumentException("Tag ${tag.tagName} has invalid attribute name ${e.key}")
}
out.append(' ')
out.append(e.key)
out.append("=\"")
out.escapeAppend(e.value)
out.append('\"')
}
}
if (xhtmlCompatible && tag.emptyTag) {
out.append("/")
}
out.append(">")
ln = false
}
HTMLStreamBuilder.onTagEnd
也是字串處理
override fun onTagEnd(tag: Tag) {
level--
if (ln) {
indent()
}
if (!tag.emptyTag) {
out.append("</")
out.append(tag.tagName)
out.append(">")
}
if (prettyPrint && !tag.inlineTag) {
appendln()
}
}
這邊還有個很有趣的小邏輯 indent()
private fun indent() {
if (prettyPrint) {
if (!ln) {
out.append("\n")
}
var remaining = level
while (remaining >= 4) {
out.append(" ")
remaining -= 4
}
while (remaining >= 2) {
out.append(" ")
remaining -= 2
}
if (remaining > 0) {
out.append(" ")
}
ln = false
}
}
到這邊,我們看完了 call.respondHtml
裡面的邏輯,以及他怎麼生成 <html>
和 </html>
明天我們來看看其他的 HTML 元素是怎麼生成的!