Guides: How to create a plain website using ktor

预计阅读时间:7分钟

在本指南中,您将学习如何使用Ktor创建HTML网站. 我们将创建一个简单的网站,在后端向用户呈现HTML,并使用登录表单并保持持久会话.

为此,我们将使用RoutingStatusPagesAuthenticationSessionsStaticContentFreeMarkerHTML DSL功能.

目录:

Setting up the project

第一步是建立一个项目. 您可以按照《 快速入门》指南,或使用以下表单创建表单:

the pre-configured generator form

Simple routing

首先,我们将使用路由功能. 此功能是Ktor核心的一部分,因此您无需包括任何其他工件.

使用routing { }块时,将自动安装此功能.

让我们开始创建一个简单的GET路由,以" OK"响应:

fun Application.module() {
    routing {
        get("/") {
            call.respondText("OK")
        }
    }
}

Serving HTML with FreeMarker

Apache FreeMarker是JVM的模板引擎,因此您可以将其与Kotlin一起使用. 有一个支持它的Ktor功能.

现在,我们将将作为资源一部分嵌入的模板存储在templates文件夹中.

创建一个名为resources/templates/index.ftl的文件,并放入以下内容以创建一个简单的HTML列表:

<#-- @ftlvariable name="data" type="com.example.IndexData" -->
<html>
	<body>
		<ul>
		<#list data.items as item>
			<li>${item}</li>
		</#list>
		</ul>
	</body>
</html>

IntelliJ IDEA Ultimate具有FreeMarker支持以及自动补全和变量提示功能.

现在,让我们安装FreeMarker功能,然后创建为该模板提供服务的路由并将一组值传递给它:

data class IndexData(val items: List<Int>)

fun Application.module() {
    install(FreeMarker) {
        templateLoader = ClassTemplateLoader(this::class.java.classLoader, "templates")
    }
    
    routing {
        get("/html-freemarker") {
            call.respond(FreeMarkerContent("index.ftl", mapOf("data" to IndexData(listOf(1, 2, 3))), ""))
        }
    }
}

现在,您可以运行服务器并打开指向http://127.0.0.1:8080/html-freemarker的浏览器以查看结果:

Nice!

Serving static files: styles, scripts, images…

除了模板之外,您还需要提供静态内容. 静态内容的投放速度更快,并且与"部分内容"等其他功能兼容,该功能可让您恢复下载或部分下载文件.

For now, we are going to serve a simple styles.css file to apply styles to our simple page.

提供静态文件不需要安装任何功能,但它是简单的Route处理程序. 要从/resources/static/static网址提供静态文件,您需要编写以下代码:

routing {
    // ...
    static("/static") {
        resources("static")
    }
}

现在,让我们创建具有以下内容的resources/static/styles.css文件:

body {
    background: #B9D8FF;
}

除此之外,我们还必须更新模板以包含style.css文件:

<#-- @ftlvariable name="data" type="com.example.IndexData" -->
<html>
    <head>
        <link rel="stylesheet" href="/static/styles.css">
    </head>
    <body>
	<!-- ... -->
    </body>
</html>

结果:

现在我们有了一个始于1990年的丰富多彩的网站!

静态文件不仅是文本文件! 尝试将图像(花式动画闪烁gif文件如何?)添加到static文件夹,并在HTML模板中包含<img src="...">标签.

Enabling partial content: large files and videos

尽管在特定情况下并不需要,但是如果启用部分内容支持,人们将能够在出现频繁问题的连接上恢复较大的静态文件,或者在提供和观看视频时寻求支持.

启用部分内容非常简单:

install(PartialContent) {
}

Creating a form

现在,我们将创建一个伪造的登录表单. 为简单起见,我们将接受具有相同密码的用户,并且我们将不执行注册表格.

创建一个resources/templates/login.ftl

<html>
<head>
    <link rel="stylesheet" href="/static/styles.css">
</head>
<body>
<#if error??>
    <p style="color:red;">${error}</p>
</#if>
<form action="/login" method="post" enctype="application/x-www-form-urlencoded">
    <div>User:</div>
    <div><input type="text" name="username" /></div>
    <div>Password:</div>
    <div><input type="password" name="password" /></div>
    <div><input type="submit" value="Login" /></div>
</form>
</body>
</html>

除了模板之外,我们还需要向其中添加一些逻辑. 在这种情况下,我们将在不同的代码块中处理GET和POST方法:

route("/login") {
    get {
        call.respond(FreeMarkerContent("login.ftl", null))
    }
    post {
        val post = call.receiveParameters()
        if (post["username"] != null && post["username"] == post["password"]) {
            call.respondText("OK")
        } else {
            call.respond(FreeMarkerContent("login.ftl", mapOf("error" to "Invalid login")))
        }
    }
}

如前所述,我们接受具有相同password username ,但我们不接受空值. 如果登录名有效,我们现在仅需单击一次OK,而如果登录名无法显示相同形式但出现错误,我们将重新使用模板.

Redirections

在某些情况下,例如路由重构或表单,我们将要执行重定向(临时或永久重定向). 在这种情况下,我们希望在成功登录后暂时重定向到主页,而不是使用纯文本回复.

Original:Change:
call.respondText("OK")
call.respondRedirect("/", permanent = false)

Using the Form authentication

为了说明如何接收POST参数,我们已经手动处理了登录,但是我们也可以将身份验证功能与表单提供程序一起使用:

install(Authentication) {
    form("login") {
        userParamName = "username"
        passwordParamName = "password"
        challenge = FormAuthChallenge.Unauthorized
        validate { credentials -> if (credentials.name == credentials.password) UserIdPrincipal(credentials.name) else null }
    }
}
route("/login") {
    get {
        // ...
    }
    authenticate("login") {
        post {
            val principal = call.principal<UserIdPrincipal>()
            call.respondRedirect("/", permanent = false)
        }
    }
}

Sessions

为了避免对所有页面进行身份验证,我们将用户存储在会话中,并且该会话将使用会话cookie传播到所有页面.

data class MySession(val username: String)

fun Application.module() {
    install(Sessions) {
        cookie<MySession>("SESSION")
    }
    routing {
        authenticate("login") {
            post {
                val principal = call.principal<UserIdPrincipal>() ?: error("No principal")
                call.sessions.set("SESSION", MySession(principal.name))
                call.respondRedirect("/", permanent = false)
            }
        }
    }
} 

在页面内,我们可以尝试获取会话并产生不同的结果:

fun Application.module() {
    // ...
    get("/") {
        val session = call.sessions.get<MySession>()
        if (session != null) {
            call.respondText("User is logged")
        } else {
            call.respond(FreeMarkerContent("index.ftl", mapOf("data" to IndexData(listOf(1, 2, 3))), ""))
        }
    }
}

Using HTML DSL instead of FreeMarker

您可以选择直接从代码生成HTML,而不使用模板引擎. 为此,您可以使用HTML DSL. 该DSL不需要安装,但是需要其他工件(有关详细信息,请参见HTML DSL ). 该工件提供了扩展以响应HTML块:

get("/") { 
    val data = IndexData(listOf(1, 2, 3))
    call.respondHtml {
        head {
            link(rel = "stylesheet", href = "/static/styles.css")
        }
        body {
            ul {
                for (item in data.items) {
                    li { +"$item" }                
                }
            }
        }
    }
}

HTML DSL的主要好处是您可以完全静态地访问变量,并且可以与代码库完全集成.

所有这些的缺点是,您必须重新编译才能更改HTML,并且无法搜索完整的HTML块. 但这很快,您可以使用autoreload功能在更改时重新编译并重新加载相关的JVM类.

Exercises

Exercise 1

创建一个注册页面,并将用户/密码数据源存储在哈希图中的内存中.

Exercise 2

使用数据库存储用户.

by  ICOPY.SITE