iT邦幫忙

2023 iThome 鐵人賽

DAY 9
0

看了怎麼處理靜態檔案之後,今天我們來看網頁後端服務必備的一個項目:處理網頁畫面

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)。這讓實作 TagConsumerHTMLStreamBuilder 裡面實作可以更加彈性。

接著我們呼叫 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 的 inlinecrossinline 語法。簡單的說,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 的結構,都是會用一個 TagStartTagEnd 包起裡面的內容的

所以這邊首先宣告了 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 元素是怎麼生成的!


上一篇
Day 08:用 staticFiles() 處理靜態檔案
下一篇
Day 10:從 head title 等函數窺探神通廣大的 HTMLTag 物件
系列文
深入解析 Kotlin 專案 Ktor 的程式碼,探索 Ktor 的強大功能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言