HTML

  • Ktor és un servidor HTML ...

    Introducció

    Ktor està construït des de zero utilitzant Corutines i et permet utilitzar només el que necessites i estructurar la teva aplicació de la manera que necessitis.

    A més, també pots estendre Ktor amb el teu propi plugin molt fàcilment.

    Entorn de treball

    Genera un projecte “Ktor server application” amb Amper.

    Pots veure que s’han creat dos fitxers dins de src amb els noms Application.kt i Routing.kt

    Executa el projecte:

    Per confirmar que el projecte s’està executant, obre el navegador a l’URL especificat: http://localhost:8080.

    Hauries de veure el missatge “Hello World!” mostrat a la pantalla:

    Reiniciar un servidor durant el desenvolupament pot trigar una estona. Ktor et permet superar aquesta limitació utilitzant la recàrrega automàtica, que recarrega les classes de l’aplicació quan hi ha canvis en el codi i proporciona un cicle de retroalimentació ràpid.

    ktor:
        development: true

    No funciona 🤨: Auto-reload

    HTML DSL integra la biblioteca kotlinx.html a Ktor i et permet respondre a un client amb blocs HTML. Amb HTML DSL, pots escriure HTML pur en Kotlin, interpolar variables en les vistes i fins i tot construir dissenys HTML complexos utilitzant plantilles.

    HTML DSL no necessita instal·lació, però requereix l’artefacte ktor-server-html-builder:

    dependencies:
      - $ktor.server.htmlBuilder

    Modifica el fitxer Routing.kt amb codi “DSL”:

    fun Application.configureRouting() {
        routing {
            get("/") {
                call.respondHtml {
                    body { h1 { +"Hello World!" } }
                }
            }
        }
    }

    Gestor de tasques

    Ara construiràs incrementalment una aplicació de Gestor de Tasques amb la següent funcionalitat:

    • Veure totes les tasques disponibles en una taula HTML.
    • Veure les tasques per prioritat i nom, també en HTML.
    • Afegir tasques addicionals mitjançant un formulari HTML.

    Mostrar contingut HTML estàtic

    En la primera iteració afegiràs una nova ruta a la teva aplicació que retornarà contingut HTML estàtic.

    Obre el fitxer Routing.kt i modifica la funció Application.configureRouting() creating a new route for the URL /tasks and the GET request type:

    import kotlinx.html.*
    
    fun Application.configureRouting() {
        routing {
            get("/") {
                call.respondText("Hello David!")
            }
            get("/tasks") {
                call.respondHtml {
                    body {
                        h1 {
                            +"Tasks"
                        }
                        ul {
                            li {
                                +"Create project documentation"
                            }
                            li {
                                +"Implement user authentication"
                            }
                        }
                    }
                }
            }
        }
    }

    Una petició GET és el tipus de petició més bàsic en HTTP. S’activa quan l’usuari escriu a la barra d’adreces del navegador o fa clic en un enllaç HTML normal.

    De moment només estàs retornant contingut estàtic. Per notificar al client que enviaràs HTML, has de configurar la capçalera HTTP Content Type a “text/html”.

    Navega a http://localhost:8080/tasks al teu navegador.

    Hauries de veure la llista de tasques:

    Tasks

    • Create project documentation
    • Implement user authentication

    Implementar un Model de Tasca

    1. Dins de src crea un nou subpaquet anomenat model.

    2. Dins del directori model crea un nou fitxer Task.kt.

    3. Obre el fitxer Task.kt i afegeix el següent enum per representar prioritats i una classe per representar tasques:

    package org.jetbrains.amper.ktor.model
    
    enum class Priority {
        Low, Medium, High, Vital
    }
    data class Task(
        val name: String,
        val description: String,
        val priority: Priority
    )

    Com enviaràs informació de tasques al client dins de taules HTML, també afegeix la següent funció d’extensió:

    import kotlinx.html.*
    
    fun List<Task>.asTable(body: BODY) = body.table {
        thead {
            tr {
                th { +"Name" }
                th { +"Description" }
                th { +"Priority" }
            }
            this@asTable.map {
                tr {
                    td { +it.name }
                    td { +it.description }
                    td { +it.priority.name }
                }
            }
        }
    }

    La funció List<Task>.asTable() permet que una llista de tasques es mostri com una taula.

    1. Obre el fitxer Routing.kt i modifica la funció existent Application.configureRouting() amb la implementació següent:
    import org.jetbrains.amper.ktor.model.*
    
    fun Application.configureRouting() {
        routing {
            get("/tasks") {
                call.respondHtml {
                    body {
                        tasks.asTable(this)
                    }
                }
            }
        }
    }

    En lloc de retornar contingut estàtic al client, ara estàs proporcionant una llista de tasques. Com que una llista no es pot enviar directament per la xarxa, s’ha de convertir a un format que el client pugui entendre. En aquest cas, les tasques es converteixen en una taula HTML.

    Fes clic al botó de reinici per reiniciar l’aplicació.

    Navega a http://localhost:8080/tasks al teu navegador. Hauria de mostrar una taula HTML amb les tasques.

    Refactor the model

    Abans de continuar ampliant la funcionalitat de la teva aplicació, has de refactoritzar el disseny encapsulant la llista de valors dins d’un repositori. Això et permetrà centralitzar la gestió de dades i així concentrar-te en el codi específic de Ktor.

    Torna al fitxer TaskRepository.kt i substitueix la llista existent de tasques amb el codi següent:

    object TaskRepository {
        private val tasks = mutableListOf(
            Task("cleaning", "Clean the house", Priority.Low),
            Task("gardening", "Mow the lawn", Priority.Medium),
            Task("shopping", "Buy the groceries", Priority.High),
            Task("painting", "Paint the fence", Priority.Medium)
        )
    
        fun allTasks(): List<Task> = tasks
    
        fun tasksByPriority(priority: Priority) = tasks.filter {
            it.priority == priority
        }
    
        fun taskByName(name: String) = tasks.find {
            it.name.equals(name, ignoreCase = true)
        }
    
        fun addTask(task: Task) {
            if (taskByName(task.name) != null) {
                throw IllegalStateException("Cannot duplicate task names!")
            }
            tasks.add(task)
        }
    }

    Això implementa un magatzem de dades molt simple per a tasques basat en una llista. Per als propòsits de l’exemple, es mantindrà l’ordre en què s’afegeixen les tasques, però no es permetran duplicats llançant una excepció.

    Obre el fitxer Routing.kt i substitueix la funció Application.configureRouting() existent amb la implementació següent:

    fun Application.configureRouting() {
        routing {
            get("/") {
                call.respondText("Hello World!")
            }
            get("/tasks") {
                val tasks = TaskRepository.allTasks()
                call.respondHtml {
                    body {
                        tasks.asTable(this)
                    }
                }
            }
        }
    }

    Treballar amb paràmetres

    En aquesta iteració, permetràs a l’usuari veure tasques per prioritat. Per fer això, la teva aplicació ha de permetre peticions GET a les següents URLs:

    La ruta que afegiràs és /tasks/priority/{priority?} on {priority?} representa un paràmetre de ruta que hauràs d’extreure en temps d’execució, i el signe d’interrogació s’utilitza per indicar que un paràmetre és opcional. El paràmetre de consulta pot tenir qualsevol nom que vulguis, però priority sembla l’opció òbvia.

    Obre el fitxer Routing.kt i afegeix la següent ruta al teu codi, com es mostra a continuació:

    fun Application.configureRouting() {
        routing {
    
            // ...
    
            get("/tasks/priority/{priority}") {
                val priorityAsText = call.parameters["priority"]
                if (priorityAsText == null) {
                    call.respond(HttpStatusCode.BadRequest)
                    return@get
                }
    
                try {
                    val priority = Priority.valueOf(priorityAsText.replaceFirstChar { it.uppercase() })
                    val tasks = TaskRepository.tasksByPriority(priority)
    
                    if (tasks.isEmpty()) {
                        call.respond(HttpStatusCode.NotFound)
                        return@get
                    }
    
                    call.respondHtml {
                        body {
                            tasks.asTable(this)
                        }
                    }
                } catch (e: IllegalArgumentException) {
                    call.respond(HttpStatusCode.BadRequest)
                }
            }
        }
    }

    Com s’ha resumit anteriorment, has escrit un gestor per a l’URL /tasks/priority/{priority?}. El símbol priority representa el paràmetre de ruta que l’usuari ha afegit. Malauradament, al servidor no hi ha manera de garantir que aquest sigui un dels quatre valors de l’enumeració Kotlin corresponent, així que s’ha de comprovar manualment.

    Si el paràmetre de ruta està absent, el servidor retorna un codi d’estat 400 al client. En cas contrari, extreu el valor del paràmetre i intenta convertir-lo a un membre de l’enumeració. Si això falla, es llançarà una excepció, que el servidor captura i retorna un codi d’estat 400.

    Suposant que la conversió tingui èxit, el repositori s’utilitza per trobar les Tasques coincidents. Si no hi ha tasques de la prioritat especificada, el servidor retorna un codi d’estat 404, en cas contrari envia les coincidències en una taula HTML.

    Pots provar aquesta funcionalitat al navegador sol·licitant les diferents URLs.

    Malauradament, les proves que pots fer a través del navegador són limitades en el cas d’errors. El navegador no mostrarà els detalls d’una resposta fallida, tret que utilitzis extensions de desenvolupador.

    Una alternativa més senzilla seria utilitzar una eina especialitzada, com HTTP Client:

    Afageix tests unitaris

    Fins ara has afegit dues rutes - una per recuperar totes les tasques i una per recuperar tasques per prioritat. Eines com HTTP Client et permeten provar completament aquestes rutes, però requereixen inspecció manual i s’executen externament a Ktor.

    Això és acceptable quan es fa prototipatge i en aplicacions petites. No obstant això, aquest enfocament no escala a aplicacions grans on hi pot haver milers de proves que necessiten executar-se freqüentment. Una millor solució és automatitzar completament les proves.

    Ktor proporciona el seu propi marc de proves per donar suport a la validació automatitzada de rutes. A continuació, escriuràs algunes proves per a la funcionalitat existent de la teva aplicació.

    1. Crea un nou directori anomenat test.
    2. Dins de test crea un nou fitxer ApplicationTest.kt.
    3. Obre el fitxer ApplicationTest.kt i afegeix el següent codi:
    package org.jetbrains.amper.ktor
    
    import io.ktor.client.request.*
    import io.ktor.client.statement.*
    import io.ktor.http.*
    import io.ktor.server.testing.*
    import kotlin.test.Test
    import kotlin.test.assertContains
    import kotlin.test.assertEquals
    
    class ApplicationTest {
        @Test
        fun tasksCanBeFoundByPriority() = testApplication {
            application {
                module()
            }
    
            val response = client.get("/tasks/priority/medium")
            val body = response.bodyAsText()
    
            assertEquals(HttpStatusCode.OK, response.status)
            assertContains(body, "Mow the lawn")
            assertContains(body, "Paint the fence")
        }
    
        @Test
        fun invalidPriorityProduces400() = testApplication {
            application {
                module()
            }
    
            val response = client.get("/tasks/priority/invalid")
            assertEquals(HttpStatusCode.BadRequest, response.status)
        }
    
        @Test
        fun unusedPriorityProduces404() = testApplication {
            application {
                module()
            }
    
            val response = client.get("/tasks/priority/vital")
            assertEquals(HttpStatusCode.NotFound, response.status)
        }
    }

    En cadascuna d’aquestes proves es crea una nova instància de Ktor. Aquesta s’executa dins d’un entorn de proves, en lloc d’un servidor web com Netty. Es carrega el mòdul escrit per tu per Amper, que al seu torn invoca la funció de routing. Després pots utilitzar l’objecte client incorporat per enviar sol·licituds a l’aplicació i validar les respostes que es retornen.

    La prova es pot executar dins l’IDE o com a part del teu pipeline CI/CD.

    Per executar les proves dins l’IDE, fes clic a la icona de la canal al costat de cada funció de prova.

    Gestionar peticions POST

    You can follow the process described above to create any number of additional routes for GET requests. These would allow the user to fetch tasks using whatever search criteria we like. But users will also want to be able to create new tasks.

    In that case the appropriate type of HTTP request is a POST. A POST request is typically triggered when a user completes and submits an HTML form.

    Unlike a GET request, a POST request has a body, which contains the names and values of all the inputs that are present on the form. This information is encoded to separate the data from different inputs and to escape illegal characters. You do not need to worry about the details of this process, as the browser and Ktor will manage it for us.

    Crea el fitxer TaskUI.kt:

    package org.jetbrains.amper.ktor.ui
    
    import kotlinx.html.*
    
    object TaskUI {
        fun form(body: BODY, action: String) = body.div {
            h1 {
                + "Adding a new task"
            }
            form(method = FormMethod.post, action = action) {
                div {
                    label {
                        htmlFor = "name"
                        + "Name: "
                    }
                    input(type = InputType.text, name = "name") {
                        id = "name"
                        size = "10"
                    }
                }
                div {
                    label {
                        htmlFor = "description"
                        + "Description: "
                    }
                    input(type = InputType.text, name = "description") {
                        id = "description"
                        size = "20"
                    }
                }
                div {
                    label {
                        htmlFor = "priority"
                        + "Priority: "
                    }
                    select {
                        id = "priority"
                        name = "priority"
                        option {
                            value = "Low"
                            +"Low"
                        }
                        option {
                            value = "Medium"
                            +"Medium"
                        }
                        option {
                            value = "High"
                            +"High"
                        }
                        option {
                            value = "Vital"
                            +"Vital"
                        }
    
                    }
                }
                input(type = InputType.submit) {
                }
            }
        }
    }

    Crea una nova ruta a Routing.kt per mostrar el formulari a l’usuari:

        get("/task/new") {
            call.respondHtml {
                body {
                    TaskUI.form(this, "/task/new")
                }
            }
        }

    Add a handler for the form

    A continuació afegeix un “handler” pel formulari a la mateixa ruta:

    As you can see the new route is mapped to POST requests rather than GET requests. Ktor processes the body of the request via the call to receiveParameters(). This returns a collection of the parameters that were present in the body of the request.

    There are three parameters, so you can store the associated values in a Triple. If a parameter is not present then an empty string is stored instead.

    If any of the values are empty, the server will return a response with a status code of 400. Then, it will attempt to convert the third parameter to a Priority and, if successful, add the information to the repository in a new Task. Both of these actions may result in an exception, in which case once again return a status code 400.

    Otherwise, if everything is successful, the server will return a 204 status code ( No Content) to the client. This signifies that their request has succeeded, but there’s no fresh information to send them as a result.

    Test the finished functionality

    Navigate to http://localhost:8080/task/new in the browser.

    Fill in the form with sample data and click Submit.

    When you submit the form you should not be directed to a new page.

    Navigate to the URL http://localhost:8080/task.

    You should see that the new task has been added.

    To validate the functionality, add the following test to ApplicationTest.kt:

        @Test
        fun newTasksCanBeAdded() = testApplication {
            application {
                module()
            }
    
            val response1 = client.post("/task/new") {
                header(
                    HttpHeaders.ContentType,
                    ContentType.Application.FormUrlEncoded.toString()
                )
                setBody(
                    listOf(
                        "name" to "swimming",
                        "description" to "Go to the beach",
                        "priority" to "Low"
                    ).formUrlEncode()
                )
            }
    
            assertEquals(HttpStatusCode.NoContent, response1.status)
    
            val response2 = client.get("/task")
            assertEquals(HttpStatusCode.OK, response2.status)
            val body = response2.bodyAsText()
    
            assertContains(body, "swimming")
            assertContains(body, "Go to the beach")
        }

    In this test two requests are sent to the server, a POST request to create a new task and a GET request to confirm the new task has been added.

    When making the first request, the setBody() method is used to insert content into the body of the request.

    The test framework provides a formUrlEncode() extension method on collections, which abstracts the process of formatting the data as the browser would.

    Refactor the routing

    If you examine your routing thus far you will see that all the routes begin with /task.

    You can remove this duplication by placing them into their own sub-route:

    TODO