用Spring Boot 3和Vue 3打造Todo应用:全栈开发保姆级教程

发布时间:2026/5/17 2:33:02

用Spring Boot 3和Vue 3打造Todo应用:全栈开发保姆级教程 从零构建现代化Todo应用Spring Boot 3与Vue 3全栈实践指南在当今快速迭代的技术环境中全栈开发能力已成为开发者职业发展的关键竞争力。本教程将带您从零开始通过构建一个功能完整的Todo应用掌握Spring Boot 3后端与Vue 3前端协同开发的核心技能。不同于简单的CRUD示例我们将深入探讨现代化全栈开发中的最佳实践包括TypeScript的类型安全、响应式状态管理、RESTful API设计等实用技巧。1. 开发环境与项目初始化工欲善其事必先利其器。在开始编码前我们需要配置一套高效的开发环境后端工具链JDK 17Spring Boot 3最低要求IntelliJ IDEA社区版即可满足需求Maven 3.8或Gradle 7.6Postman或Insomnia用于API测试前端工具链Node.js 18推荐使用nvm进行版本管理VS Code配合Volar插件获得最佳Vue开发体验Chrome浏览器配合Vue Devtools提示建议使用Docker运行数据库服务避免本地环境配置问题。本教程将使用PostgreSQL作为生产级数据库开发时也可选择H2内存数据库。后端项目初始化# 使用Spring Initializr创建项目 curl https://start.spring.io/starter.zip \ -d typegradle-project \ -d languagejava \ -d bootVersion3.1.0 \ -d baseDirtodo-backend \ -d groupIdcom.example \ -d artifactIdtodo \ -d dependenciesweb,data-jpa,postgresql,lombok \ -o todo-backend.zip前端项目初始化# 创建Vue 3 TypeScript项目 npm create vitelatest todo-frontend -- --template vue-ts cd todo-frontend npm install axios vue-router4 pinia vueuse/core2. 后端核心架构设计2.1 领域模型与数据库设计我们采用DDD领域驱动设计的轻量级实现方式首先定义Todo聚合根Entity Table(name todos) Getter Setter NoArgsConstructor AllArgsConstructor Builder public class Todo { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(nullable false) private String title; private String description; Column(nullable false) private boolean completed; Column(nullable false) private LocalDateTime createdAt; private LocalDateTime updatedAt; PrePersist protected void onCreate() { createdAt LocalDateTime.now(); } PreUpdate protected void onUpdate() { updatedAt LocalDateTime.now(); } }2.2 分层架构实现采用经典的三层架构但引入一些现代化改进src/main/java/com/example/todo ├── application # 应用服务层 ├── domain # 领域模型 ├── infrastructure # 基础设施层 │ ├── config │ ├── repository │ └── web └── TodoApplication.javaRepository接口示例public interface TodoRepository extends JpaRepositoryTodo, Long { ListTodo findByCompleted(boolean completed); Query(SELECT t FROM Todo t WHERE LOWER(t.title) LIKE LOWER(CONCAT(%, :query, %))) ListTodo searchByTitle(Param(query) String query); }2.3 RESTful API设计规范遵循API最佳实践我们设计清晰的端点规范HTTP方法路径描述GET/api/todos获取所有Todo项GET/api/todos/{id}获取指定ID的Todo项POST/api/todos创建新的Todo项PUT/api/todos/{id}更新指定ID的Todo项DELETE/api/todos/{id}删除指定ID的Todo项控制器实现示例RestController RequestMapping(/api/todos) RequiredArgsConstructor public class TodoController { private final TodoService todoService; GetMapping public ResponseEntityListTodoResponse getAllTodos( RequestParam(required false) Boolean completed) { ListTodo todos completed ! null ? todoService.findByCompleted(completed) : todoService.findAll(); return ResponseEntity.ok(todos.stream() .map(TodoResponse::fromDomain) .toList()); } PostMapping public ResponseEntityTodoResponse createTodo( Valid RequestBody CreateTodoRequest request) { Todo todo todoService.create(request.toDomain()); return ResponseEntity .created(URI.create(/api/todos/ todo.getId())) .body(TodoResponse.fromDomain(todo)); } }3. 前端架构与状态管理3.1 Vue 3组合式API实践使用script setup语法简化组件编写script setup langts import { ref, onMounted } from vue import { useTodoStore } from /stores/todo const todoStore useTodoStore() const newTodoTitle ref() const handleSubmit async () { if (!newTodoTitle.value.trim()) return await todoStore.addTodo({ title: newTodoTitle.value, completed: false }) newTodoTitle.value } /script template form submit.preventhandleSubmit input v-modelnewTodoTitle placeholderWhat needs to be done? classtodo-input / button typesubmitAdd/button /form /template3.2 Pinia状态管理配置全局状态管理实现业务逻辑与UI解耦// stores/todo.ts import { defineStore } from pinia import { ref } from vue import type { Todo } from /types/todo import todoApi from /api/todo export const useTodoStore defineStore(todo, () { const todos refTodo[]([]) const loading ref(false) const error refstring | null(null) const fetchTodos async () { try { loading.value true todos.value await todoApi.getAll() } catch (err) { error.value err instanceof Error ? err.message : Failed to fetch todos } finally { loading.value false } } const addTodo async (todo: OmitTodo, id) { const newTodo await todoApi.create(todo) todos.value.push(newTodo) } return { todos, loading, error, fetchTodos, addTodo } })3.3 类型安全的API客户端使用TypeScript定义强类型的API客户端// api/todo.ts import axios from axios export interface Todo { id: number title: string completed: boolean createdAt: string updatedAt?: string } const apiClient axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, headers: { Content-Type: application/json, }, }) export default { async getAll(): PromiseTodo[] { const response await apiClient.get(/todos) return response.data }, async create(todo: OmitTodo, id): PromiseTodo { const response await apiClient.post(/todos, todo) return response.data }, // 其他CRUD操作... }4. 前后端联调与优化4.1 跨域解决方案后端配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173) .allowedMethods(*) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }前端代理配置vite.config.tsexport default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ), }, }, }, })4.2 性能优化技巧后端优化启用Spring Boot Actuator监控端点配置HikariCP连接池参数添加Spring Cache抽象层前端优化实现列表虚拟滚动使用Vue的v-memo优化静态列表按需加载路由组件// 路由懒加载示例 const routes [ { path: /, component: () import(/views/TodoList.vue), }, { path: /stats, component: () import(/views/StatsView.vue), }, ]4.3 错误处理与用户反馈全局错误拦截器// api/interceptors.ts apiClient.interceptors.response.use( (response) response, (error) { const message error.response?.data?.message || error.message || An unexpected error occurred // 显示错误通知 useNotificationStore().showError(message) return Promise.reject(error) } )Spring Boot全局异常处理RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntityErrorResponse handleValidationExceptions( MethodArgumentNotValidException ex) { ListString errors ex.getBindingResult() .getFieldErrors() .stream() .map(error - error.getField() : error.getDefaultMessage()) .collect(Collectors.toList()); return ResponseEntity.badRequest() .body(new ErrorResponse(Validation failed, errors)); } record ErrorResponse(String message, ListString details) {} }5. 测试策略与质量保障5.1 后端测试金字塔单元测试示例ExtendWith(MockitoExtension.class) class TodoServiceTest { Mock private TodoRepository todoRepository; InjectMocks private TodoService todoService; Test void shouldCreateTodoSuccessfully() { CreateTodoRequest request new CreateTodoRequest(Learn testing); Todo savedTodo Todo.builder().id(1L).title(Learn testing).build(); when(todoRepository.save(any(Todo.class))).thenReturn(savedTodo); Todo result todoService.create(request.toDomain()); assertThat(result.getId()).isEqualTo(1L); verify(todoRepository).save(any(Todo.class)); } }集成测试示例SpringBootTest AutoConfigureMockMvc class TodoControllerIT { Autowired private MockMvc mockMvc; Autowired private ObjectMapper objectMapper; Test void shouldReturnAllTodos() throws Exception { mockMvc.perform(get(/api/todos)) .andExpect(status().isOk()) .andExpect(jsonPath($, hasSize(greaterThanOrEqualTo(0)))); } }5.2 前端测试方案组件测试示例import { mount } from vue/test-utils import TodoItem from /components/TodoItem.vue describe(TodoItem, () { it(emits toggle event when checkbox clicked, async () { const wrapper mount(TodoItem, { props: { todo: { id: 1, title: Test todo, completed: false, }, }, }) await wrapper.find(input[typecheckbox]).trigger(click) expect(wrapper.emitted(toggle)).toBeTruthy() }) })E2E测试示例describe(Todo App, () { it(should add new todo, () { cy.visit(/) cy.get(.todo-input).type(New todo item) cy.get(button[typesubmit]).click() cy.contains(New todo item).should(exist) }) })6. 部署与持续交付6.1 容器化部署Dockerfile后端FROM eclipse-temurin:17-jdk-jammy as builder WORKDIR /workspace COPY . . RUN ./gradlew bootJar FROM eclipse-temurin:17-jre-jammy VOLUME /tmp COPY --frombuilder /workspace/build/libs/*.jar app.jar ENTRYPOINT [java,-jar,/app.jar]Dockerfile前端FROM node:18-alpine as builder WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM nginx:alpine COPY --frombuilder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD [nginx, -g, daemon off;]6.2 CI/CD流水线配置GitHub Actions示例name: Backend CI on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up JDK 17 uses: actions/setup-javav3 with: java-version: 17 distribution: temurin - name: Build with Gradle run: ./gradlew build - name: Run tests run: ./gradlew test - name: Build Docker image run: docker build -t todo-backend .6.3 监控与日志Spring Boot Actuator配置management.endpoints.web.exposure.includehealth,info,metrics management.endpoint.health.show-detailsalways management.metrics.tags.application${spring.application.name}前端错误监控// 错误边界组件 onErrorCaptured((err) { logErrorToService(err) return false // 阻止错误继续向上传播 })在完成这个Todo应用的开发过程中最让我印象深刻的是TypeScript与Java的类型系统协同带来的开发体验提升。通过前后端共享API契约如OpenAPI规范可以大幅减少联调时的类型不匹配问题。实际项目中建议进一步添加用户认证、实时同步等功能来扩展应用场景。

相关新闻