Kotlin
Ktor
kotlinx
kotlinx.html

Ktorで軽い静的ページを作ってみた話

KtorのHello Worldをちょっと弄って静的ページを返すようにしてみました。
ただ単純に返すことだけを考えるのなら、respondText()でHTMLをそのまま返せば良いだけなんですか、今後の拡張(動的生成されるページ等)も考え、Ktorの機能を活用する方法を探ってみました。

環境

環境構築や、Hello, Worldのコードに関しては、 @KissyBnts さんのKtorでJSONを返すまで の方を参照してください。
私もお世話になりました。

今回使うbuild.gradleはこちら

group 'toliner'
version '0.1.0'

buildscript {
    ext.kotlin_version = '1.2.10'
    ext.ktor_version = '0.9.0'

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'application'

mainClassName = "io.ktor.server.netty.DevelopmentEngine"

repositories {
    mavenCentral()
    jcenter()
    maven { url "http://dl.bintray.com/kotlin/ktor" }
    maven { url "https://dl.bintray.com/kotlin/kotlinx" }
}

dependencies {
    //kotlin
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
    //ktor
    compile "io.ktor:ktor-server-core:$ktor_version"
    compile "io.ktor:ktor-server-netty:$ktor_version"
    compile "io.ktor:ktor-html-builder:$ktor_version"
    //log
    compile "ch.qos.logback:logback-classic:1.2.1"
    //test
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

kotlin {
    experimental {
        coroutines "enable"
    }
}

run {
    standardInput = System.in
}

repositoriesに"https://dl.bintray.com/kotlin/kotlinx" が、
dependenciesにio.ktor:ktor-html-builder:$ktor_versionが加わっています。
あと、現在のKotlinの最新版は1.2.10なのですが、Ktorの最終リリースである0.9.0がKotlin 1.1.51を参照していて、ランタイムのバージョン違いが発生しているため、kotlin-reflectは最新を使うように明記しています。

StringBuilder + kotlinx.html.stream

まず、StringBuilderでHTMLの文字列を作っていく方法です。
kotlinx.htmlを使うことで、HTMLを階層的なkotlinのコードで記述できます。
今回の実装は、以下のとおりです。

import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.CallLogging
import io.ktor.features.DefaultHeaders
import io.ktor.http.ContentType
import io.ktor.response.respondText
import io.ktor.routing.Routing
import io.ktor.routing.get
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.html.*
import kotlinx.html.stream.appendHTML

fun Application.main() {
    embeddedServer(Netty) {
        install(DefaultHeaders)
        install(CallLogging)
        install(Routing) {
            get("/hello") {
                call.respondText(buildString {
                    appendln("<!DOCTYPE html>")
                    appendHTML().html {
                        body {
                            h1 { +"Hello, World!" }
                            p { +"This is Sample Page" }
                        }
                    }
                    appendln()
                }, ContentType.Text.Html)
            }
        }
    }.start()
}

このコードをコンパイル・実行し、localhost/helloにアクセスすると、以下のようなHTMLが返ってきます。

<!DOCTYPE html>
<html>
  <body>
    <h1>Hello, World!</h1>
    <p>This is Sample Page</p>
  </body>
</html>

まぁ、そのままですね

Styleを指定してみる

ここらへんは、Ktorというよりkotlinx.htmlの領域ですね。
h1{}や、p{}等のブロック内で、変数styleに対して代入することでstyle属性を指定することができます。
以下は、pブロックのフォントサイズと文字色をstyleで指定したコードです。

package test

import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.CallLogging
import io.ktor.features.DefaultHeaders
import io.ktor.http.ContentType
import io.ktor.response.respondText
import io.ktor.routing.Routing
import io.ktor.routing.get
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.html.*
import kotlinx.html.stream.appendHTML

fun Application.main() {
    embeddedServer(Netty) {
        install(DefaultHeaders)
        install(CallLogging)
        install(Routing) {
            get("/hello") {
                call.respondText(buildString {
                    appendln("<!DOCTYPE html>")
                    appendHTML().html {
                        body {
                            h1 { +"Hello, World!" }
                            p {
                                style = "font-size: 32px;color: #ac16ce;"
                                +"This is Sample Page" 
                            }
                        }
                    }
                    appendln()
                }, ContentType.Text.Html)
            }
        }
    }.start()
}

返ってくるHTMLがこちら

<!DOCTYPE html>
<html>
  <body>
    <h1>Hello, World!</h1>
    <p style="font-size: 32px;color: #ac16ce;">This is Sample Page</p>
  </body>
</html>

まぁ、p句のstyle属性にそのまま代入した文字列が使われてるだけですね。
Webデザインとかやってる人に怒られそうなコードです。

io.ktor.respondHtml

これは、ktor-html-builderで追加される拡張関数です。
まぁ、内部的にやってる処理はさっき書いたものと同じことなんですが、利用側では冗長な処理を消せ、若干スッキリとしたコードになります。
が、正直言ってこれのためだけに依存を1つ増やす価値があるかと言われると微妙でしょう。
まぁ、その話は後にして、とりあえずコードです。

package test

import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.CallLogging
import io.ktor.features.DefaultHeaders
import io.ktor.html.respondHtml
import io.ktor.routing.Routing
import io.ktor.routing.get
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.html.*

fun Application.main() {
    embeddedServer(Netty) {
        install(DefaultHeaders)
        install(CallLogging)
        install(Routing) {
            get("/hello") {
                call.respondHtml {
                    body {
                        h1 { +"Hello, World!" }
                        p {
                            style = "font-size: 32px;color: #ac16ce;"
                            +"This is Sample Page"
                        }
                    }
                }
            }
        }
    }.start()
}

書かなければならない処理が、先程のappendHtml.html{}内の処理だけになりましたね。
これだけでもだいぶ読みやすくなったように感じます。
ちなみに、HTML生成部分の処理は同じなので、返ってきたHTMLも同一の内容です。

CSSを埋め込んでスタイル指定する

一度、最初のコードを見てみましょう。
この部分です。

html {
    body {
        h1 { +"Hello, World!" }
        p { +"This is Sample Page" }
    }
}

これのタグ階層は、HTMLのタグの階層と一致しています。
ということは、head{}等もあるのか?となると思いますが、普通にあります。
なので、そこにstyle{}を使うことでCSSを埋め込むこともできます。
実際のコードがこちら

package test

import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.CallLogging
import io.ktor.features.DefaultHeaders
import io.ktor.html.respondHtml
import io.ktor.routing.Routing
import io.ktor.routing.get
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.html.*

fun Application.main() {
    embeddedServer(Netty) {
        install(DefaultHeaders)
        install(CallLogging)
        install(Routing) {
            get("/hello") {
                call.respondHtml {
                    head {
                        style {
                            +".hw{color: #060196;}"
                            +".test{font-size: 32px;color: #ac16ce;}"
                        }
                    }
                    body {
                        h1("hw") {
                            +"Hello, World!"
                        }
                        div("test") {
                            p { +"This is Sample Page" }
                        }
                    }
                }
            }
        }
    }.start()
}

返ってきたHTMLがこちら

<!DOCTYPE html>
<html>
  <head>
    <style>.hw{color: #060196;}.test{font-size: 32px;color: #ac16ce;}</style>
  </head>
  <body>
    <h1 class="hw">Hello, World!</h1>
    <div class="test">
      <p>This is Sample Page</p>
    </div>
  </body>
</html>

クラス間に空白すら無く、改行が一切無い、普通に書いたら絶対怒られるCSSですが、FireFoxでは普通に表示されました。
kotlinx.htmlにおいては、全てのHTMLタグに対応するクラス/メソッドが存在します
まぁ、自動生成らしいんですが。
なので、HTMLでのタグ階層を、関数ブロックによる階層構造で表現することができるというわけです。
ただの静的ページならこんなことせずに生で書けば良いと思いますが、サーバー側で動的生成して返す場合にはすごく便利で楽ですね。
Kotlinの便利機能を全面的に押し出した機能と言えるでしょう。

最後に

Ktorもkotlinx.htmlも、まだプレリリース状態で正式版では無いのですが、軽く触ってる分にはだいぶ便利だと思います。
Kotlinの機能をフル活用して作ってる感じが個人的にポイント高いですね。
ですが、ドキュメントが少なく手探りなところが多いので、もっと広まって情報が増えてくれればと思います。