Kotlin教程实战项目开发:构建一个全栈待办事项应用
在当今的软件开发领域,跨平台和全栈能力已成为开发者提升竞争力的关键。Kotlin,作为一门现代、简洁且安全的编程语言,不仅在Android原生开发中占据主导地位,更通过Kotlin Multiplatform(KMP)等框架,将其影响力扩展至后端、Web前端乃至桌面应用。本教程将带你进行一次实战演练,使用Kotlin为核心技术栈,结合你提到的Vue.js和Ubuntu环境,开发一个完整的全栈待办事项应用。我们将涵盖从后端API(使用Ktor框架)到前端界面(使用Compose for Web)的构建,并在Ubuntu开发环境中进行部署,为你串联起Kotlin、Vue.js和Android开发的关联知识。
项目概述与环境搭建
我们的目标是构建一个“TodoFull”应用,它包含一个用Kotlin Ktor编写的RESTful API后端,以及一个使用Jetpack Compose for Web(或作为备选方案,与Vue.js交互)构建的前端界面。我们选择在Ubuntu系统上进行开发,因为它是服务器和开发环境的流行选择。
1.1 开发环境准备(Ubuntu)
首先,确保你的Ubuntu系统已安装必要的工具。
- 安装Kotlin编译器:
sudo apt update
sudo apt install kotlin
- 安装JDK 11或更高版本:
sudo apt install openjdk-11-jdk
- 安装Ktor项目生成器(通过curl): 我们将使用Ktor的项目初始化工具。
curl -s https://get-ktor.io/ktor.jar -o ktor.jar
java -jar ktor.jar
对于前端,如果你选择使用Vue.js作为独立前端(与Kotlin后端分离),则需要安装Node.js和npm:
sudo apt install nodejs npm
npm install -g @vue/cli
1.2 创建Ktor后端项目
使用Ktor CLI创建一个新的后端项目。我们选择使用routing、content-negotiation(用于JSON序列化)和cors插件。
java -jar ktor.jar create TodoBackend -plugins=ktor.plugins.routing,ktor.plugins.content-negotiation,ktor.plugins.cors
cd TodoBackend
打开项目,主要的应用逻辑在src/Application.kt中。
构建Kotlin后端RESTful API
我们将实现一个简单的内存存储的待办事项API,包含创建、读取、更新和删除(CRUD)操作。
2.1 定义数据模型与模拟数据库
在Application.kt中,首先定义数据类(Data Class)和模拟的存储服务。
import kotlinx.serialization.Serializable
import java.util.concurrent.atomic.AtomicInteger
@Serializable
data class TodoItem(val id: Int, val title: String, val completed: Boolean)
object TodoService {
private val todos = mutableListOf()
private val idCounter = AtomicInteger(1)
fun getAll(): List = todos.toList()
fun getById(id: Int): TodoItem? = todos.find { it.id == id }
fun create(title: String): TodoItem {
val newItem = TodoItem(idCounter.getAndIncrement(), title, false)
todos.add(newItem)
return newItem
}
fun update(id: Int, title: String?, completed: Boolean?): TodoItem? {
val index = todos.indexOfFirst { it.id == id }
if (index == -1) return null
val oldItem = todos[index]
val updatedItem = oldItem.copy(
title = title ?: oldItem.title,
completed = completed ?: oldItem.completed
)
todos[index] = updatedItem
return updatedItem
}
fun delete(id: Int): Boolean = todos.removeIf { it.id == id }
}
2.2 配置插件与路由
在Application.module()函数中,安装并配置必要的插件,然后定义路由。
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.serialization.*
fun Application.module() {
install(ContentNegotiation) {
json() // 启用JSON序列化/反序列化
}
install(CORS) {
anyHost() // 在生产环境中应限制来源
allowHeader(HttpHeaders.ContentType)
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Get)
allowMethod(HttpMethod.Post)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
}
routing {
route("/api/todos") {
get {
call.respond(TodoService.getAll())
}
get("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid ID")
return@get
}
val item = TodoService.getById(id)
if (item == null) {
call.respond(HttpStatusCode.NotFound)
} else {
call.respond(item)
}
}
post {
val request = call.receive()
if (request.title.isBlank()) {
call.respond(HttpStatusCode.BadRequest, "Title is required")
return@post
}
val newItem = TodoService.create(request.title)
call.respond(HttpStatusCode.Created, newItem)
}
put("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid ID")
return@put
}
val request = call.receive()
val updatedItem = TodoService.update(id, request.title, request.completed)
if (updatedItem == null) {
call.respond(HttpStatusCode.NotFound)
} else {
call.respond(updatedItem)
}
}
delete("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid ID")
return@delete
}
val success = TodoService.delete(id)
if (success) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound)
}
}
}
}
}
@Serializable
data class CreateTodoRequest(val title: String)
@Serializable
data class UpdateTodoRequest(val title: String? = null, val completed: Boolean? = null)
现在,你可以运行后端服务器:./gradlew run。API将在 http://localhost:8080 上可用。
构建前端界面:Kotlin/JS与Vue.js的抉择
你有两个主要选择来构建前端:使用Kotlin的Compose for Web(共享Kotlin逻辑),或使用更传统的Vue.js。这里我们简要介绍两种思路。
3.1 方案A:使用Jetpack Compose for Web(纯Kotlin)
这是Kotlin Multiplatform的愿景之一。你可以在同一个Kotlin项目中创建一个jsMain源集。
- 在
build.gradle.kts中配置Kotlin/JS和Compose。 - 在
jsMain/kotlin中编写Compose UI代码,直接调用通过ktor-client封装的API。 - 编译成静态JavaScript文件,并可由任何Web服务器托管。
这种方式实现了前后端语言统一,但Compose for Web仍处于积极发展阶段。
3.2 方案B:使用Vue.js(分离前端)
这是更成熟和常见的选择。我们创建一个独立的Vue.js项目来消费Kotlin后端API。
- 创建Vue项目:
vue create todo-frontend
cd todo-frontend
- 安装Axios(用于HTTP请求):
npm install axios
- 创建组件(例如
TodoList.vue): 下面是一个简化的示例,展示如何与我们的Kotlin API交互。
<template>
<div>
<input v-model="newTodoTitle" @keyup.enter="addTodo" placeholder="添加新任务..."/>
<ul>
<li v-for="todo in todos" :key="todo.id">
<input type="checkbox" v-model="todo.completed" @change="updateTodo(todo)"/>
<span :class="{ completed: todo.completed }">{{ todo.title }}</span>
<button @click="deleteTodo(todo.id)">删除</button>
</li>
</ul>
</div>
</template>
<script>
import axios from 'axios';
const API_URL = 'http://localhost:8080/api/todos';
export default {
name: 'TodoList',
data() {
return {
todos: [],
newTodoTitle: ''
};
},
mounted() {
this.fetchTodos();
},
methods: {
async fetchTodos() {
try {
const response = await axios.get(API_URL);
this.todos = response.data;
} catch (error) {
console.error('获取待办事项失败:', error);
}
},
async addTodo() {
if (!this.newTodoTitle.trim()) return;
try {
const response = await axios.post(API_URL, { title: this.newTodoTitle });
this.todos.push(response.data);
this.newTodoTitle = '';
} catch (error) {
console.error('添加待办事项失败:', error);
}
},
async updateTodo(todo) {
try {
await axios.put(`${API_URL}/${todo.id}`, {
title: todo.title,
completed: todo.completed
});
} catch (error) {
console.error('更新待办事项失败:', error);
}
},
async deleteTodo(id) {
try {
await axios.delete(`${API_URL}/${id}`);
this.todos = this.todos.filter(todo => todo.id !== id);
} catch (error) {
console.error('删除待办事项失败:', error);
}
}
}
};
</script>
<style scoped>
.completed {
text-decoration: line-through;
color: #888;
}
</style>
运行npm run serve,Vue应用将在另一个端口(如http://localhost:8081)启动,并跨域请求Kotlin后端。这演示了现代前后端分离架构。
在Ubuntu上部署与整合
开发完成后,你可能希望将应用部署到Ubuntu服务器上。
4.1 打包后端应用
使用Gradle的shadowJar(或installDist)插件创建一个包含所有依赖的可执行JAR。
./gradlew build
# 生成的JAR通常在 build/libs/ 目录下
java -jar build/libs/TodoBackend-all.jar
4.2 部署前端静态资源
构建Vue.js项目,生成静态文件。
npm run build
生成的dist文件夹中的内容,可以:
- 托管到Nginx/Apache: 将
dist内容复制到Web服务器根目录(如/var/www/html)。 - 由Ktor服务托管: 你甚至可以修改Ktor后端,使用
static插件来直接服务这些前端文件,从而实现一个单体部署。在Application.kt中添加:
install(Compression)
install(StaticContent) {
static("/") {
files("frontend-dist") // 假设你把Vue的dist内容放在这里
default("index.html")
}
}
这样,访问 http://your-server-ip:8080 就能看到完整的应用。
4.3 使用PM2管理进程(可选)
为了确保后端JAR在服务器上持续运行,可以使用PM2(Node.js进程管理器)来管理Java进程。
sudo npm install -g pm2
pm2 start java --name "todo-backend" -- -jar /path/to/TodoBackend-all.jar
pm2 save
pm2 startup
总结
通过本实战教程,我们完成了一个从零开始的全栈“TodoFull”应用开发。我们利用Kotlin和Ktor高效构建了健壮的后端REST API,并探讨了两种前端实现路径:一是拥抱Kotlin生态的Compose for Web,二是采用更广泛使用的Vue.js框架。整个开发流程在Ubuntu环境中进行,涵盖了环境搭建、编码、调试和基础部署。
这个项目虽然简单,但清晰地展示了现代全栈开发的核心模式:前后端分离、API驱动、跨平台语言的应用。无论你是专注于Android开发的Kotlin开发者,希望向后端拓展,还是Web开发者想探索Kotlin的潜力,这个练习都提供了一个坚实的起点。你可以在此基础上继续扩展,例如引入数据库(如PostgreSQL)、添加用户认证、或尝试将业务逻辑通过Kotlin Multiplatform在Android、iOS和Web之间共享,从而深入体验Kotlin作为全栈语言的强大能力。




