公益平台网站怎么做,经典重庆论坛新闻论坛发展论坛,ftp网站后台,看网站的浏览器首发于Enaium的个人博客 前言
本文将介绍如何使用 Kotlin 全栈技术栈KtorKotlin/JS来构建一个简单的全栈应用。
准备工作
创建项目
首先我们需要创建一个Kotlin项目#xff0c;之后继续在其中新建两个子项目#xff0c;一个是Kotlin/JS项目#xff0c;另一个是Ktor项目。…首发于Enaium的个人博客 前言
本文将介绍如何使用 Kotlin 全栈技术栈KtorKotlin/JS来构建一个简单的全栈应用。
准备工作
创建项目
首先我们需要创建一个Kotlin项目之后继续在其中新建两个子项目一个是Kotlin/JS项目另一个是Ktor项目。
添加依赖和插件
这里我使用了Gradle的catalog在项目中的gradle目录下创建一个libs.versions.toml文件用于管理项目中的依赖版本。
[versions]
jimmer 0.0.9
kotlin 1.9.23
ktor 2.3.9
ksp 1.9.23-1.0.20
coroutines 1.8.0
serialization 1.6.3
wrappers 1.0.0-pre.729
logback 1.5.3
postgresql 42.7.3
hikari 5.1.0
koin 3.5.6[libraries]
ktor-server-core { module io.ktor:ktor-server-core-jvm, version.ref ktor }
ktor-server-netty { module io.ktor:ktor-server-netty-jvm, version.ref ktor }
ktor-server-cors { module io.ktor:ktor-server-cors, version.ref ktor }
ktor-server-content-negotiation { module io.ktor:ktor-server-content-negotiation, version.ref ktor }
ktor-serialization-jsackson { module io.ktor:ktor-serialization-jackson, version.ref ktor }
ktor-server-config-yaml { module io.ktor:ktor-server-config-yaml, version.ref ktor }
logback { module ch.qos.logback:logback-classic, version.ref logback }
kotlinx-coroutines-core { module org.jetbrains.kotlinx:kotlinx-coroutines-core, version.ref coroutines }
kotlinx-serialization-json { module org.jetbrains.kotlinx:kotlinx-serialization-json, version.ref serialization }
kotlin-wrappers { module org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom, version.ref wrappers }
kotlin-wrappers-react { module org.jetbrains.kotlin-wrappers:kotlin-react }
kotlin-wrappers-react-dom { module org.jetbrains.kotlin-wrappers:kotlin-react-dom }
kotlin-wrappers-emotion { module org.jetbrains.kotlin-wrappers:kotlin-emotion }
postgresql { module org.postgresql:postgresql, version.ref postgresql }
hikari { module com.zaxxer:HikariCP, version.ref hikari }
koin { module io.insert-koin:koin-ktor, version.ref koin }[bundles]
api [ktor-server-core, ktor-server-netty, ktor-server-cors, ktor-server-content-negotiation, ktor-serialization-jsackson, ktor-server-config-yaml, logback, postgresql, hikari, koin]
app [kotlinx-coroutines-core, kotlinx-serialization-json, kotlin-wrappers-react, kotlin-wrappers-react-dom, kotlin-wrappers-emotion][plugins]
jimmer { id cn.enaium.jimmer.gradle, version.ref jimmer }
kotlin-jvm { id org.jetbrains.kotlin.jvm, version.ref kotlin }
kotlin-multiplatform { id org.jetbrains.kotlin.multiplatform, version.ref kotlin }
ktor { id io.ktor.plugin, version.ref ktor }
ksp { id com.google.devtools.ksp, version.ref ksp }
kotlin-plugin-serialization { id org.jetbrains.kotlin.plugin.serialization, version.ref kotlin }之后我们分别在前端和后端项目中的build.gradle.kts文件中引入这些依赖和插件。
后端
plugins {alias(libs.plugins.kotlin.jvm)alias(libs.plugins.ktor)alias(libs.plugins.ksp)alias(libs.plugins.jimmer)application
}group cn.enaium
version 1.0.0application {mainClass cn.enaium.TodoKtapplicationDefaultJvmArgs listOf(-Dio.ktor.development${extra[development] ?: false})
}dependencies {implementation(libs.bundles.api)
}这里有一个配置添加到gradle.properties文件中。
developmenttrue前端
plugins {alias(libs.plugins.kotlin.multiplatform)alias(libs.plugins.kotlin.plugin.serialization)
}kotlin {js {browser {commonWebpackConfig {cssSupport {enabled.set(true)}}}binaries.executable()}sourceSets {val jsMain by getting {dependencies {implementation(project.dependencies.enforcedPlatform(libs.kotlin.wrappers))implementation(libs.bundles.app)}}}
}这里需要将前端项目的src/main改为src/jsMain。
最后进入到根项目的settings.gradle.kts文件中添加以下代码。
pluginManagement {repositories {maven(https://maven.pkg.jetbrains.space/public/p/compose/dev)google()gradlePluginPortal()mavenCentral()}
}dependencyResolutionManagement {repositories {google()mavenCentral()maven(https://maven.pkg.jetbrains.space/public/p/compose/dev)}
}还有gradle.build.kts文件中只保留以下代码。
plugins {alias(libs.plugins.kotlin.jvm) apply falsealias(libs.plugins.kotlin.multiplatform) apply false
}好了现在我们的项目已经准备好了。
编写代码
后端
首先创建配置文件src/main/resources/application.yml。
ktor:deployment:port: 8080application:modules:- cn.enaium.TodoKt.module
jdbc:driver: org.postgresql.Driverurl: jdbc:postgresql://localhost:5432/postgres?currentSchematodousername: postgrespassword: postgres之后创建logback配置文件src/main/resources/logback.xml。
configurationappender nameSTDOUT classch.qos.logback.core.ConsoleAppenderencoderpattern%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern/encoder/appenderroot leveltraceappender-ref refSTDOUT//rootlogger nameorg.eclipse.jetty levelINFO/logger nameio.netty levelINFO/
/configuration还有创建数据库。
drop schema if exists todo cascade;
create schema todo;drop table if exists todo.task;
create table todo.task
(id uuid primary key,name text not null,start_time timestamp default now(),end_time timestamp
)之后创建一个主类cn.enaium.Todo。
fun main(args: ArrayString) EngineMain.main(args)之后编写一个扩展函数cn.enaium.Todo.module。
fun Application.module() {}安装一些插件
Koin
install(Koin) {modules(module {singleApplicationEnvironment { environment }})
}CORS
install(CORS) {allowMethod(HttpMethod.Options)allowMethod(HttpMethod.Post)allowMethod(HttpMethod.Get)allowHeader(HttpHeaders.AccessControlAllowOrigin)allowHeader(HttpHeaders.ContentType)anyHost()
}Jackson
install(ContentNegotiation) {jackson {registerModules(ImmutableModule())}
}Jimmer
接下来配置一下Jimmer。
fun sql(environment: ApplicationEnvironment): KSqlClient {return newKSqlClient {setConnectionManager {HikariPool(HikariConfig().apply {driverClassName environment.config.property(jdbc.driver).getString()jdbcUrl environment.config.property(jdbc.url).getString()username environment.config.property(jdbc.username).getString()password environment.config.property(jdbc.password).getString()maximumPoolSize 10connectionTimeout 30000}).connection.use {proceed(it)}}setDialect(PostgresDialect())}
}之后添加到Koin中。
singleKSqlClient { sql(get()) }编写一个Task实体类。
package cn.enaium.entityimport org.babyfish.jimmer.sql.Entity
import org.babyfish.jimmer.sql.GeneratedValue
import org.babyfish.jimmer.sql.Id
import org.babyfish.jimmer.sql.Table
import org.babyfish.jimmer.sql.meta.UUIDIdGenerator
import java.util.*/*** author Enaium*/
Entity
Table(name task)
interface Task {IdGeneratedValue(generatorType UUIDIdGenerator::class)val id: UUIDval name: Stringval startTime: Dateval endTime: Date?
}接下来就可以编写Service了。
package cn.enaium.serviceimport cn.enaium.entity.Task
import cn.enaium.entity.endTime
import cn.enaium.entity.startTime
import org.babyfish.jimmer.sql.kt.KSqlClient
import org.babyfish.jimmer.sql.kt.ast.expression.isNotNull/*** author Enaium*/
class TodoServe(private val sql: KSqlClient) {fun getTasks(): ListTask {return sql.createQuery(Task::class) {orderBy(table.endTime.isNotNull(), table.startTime)select(table)}.execute()}fun saveTask(task: Task) {sql.save(task)}
}这里我们添加两个方法getTasks和saveTaskgetTasks用于获取所有任务并按照创建时间和是否完成排序saveTask用于保存任务之后还是添加到Koin中。
singleTodoServe { TodoServe(get()) }之后我们在module添加路由。
val todoServe by injectTodoServe()routing {get(/task) {call.respond(todoServe.getTasks())}post(/task) {todoServe.saveTask(call.receive())call.response.status(HttpStatusCode.OK)}
}前端
首先在src/jsMain/resources/index.html中添加以下代码这里需要注意的是app.js这个文件名称需要和前端的项目名称一致。
!doctype html
html langen
headmeta charsetUTF-8titleHello, Kotlin/JS!/title
/head
body
div idroot/div
script srcapp.js/script
/body
/html之后写一个main函数。
import react.dom.client.createRoot
import web.dom.document/*** author Enaium*/
fun main() {val container document.getElementById(root) ?: error(Couldnt find root container!)createRoot(container).render(App.create())
}val App FC {}然后就可以编写组件了。
首先需要创建两个data类一个是Task另一个是TaskInputTask用于展示任务TaskInput用于请求。
Serializable
data class Task(val id: String, var name: String, val startTime: Long, val endTime: Long?) {fun copy(name: String this.name, startTime: Long this.startTime, endTime: Long? this.endTime) Task(id, name, startTime, endTime)fun toInput() TaskInput(id, name, startTime, endTime)
}Serializable
data class TaskInput(val id: String? null,val name: String? null,val startTime: Long? null,val endTime: Long? null
)之后编写请求函数使用fetch发送请求。
val coroutine CoroutineScope(window.asCoroutineDispatcher())suspend fun fetchTasks(): ListTask {window.fetch(http://localhost:8080/task).await().let {if (it.status ! 200.toShort()) {throw Exception(Failed to fetch)}return Json.decodeFromDynamicListTask(it.json().await())}
}suspend fun saveTask(task: TaskInput) {window.fetch(http://localhost:8080/task,RequestInit(method POST,body Json.encodeToString(TaskInput.serializer(), task),headers json(Content-Type to application/json))).await().let {if (it.status ! 200.toShort()) {throw Exception(Failed to save)}}
}TaskItem
编写一个TaskItem组件用于展示任务编辑任务完成任务逻辑就是点击Edit按钮可以编辑任务按Enter保存按Escape取消点击Finish按钮完成任务。
external interface TaskItemProps : Props {var task: Task
}val TaskItem FCTaskItemProps { props -var editState by useState(false)var taskState by useStateTaskInput()useEffect(listOf(taskState)) {taskState?.let {coroutine.launch {saveTask(it)window.location.reload()}}}div {if (editState) {input {defaultValue props.task.nameonKeyUp {if (it.asDynamic().key Enter) {taskState props.task.copy(name it.target.asDynamic().value as String).toInput()editState false}if (it.asDynamic().key Escape) {editState false}}}} else {div {css {color if (props.task.endTime null) Color(red) else Color(green)}div {props.task.id}div {props.task.name}div {kotlin.js.Date(props.task.startTime).toLocaleString()props.task.endTime?.let { - kotlin.js.Date(it).toLocaleString()}}}button {EditonClick {editState !editState}}button {FinishonClick {taskState props.task.copy(endTime Date().getTime().toLong()).toInput()}}}}
}App
最后编写App组件获取任务列表添加任务。
val App FC {var tasksState by useState(emptyListTask())var taskState by useStateTaskInput()useEffectOnce {coroutine.launch {tasksState fetchTasks()}}useEffect(listOf(taskState)) {taskState?.let {coroutine.launch {saveTask(it)window.location.reload()}}}div {input {css {fontSize 24.px}onKeyUp {if (it.asDynamic().key Enter) {taskState TaskInput(name it.target.asDynamic().value as String)}}}div {css {marginTop 10.pxdisplay Display.flexflexDirection FlexDirection.columngap 10.px}tasksState.forEach {TaskItem {key it.idtask it}}}}
}运行
前端和后端默认端口都是8080所以先运行后端之后运行前端。
后端使用application插件的run任务前端使用jsBrowserDevelopmentRun任务。