SpringBoot3+Vue3+TypeScript全栈实战:图书管理系统从开发到部署
🚀 30+款热门AI模型一站整合,DeepSeek/GLM/Qwen 随心用,限时 5 折。 👉 点击领海量免费额度
很多开发者学完 SpringBoot 和 Vue 的语法后,一到自己动手整合前后端项目,就卡在跨域、接口联调、数据格式、打包部署这些环节。本文将以一个完整的图书管理系统为例,手把手带你走通 SpringBoot3、Vue3 和 TypeScript 这套现代全栈技术栈的完整开发流程。这不是一个炫技的 Demo,而是一个从零到一、覆盖环境搭建、核心编码、前后端联调,直至生产部署的实战指南。无论你是想巩固全栈技能的学生,还是需要快速搭建后台管理系统的开发者,都能从中获得一套可直接复用的工程化方案。
1. 项目定位与技术选型
1.1 为什么选择这个技术栈?
图书管理系统是一个经典的 CRUD 应用场景,但它涵盖了现代 Web 应用开发的几乎所有核心环节:数据库设计、RESTful API 构建、前端组件化开发、状态管理、路由以及最终的部署上线。选择SpringBoot3 + Vue3 + TypeScript组合,旨在让你体验一套高效、类型安全且易于维护的全栈开发模式。
- SpringBoot 3.x:作为后端核心,它提供了极简的配置和强大的自动装配能力,能快速构建出稳定、高性能的 REST API。SpringBoot 3.x 基于 Spring Framework 6,要求 JDK 17+,带来了更好的性能和新的特性(如 ProblemDetail 异常处理)。
- Vue 3:其 Composition API 带来了更灵活的逻辑复用和组织方式,配合
<script setup>语法,代码更加简洁。 - TypeScript:为 JavaScript 提供了静态类型检查,能在编码阶段就发现潜在的类型错误,极大地提升了前端代码的健壮性和可维护性,尤其在与后端接口对接时,类型定义能保证数据一致性。
- Vite:作为新一代前端构建工具,其极快的冷启动和热更新速度,能显著提升开发体验。
1.2 环境准备与版本确认
在开始编码前,请确保你的开发环境已就绪。为了避免因版本不兼容导致的“玄学”问题,建议固定以下版本组合:
- JDK: 17 或更高版本 (SpringBoot 3.x 的硬性要求)
- Node.js: 18.x 或更高版本
- npm / yarn / pnpm: 任选其一,本文使用 npm
- MySQL: 8.x (也可使用 Docker 快速启动一个实例)
- IDE: IntelliJ IDEA (后端) + VSCode (前端) 是高效组合
- 构建工具: Maven 3.6+ 或 Gradle
你可以通过以下命令快速检查环境:
# 检查 Java 版本 java -version # 检查 Node.js 和 npm 版本 node -v npm -v # 检查 Maven 版本 mvn -v2. 后端工程:SpringBoot3 构建 REST API
2.1 项目初始化与目录结构
我们采用前后端分离的架构,创建两个独立的项目目录,并通过 Git 统一管理。
推荐的项目结构如下:
book-management-system/ ├── backend/ # SpringBoot 后端项目 │ ├── src/main/java/com/example/bookstore │ │ ├── controller/ # 控制器层,处理HTTP请求 │ │ ├── service/ # 业务逻辑层 │ │ ├── repository/ # 数据访问层 (Spring Data JPA) │ │ ├── entity/ # 实体类,映射数据库表 │ │ ├── dto/ # 数据传输对象 │ │ ├── config/ # 配置类(如跨域、安全) │ │ └── BookstoreApplication.java # 启动类 │ ├── src/main/resources │ │ ├── application.yml # 主配置文件 │ │ └── ... │ └── pom.xml # Maven 依赖管理 └── frontend/ # Vue 3 + TypeScript 前端项目 ├── src/ │ ├── api/ # 封装所有后端API请求 │ ├── views/ # 页面级组件 │ ├── components/ # 可复用组件 │ ├── router/ # 路由配置 │ ├── store/ # 状态管理 (Pinia) │ ├── types/ # TypeScript 类型定义 │ └── main.ts # 应用入口 ├── index.html ├── vite.config.ts # Vite 配置 ├── tsconfig.json # TypeScript 配置 └── package.json使用 Spring Initializr (https://start.spring.io/) 快速生成后端项目,依赖选择:Spring Web,Spring Data JPA,MySQL Driver,Lombok。
2.2 数据库设计与实体映射
我们首先设计核心的book表,并创建对应的 JPA 实体类。
// 文件路径:backend/src/main/java/com/example/bookstore/entity/Book.java package com.example.bookstore.entity; import jakarta.persistence.*; import lombok.Data; import java.time.LocalDate; import java.time.LocalDateTime; @Data @Entity @Table(name = "book") public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String isbn; // 国际标准书号,唯一标识 @Column(nullable = false) private String title; private String author; private String publisher; @Column(name = "publish_date") private LocalDate publishDate; private Double price; @Column(name = "total_copies") private Integer totalCopies; // 总册数 @Column(name = "available_copies") private Integer availableCopies; // 可借册数 @Column(updatable = false) private LocalDateTime createTime; private LocalDateTime updateTime; @PrePersist protected void onCreate() { createTime = LocalDateTime.now(); updateTime = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updateTime = LocalDateTime.now(); } }关键点解析:
@Entity和@Table: 将类标记为 JPA 实体,并指定对应的数据库表名。@Id和@GeneratedValue: 定义主键及其生成策略(这里使用数据库自增)。@Column: 定义字段与数据库列的映射关系,可设置约束如nullable(非空)、unique(唯一)。@PrePersist和@PreUpdate: 生命周期回调注解,用于在实体持久化(插入)和更新前自动填充时间字段。这是一种优雅的审计字段处理方式。
2.3 数据访问层与业务逻辑层
Spring Data JPA 极大地简化了数据库操作。
// 文件路径:backend/src/main/java/com/example/bookstore/repository/BookRepository.java package com.example.bookstore.repository; import com.example.bookstore.entity.Book; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import java.util.Optional; public interface BookRepository extends JpaRepository<Book, Long>, JpaSpecificationExecutor<Book> { Optional<Book> findByIsbn(String isbn); boolean existsByIsbn(String isbn); }JpaRepository<Book, Long>: 继承此接口,即免费获得了基本的 CRUD 方法(save,findById,findAll,deleteById等)。JpaSpecificationExecutor<Book>: 用于支持复杂的动态查询(如多条件组合分页查询),通过Specification构建查询条件。- 自定义方法:
findByIsbn和existsByIsbn。Spring Data JPA 会根据方法名自动推导并实现 SQL 查询逻辑,这是其强大之处。
业务逻辑层负责处理具体的业务规则,如数据校验、事务管理等。
// 文件路径:backend/src/main/java/com/example/bookstore/service/BookService.java package com.example.bookstore.service; import com.example.bookstore.entity.Book; import com.example.bookstore.repository.BookRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.Optional; @Service @RequiredArgsConstructor // Lombok 注解,为 final 字段生成构造器,实现依赖注入 public class BookService { private final BookRepository bookRepository; public Page<Book> getBooks(Pageable pageable) { return bookRepository.findAll(pageable); } public Optional<Book> getBookById(Long id) { return bookRepository.findById(id); } @Transactional public Book saveBook(Book book) { // 业务校验:例如ISBN不能重复 if (bookRepository.existsByIsbn(book.getIsbn())) { throw new RuntimeException("ISBN已存在"); } return bookRepository.save(book); } @Transactional public Book updateBook(Long id, Book bookDetails) { Book book = bookRepository.findById(id) .orElseThrow(() -> new RuntimeException("图书不存在")); // 部分更新,避免覆盖如 createTime 等不应修改的字段 book.setTitle(bookDetails.getTitle()); book.setAuthor(bookDetails.getAuthor()); book.setPublisher(bookDetails.getPublisher()); book.setPublishDate(bookDetails.getPublishDate()); book.setPrice(bookDetails.getPrice()); book.setTotalCopies(bookDetails.getTotalCopies()); book.setAvailableCopies(bookDetails.getAvailableCopies()); // updateTime 由 @PreUpdate 自动处理 return bookRepository.save(book); } @Transactional public void deleteBook(Long id) { bookRepository.deleteById(id); } }关键点解析:
@Service: 标记为 Spring 的业务层组件。@Transactional:至关重要。它声明该方法需要事务管理。例如在saveBook中,如果 ISBN 重复抛出异常,则整个保存操作会回滚,保证数据一致性。在 Service 层使用该注解是常见的最佳实践。
2.4 控制器与 RESTful API 设计
控制器是前后端交互的入口,我们设计符合 RESTful 风格的 API。
// 文件路径:backend/src/main/java/com/example/bookstore/controller/BookController.java package com.example.bookstore.controller; import com.example.bookstore.entity.Book; import com.example.bookstore.service.BookService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/books") @RequiredArgsConstructor public class BookController { private final BookService bookService; @GetMapping public Page<Book> listBooks(@PageableDefault(size = 10, sort = "id") Pageable pageable) { return bookService.getBooks(pageable); } @GetMapping("/{id}") public Book getBook(@PathVariable Long id) { return bookService.getBookById(id) .orElseThrow(() -> new RuntimeException("图书不存在")); } @PostMapping @ResponseStatus(HttpStatus.CREATED) public Book createBook(@RequestBody Book book) { return bookService.saveBook(book); } @PutMapping("/{id}") public Book updateBook(@PathVariable Long id, @RequestBody Book book) { return bookService.updateBook(id, book); } @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteBook(@PathVariable Long id) { bookService.deleteBook(id); } }关键点解析:
@RestController: 组合了@Controller和@ResponseBody,表示该类所有方法的返回值都会自动序列化为 JSON。@RequestMapping(“/api/books”): 定义 API 的基础路径。@PageableDefault: 为分页参数提供默认值(每页10条,按id排序)。@RequestBody: 将 HTTP 请求体中的 JSON 数据绑定到方法参数对象上。- HTTP状态码:正确使用状态码(201创建成功,204删除成功无内容,404未找到)是 RESTful 设计的重要原则。
2.5 关键配置:数据库、跨域与文档
配置文件application.yml是项目的控制中心。
# 文件路径:backend/src/main/resources/application.yml spring: datasource: url: jdbc:mysql://localhost:3306/book_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password # 请替换为你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update # 开发环境:启动时根据实体更新表结构。生产环境务必改为 validate 或 none show-sql: true # 开发时在控制台打印SQL,便于调试 properties: hibernate: format_sql: true # 格式化打印的SQL,更易读 database-platform: org.hibernate.dialect.MySQL8Dialect server: port: 8080 # 后端服务端口 # 开发环境跨域配置(生产环境建议在Nginx或网关层配置) cors: allowed-origins: “http://localhost:5173“ # Vite 默认前端开发服务器地址 allowed-methods: “*” allowed-headers: “*”为了让跨域配置生效,需要创建一个配置类:
// 文件路径:backend/src/main/java/com/example/bookstore/config/WebConfig.java package com.example.bookstore.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig { @Value(“${cors.allowed-origins}“) private String allowedOrigins; @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(“/api/**“) // 对 /api 路径下的所有请求启用CORS .allowedOrigins(allowedOrigins.split(“,”)) .allowedMethods(“*“) .allowedHeaders(“*“) .allowCredentials(true); } }; } }强烈建议集成 API 文档:在pom.xml中添加springdoc-openapi-starter-webmvc-ui依赖,启动项目后访问http://localhost:8080/swagger-ui.html即可查看和测试所有接口,极大提升前后端协作效率。
至此,一个具备完整 CRUD 功能的图书管理后端 API 就搭建完成了。启动应用,确保能正常访问http://localhost:8080/api/books。
3. 前端工程:Vue3 + TypeScript 构建管理界面
3.1 项目初始化与依赖安装
在前端目录frontend/下,使用 Vite 快速初始化一个 Vue3 + TypeScript 项目。
# 在 frontend 目录下执行 npm create vite@latest . -- --template vue-ts安装项目所需的核心依赖:
npm install axios pinia vue-router@4 element-plus npm install -D @types/node # 提供 Node.js 类型定义,方便配置路径别名axios: 用于发送 HTTP 请求。pinia: Vue 3 官方推荐的状态管理库,比 Vuex 更简洁直观。vue-router: 实现前端路由。element-plus: 基于 Vue 3 的桌面端组件库,能快速搭建美观的界面。
3.2 配置 Vite 与解决开发跨域
修改vite.config.ts,配置路径别名和开发服务器代理,这是解决开发阶段跨域问题的关键。
// 文件路径:frontend/vite.config.ts import { defineConfig } from ‘vite’ import vue from ‘@vitejs/plugin-vue’ import { resolve } from ‘path’ export default defineConfig({ plugins: [vue()], resolve: { alias: { ‘@’: resolve(__dirname, ‘src’), // 配置 @ 指向 src 目录 }, }, server: { port: 5173, // 前端开发服务器端口 proxy: { // 配置代理,将 /api 开头的请求转发到后端 ‘/api’: { target: ‘http://localhost:8080’, // 后端服务地址 changeOrigin: true, }, }, }, })配置代理后,前端代码中请求/api/books,Vite 开发服务器会自动将其代理到http://localhost:8080/api/books,完美绕过浏览器的同源策略限制。
3.3 类型定义与 API 封装
清晰的类型定义是 TypeScript 项目的基石。首先定义与后端实体对应的接口。
// 文件路径:frontend/src/types/book.ts export interface Book { id?: number; isbn: string; title: string; author: string; publisher?: string; publishDate?: string; // 前端通常用字符串接收日期 price?: number; totalCopies: number; availableCopies: number; createTime?: string; updateTime?: string; } export interface PageResult<T> { content: T[]; totalElements: number; totalPages: number; size: number; number: number; }接着,封装统一的 API 请求模块,避免在组件中直接写axios调用,提高代码复用性和可维护性。
// 文件路径:frontend/src/api/bookApi.ts import axios from ‘axios’; import type { Book, PageResult } from ‘@/types/book’; // 创建 axios 实例,统一配置 const apiClient = axios.create({ baseURL: ‘/api’, // 利用 Vite 代理,生产环境需替换为实际后端地址 timeout: 10000, headers: { ‘Content-Type’: ‘application/json’, }, }); export const bookApi = { // 分页查询图书列表 getBooks(page: number = 0, size: number = 10): Promise<PageResult<Book>> { return apiClient.get(`/books?page=${page}&size=${size}`).then(res => res.data); }, // 根据ID获取图书 getBookById(id: number): Promise<Book> { return apiClient.get(`/books/${id}`).then(res => res.data); }, // 新增图书 createBook(book: Book): Promise<Book> { return apiClient.post(‘/books’, book).then(res => res.data); }, // 更新图书 updateBook(id: number, book: Book): Promise<Book> { return apiClient.put(`/books/${id}`, book).then(res => res.data); }, // 删除图书 deleteBook(id: number): Promise<void> { return apiClient.delete(`/books/${id}`); }, };3.4 状态管理:使用 Pinia
使用 Pinia 来管理图书列表的全局状态,将数据获取逻辑与组件解耦。
// 文件路径:frontend/src/store/bookStore.ts import { defineStore } from ‘pinia’; import { ref } from ‘vue’; import type { Book, PageResult } from ‘@/types/book’; import { bookApi } from ‘@/api/bookApi’; export const useBookStore = defineStore(‘book’, () => { // 状态 const bookList = ref<Book[]>([]); const total = ref(0); const loading = ref(false); // 动作 (Actions) const fetchBooks = async (page: number = 0, size: number = 10) => { loading.value = true; try { const result: PageResult<Book> = await bookApi.getBooks(page, size); bookList.value = result.content; total.value = result.totalElements; } catch (error) { console.error(‘获取图书列表失败:’, error); // 在实际项目中,这里可以触发一个全局的 UI 错误提示 throw error; // 将错误抛出,由调用方处理 } finally { loading.value = false; } }; const addBook = async (book: Book) => { try { await bookApi.createBook(book); // 新增成功后,重新加载当前页数据 await fetchBooks(); } catch (error) { console.error(‘新增图书失败:’, error); throw error; } }; const updateBook = async (id: number, book: Book) => { try { await bookApi.updateBook(id, book); await fetchBooks(); // 更新后刷新列表 } catch (error) { console.error(‘更新图书失败:’, error); throw error; } }; const deleteBook = async (id: number) => { try { await bookApi.deleteBook(id); await fetchBooks(); // 删除后刷新列表 } catch (error) { console.error(‘删除图书失败:’, error); throw error; } }; return { // 状态 bookList, total, loading, // 动作 fetchBooks, addBook, updateBook, deleteBook, }; });3.5 页面组件与路由配置
首先配置路由,定义页面间的导航关系。
// 文件路径:frontend/src/router/index.ts import { createRouter, createWebHistory } from ‘vue-router’; import BookListView from ‘@/views/BookListView.vue’; import BookFormView from ‘@/views/BookFormView.vue’; const routes = [ { path: ‘/’, redirect: ‘/books’ }, { path: ‘/books’, component: BookListView }, { path: ‘/books/add’, component: BookFormView }, { path: ‘/books/edit/:id’, component: BookFormView, props: true }, // 通过 props 接收路由参数 ]; const router = createRouter({ history: createWebHistory(), routes, }); export default router;创建图书列表页组件,展示数据并处理分页、删除等操作。
<!-- 文件路径:frontend/src/views/BookListView.vue --> <template> <div class=“book-list”> <el-button type=“primary” @click=“goToAdd”>新增图书</el-button> <el-table :data=“store.bookList” v-loading=“store.loading” style=“width: 100%; margin-top: 20px;”> <el-table-column prop=“isbn” label=“ISBN” width=“180” /> <el-table-column prop=“title” label=“书名” /> <el-table-column prop=“author” label=“作者” /> <el-table-column prop=“publisher” label=“出版社” /> <el-table-column prop=“availableCopies” label=“可借数量” width=“100” /> <el-table-column label=“操作” width=“180”> <template #default=“scope”> <el-button size=“small” @click=“editBook(scope.row)”>编辑</el-button> <el-button size=“small” type=“danger” @click=“handleDelete(scope.row.id)”>删除</el-button> </template> </el-table-column> </el-table> <el-pagination @current-change=“handlePageChange” :page-size=“pageSize” :total=“store.total” :current-page=“currentPage” layout=“prev, pager, next” style=“margin-top: 20px; justify-content: center;” /> </div> </template> <script setup lang=“ts”> import { onMounted, ref } from ‘vue’; import { useRouter } from ‘vue-router’; import { useBookStore } from ‘@/store/bookStore’; import { ElMessage, ElMessageBox } from ‘element-plus’; import type { Book } from ‘@/types/book’; const router = useRouter(); const store = useBookStore(); const currentPage = ref(1); const pageSize = 10; onMounted(() => { loadBooks(); }); const loadBooks = () => { store.fetchBooks(currentPage.value - 1, pageSize); }; const handlePageChange = (page: number) => { currentPage.value = page; loadBooks(); }; const goToAdd = () => { router.push(‘/books/add’); }; const editBook = (book: Book) => { router.push(`/books/edit/${book.id}`); }; const handleDelete = async (id: number) => { try { await ElMessageBox.confirm(‘确认删除该图书?此操作不可撤销。’, ‘提示’, { type: ‘warning’, confirmButtonText: ‘确定’, cancelButtonText: ‘取消’, }); await store.deleteBook(id); ElMessage.success(‘删除成功’); } catch (error) { // 用户点击了取消 console.log(‘取消删除’); } }; </script>创建图书表单页组件,用于新增和编辑图书。
<!-- 文件路径:frontend/src/views/BookFormView.vue --> <template> <div class=“form-container”> <el-card> <template #header> <span>{{ isEditMode ? ‘编辑图书’ : ‘新增图书’ }}</span> </template> <el-form :model=“form” :rules=“rules” ref=“formRef” label-width=“100px”> <el-form-item label=“ISBN” prop=“isbn”> <el-input v-model=“form.isbn” placeholder=“请输入ISBN” :disabled=“isEditMode” /> </el-form-item> <el-form-item label=“书名” prop=“title”> <el-input v-model=“form.title” placeholder=“请输入书名” /> </el-form-item> <el-form-item label=“作者” prop=“author”> <el-input v-model=“form.author” placeholder=“请输入作者” /> </el-form-item> <el-form-item label=“出版社”> <el-input v-model=“form.publisher” placeholder=“请输入出版社” /> </el-form-item> <el-form-item label=“出版日期”> <el-date-picker v-model=“form.publishDate” type=“date” placeholder=“选择日期” value-format=“YYYY-MM-DD” /> </el-form-item> <el-form-item label=“价格”> <el-input-number v-model=“form.price” :min=“0” :precision=“2” /> </el-form-item> <el-form-item label=“总册数” prop=“totalCopies”> <el-input-number v-model=“form.totalCopies” :min=“1” /> </el-form-item> <el-form-item label=“可借册数” prop=“availableCopies”> <el-input-number v-model=“form.availableCopies” :min=“0” :max=“form.totalCopies” /> </el-form-item> <el-form-item> <el-button type=“primary” @click=“submitForm”>提交</el-button> <el-button @click=“goBack”>取消</el-button> </el-form-item> </el-form> </el-card> </div> </template> <script setup lang=“ts”> import { onMounted, reactive, ref } from ‘vue’; import { useRoute, useRouter } from ‘vue-router’; import { useBookStore } from ‘@/store/bookStore’; import { ElMessage, FormInstance, FormRules } from ‘element-plus’; import type { Book } from ‘@/types/book’; import { bookApi } from ‘@/api/bookApi’; const route = useRoute(); const router = useRouter(); const store = useBookStore(); const formRef = ref<FormInstance>(); const isEditMode = ref(false); const bookId = ref<number>(); const form = reactive<Book>({ isbn: ‘’, title: ‘’, author: ‘’, publisher: ‘’, publishDate: ‘’, price: 0, totalCopies: 1, availableCopies: 1, }); const rules = reactive<FormRules>({ isbn: [{ required: true, message: ‘请输入ISBN’, trigger: ‘blur’ }], title: [{ required: true, message: ‘请输入书名’, trigger: ‘blur’ }], author: [{ required: true, message: ‘请输入作者’, trigger: ‘blur’ }], totalCopies: [{ required: true, message: ‘请输入总册数’, trigger: ‘blur’ }], availableCopies: [{ required: true, message: ‘请输入可借册数’, trigger: ‘blur’ }], }); onMounted(() => { if (route.params.id) { isEditMode.value = true; bookId.value = Number(route.params.id); loadBookData(bookId.value); } }); const loadBookData = async (id: number) => { try { const book = await bookApi.getBookById(id); Object.assign(form, book); // 将获取的数据合并到表单对象 } catch (error) { ElMessage.error(‘加载图书信息失败’); router.push(‘/books’); } }; const submitForm = async () => { if (!formRef.value) return; const valid = await formRef.value.validate(); if (!valid) return; try { if (isEditMode.value && bookId.value) { await store.updateBook(bookId.value, form); ElMessage.success(‘更新成功’); } else { await store.addBook(form); ElMessage.success(‘新增成功’); } router.push(‘/books’); // 返回列表页 } catch (error: any) { ElMessage.error(error.response?.data?.message || ‘操作失败’); } }; const goBack = () => { router.push(‘/books’); }; </script> <style scoped> .form-container { max-width: 800px; margin: 20px auto; } </style>4. 前后端联调与问题排查
分别启动后端和前端服务:
# 后端 (在 backend 目录下) mvn spring-boot:run # 或直接运行启动类 # 前端 (在 frontend 目录下) npm run dev访问http://localhost:5173,你应该能看到图书列表页面,并能进行增删改查操作。
常见联调问题排查清单:
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 网络错误 (Network Error) 或 404 | 后端服务未启动;代理配置错误;API路径不匹配。 | 1. 访问http://localhost:8080/api/books检查后端是否正常。2. 检查 vite.config.ts中的proxy.target端口是否正确。3. 核对前端请求的 URL 和后端 @RequestMapping的路径是否一致(注意大小写和复数)。 |
| 跨域错误 (CORS) | 后端未配置 CORS 或配置不正确;Vite 代理未生效。 | 1. 确保后端WebConfig已正确配置并生效。2. 检查前端请求的 Origin 是否在允许列表中。 3. 生产环境应在 Nginx 或网关层配置 CORS。 |
| 400 Bad Request | 请求体格式错误;字段类型不匹配。 | 1. 检查前端axios请求头Content-Type是否为application/json。2. 对比前端 Book接口和后端Book实体类的字段名和类型(特别是日期格式)。3. 查看后端控制台日志,SpringBoot 会打印详细的绑定错误信息。 |
| 500 Internal Server Error | 后端业务逻辑异常;数据库操作失败。 | 1.首要步骤:查看后端控制台输出的异常堆栈信息。 2. 常见原因:数据库连接失败、SQL语法错误、空指针、业务校验抛出异常未捕获。 |
| 数据不显示或显示错误 | 请求成功但数据结构不匹配;前端渲染逻辑错误。 | 1. 打开浏览器开发者工具的Network标签,查看 API 响应数据是否正确。 2. 检查前端 Console是否有 JavaScript 或 TypeScript 错误。3. 核对前端定义的 interface与后端返回的 JSON 字段名、类型是否完全一致。 |
5. 项目打包与生产环境部署
5.1 前端打包
前端项目使用 Vite 进行构建,生成静态资源。
cd frontend npm run build命令执行后,会在frontend/dist目录下生成优化后的 HTML、CSS、JavaScript 文件。这个dist文件夹就是需要部署到 Web 服务器的内容。
5.2 后端打包
后端使用 Maven 打包成可执行的 JAR 文件。
cd backend mvn clean package -DskipTests打包成功后,在backend/target目录下会生成一个类似bookstore-0.0.1-SNAPSHOT.jar的文件。
5.3 生产环境部署方案
方案一:传统分离部署(推荐)这是最清晰、最常用的部署方式。
- 部署后端:将 JAR 包上传到服务器,使用
java -jar启动。为了进程常驻和管理,推荐使用systemd创建服务。
然后启用并启动服务:# 创建服务文件 /etc/systemd/system/bookstore.service [Unit] Description=Bookstore Backend Service After=network.target [Service] User=your_user ExecStart=/usr/bin/java -jar /path/to/bookstore.jar SuccessExitStatus=143 Restart=always [Install] WantedBy=multi-user.targetsudo systemctl daemon-reload sudo systemctl enable bookstore sudo systemctl start bookstore sudo systemctl status bookstore # 查看状态 - 部署前端:将
dist文件夹内的所有文件上传到服务器(例如/var/www/bookstore),然后配置 Nginx 作为 Web 服务器和反向代理。
重启 Nginx 使配置生效:# /etc/nginx/conf.d/bookstore.conf server { listen 80; server_name your_domain.com; # 替换为你的域名或服务器IP # 静态资源服务 location / { root /var/www/bookstore; index index.html; try_files $uri $uri/ /index.html; # 支持 Vue Router 的 history 模式 } # API 反向代理 location /api/ { proxy_pass http://localhost:8080; # 代理到后端服务 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }sudo systemctl restart nginx。
方案二:Docker 容器化部署适合需要环境一致性、快速扩展的场景。
- 后端 Dockerfile:
# backend/Dockerfile FROM openjdk:17-jdk-slim VOLUME /tmp ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar ENTRYPOINT [“java”, “-jar”, “/app.jar”] - 前端 Dockerfile:
# frontend/Dockerfile FROM nginx:alpine COPY dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.confnginx.conf内容与上述 Nginx 配置类似。 - 使用 docker-compose.yml 编排:
运行# docker-compose.yml version: ‘3.8’ services: mysql: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: root_password MYSQL_DATABASE: book_db volumes: - mysql_data:/var/lib/mysql ports: - “3306:3306” backend: build: ./backend ports: - “8080:8080” environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/book_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: root_password depends_on: - mysql frontend: build: ./frontend ports: - “80:80” depends_on: - backenddocker-compose up -d即可一键启动所有服务。
6. 进阶考量与最佳实践
一个可用的系统只是起点,要用于生产环境,还需考虑以下方面:
- API 鉴权与安全:集成 Spring Security + JWT。用户登录后,前端在请求头中携带
Authorization: Bearer <token>,后端校验 Token 有效性并控制接口访问权限。 - 输入校验:在后端 Controller 方法参数上使用
@Valid注解,并在 DTO 类字段上使用@NotBlank,@Size,@Pattern等注解进行校验,防止非法数据进入业务逻辑。 - 全局异常处理:使用
@ControllerAdvice和@ExceptionHandler创建全局异常处理器,统一处理业务异常、参数校验异常等,并返回结构化的错误信息给前端。 - 日志与监控:配置
logback-spring.xml将日志按级别输出到文件。集成 Spring Boot Actuator 提供健康检查端点 (/actuator/health)。考虑使用 Prometheus + Grafana 或 SkyWalking 进行应用性能监控 (APM)。 - 数据库优化:为高频查询字段(如
isbn,title)建立索引。对于复杂的多表关联查询,考虑使用@Query编写优化后的 JPQL 或原生 SQL。 - 前端性能:对于大型列表,考虑使用虚拟滚动(如
vue-virtual-scroller)。对不变的数据使用缓存策略。利用 Vite 的代码分割功能,按需加载组件。 - 代码质量:前后端分别引入 ESLint 和 Checkstyle 进行代码规范检查。使用 Prettier 统一代码格式。建立 Git 提交规范,并考虑使用 CI/CD 工具(如 Jenkins, GitLab CI)自动化构建、测试和部署流程。
通过这个完整的项目实践,你不仅掌握了 SpringBoot3、Vue3 和 TypeScript 的核心用法,更重要的是理解了如何将它们有机地组合起来,构建一个可维护、可扩展的全栈应用。从环境搭建到线上部署,每一步都力求清晰、可操作。建议你按照这个流程亲手实现一遍,遇到问题正是深入学习的机会。在此基础上,你可以继续扩展用户管理、借阅归还流程、数据统计图表、权限控制(RBAC)等模块,逐步完善成一个功能齐全的管理系统。
🚀 30+款热门AI模型一站整合,DeepSeek/GLM/Qwen 随心用,限时 5 折。 👉 点击领海量免费额度
