今天,我們一起來看
body {
h1 {
+"Hello from $name!"
}
}
這段程式的實作。
我們先來看看 body
inline fun HTML.body(classes : String? = null, crossinline block : BODY.() -> Unit = {}) : Unit = BODY(attributesMapOf("class", classes), consumer).visit(block)
BODY
的實作則是
open class BODY(initialAttributes : Map<String, String>, override val consumer : TagConsumer<*>) : HTMLTag("body", consumer, initialAttributes, null, false, false), HtmlBlockTag
和 head
不同,這邊實作的不是 HtmlHeadTag
,而是 HtmlBlockTag
interface HtmlBlockTag : CommonAttributeGroupFacade, FlowContent {
}
這邊我們可以看到,雖然 head
和 body
很像
但是他們並不是實作一個定義非常多函數的介面
而是實作一個介面,該介面再去實作其他介面。
這就討論到我們在設計物件導向程式時所會提到的一個原則:介面隔離原則
與其設計一個功能很多的介面,然後實作該介面的物件需要用其他方式去繞過不需要實作的函數
其實我們可以設計許多個小介面,然後讓物件去實作他們。
如果這樣會導致物件總是需要實作很多介面,那我們就像現在這樣
設計一個介面來實作對應的介面們。
這邊的 BODY
還定義了一個 attributesMapOf("class", classes)
我們來看看其實作
fun attributesMapOf(key: String, value: String?): Map<String, String> = when (value) {
null -> emptyMap
else -> singletonMapOf(key, value)
}
fun singletonMapOf(key: String, value: String): Map<String, String> = SingletonStringMap(key, value)
private data class SingletonStringMap(override val key: String, override val value: String) : Map<String, String>,
Map.Entry<String, String> {
override val entries: Set<Map.Entry<String, String>>
get() = setOf(this)
override val keys: Set<String>
get() = setOf(key)
override val size: Int
get() = 1
override val values: Collection<String>
get() = listOf(value)
override fun containsKey(key: String) = key == this.key
override fun containsValue(value: String) = value == this.value
override fun get(key: String): String? = if (key == this.key) value else null
override fun isEmpty() = false
}
這邊最終定義出了一個 private data class
來協助我們管理 body
所會包含的 class
參數。
接著我們往下看 h1
/**
* Heading
*/
@HtmlTagMarker
inline fun FlowOrHeadingContent.h1(classes : String? = null, crossinline block : H1.() -> Unit = {}) : Unit = H1(attributesMapOf("class", classes), consumer).visit(block)
@Suppress("unused")
open class H1(initialAttributes : Map<String, String>, override val consumer : TagConsumer<*>) : HTMLTag("h1", consumer, initialAttributes, null, false, false), CommonAttributeGroupFacadeFlowHeadingPhrasingContent {
}
H1
除了和 HEAD
一樣繼承了 HTMLTag
,不同的地方是還實作了 CommonAttributeGroupFacadeFlowHeadingPhrasingContent
介面
interface CommonAttributeGroupFacadeFlowHeadingPhrasingContent : CommonAttributeGroupFacade, CommonAttributeGroupFacadeFlowHeadingContent, FlowPhrasingContent, HtmlBlockInlineTag, HtmlBlockTag, HtmlInlineTag {
}
我們逐一看過這些介面,會發現一個有趣的地方
interface HtmlBlockTag : CommonAttributeGroupFacade, FlowContent {
}
interface HtmlHeadTag : CommonAttributeGroupFacade, MetaDataContent {
}
interface HtmlInlineTag : CommonAttributeGroupFacade, PhrasingContent {
}
這些介面都繼承了 CommonAttributeGroupFacade
interface CommonAttributeGroupFacade : Tag {
}
我們再往下看 Tag
@HtmlTagMarker
interface Tag {
val tagName: String
val consumer: TagConsumer<*>
val namespace: String?
val attributes: MutableMap<String, String>
val attributesEntries: Collection<Map.Entry<String, String>>
val inlineTag: Boolean
val emptyTag: Boolean
operator fun Entities.unaryPlus(): Unit {
entity(this)
}
operator fun String.unaryPlus(): Unit {
text(this)
}
fun text(s: String) {
consumer.onTagContent(s)
}
fun text(n: Number) {
text(n.toString())
}
fun entity(e: Entities) {
consumer.onTagContentEntity(e)
}
fun comment(s: String) {
consumer.onTagComment(s)
}
}
到這邊我們就可以看到 Tag
設計的方式了,除了定義了 HTML 標籤常會使用的參數之外,也預設定義了幾個要生成標籤會需要的函數。
基本上的生成細節,如我們之前看過的,還是以 consumer
實作為主
到這邊我們就看過了
body {
h1 {
+"Hello from $name!"
}
}
這兩個函數的實作,對於 Ktor 怎麼生成 HTML 內容,有了更加清楚的認識。