ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Spring Boot + Vue 3全栈项目实战:前后端分离开发与部署指南

Spring Boot + Vue 3全栈项目实战:前后端分离开发与部署指南 1. 项目概述1.1 核心需求解析做全栈开发这几年我见过太多半路出家的开发者在 Spring Boot 和 Vue 的边界线上反复横跳——后端能写接口但前端拿着 JSON 不知道往哪儿塞前端能把页面画出来但遇到跨域、鉴权、打包部署就抓瞎。这个实战指南解决的就是这个问题把一条完整的前后端分离开发链路串起来从零开始搭建一个可直接运行的全栈项目。注意这里的关键词是“全栈”不是“前后端各写一半”。项目会包含一个完整的业务闭环前端 Vue 负责页面交互和数据展示后端 Spring Boot 负责接口服务和数据持久化中间通过 RESTful API 通信最终打包部署到同一台服务器上跑通整个流程。这个过程中涉及的技术点非常密集——Vue 的生命周期、Axios 的请求拦截、Spring Boot 的分层架构、MyBatis 的数据映射、JWT 的登录态管理、Nginx 的反向代理——任何一个环节掉链子整个项目就跑不起来。这个项目的受众其实很广。刚入行的初级开发者可以通过它建立全栈思维知道前端发起的请求在后端到底经历了什么写了好几年接口的后端开发可以补上前端技能独立负责完整模块做毕设或小团队项目的同学可以直接拿这套骨架改业务逻辑就能上线。我的建议是无论你偏向前端还是后端都完整地把项目过一遍收获最大的恰恰是你知识短板那部分的内容。1.2 技术栈全景与选型理由Spring Boot 3.x 搭配 Vue 3.x 是当前主流选择我用的版本组合是 Spring Boot 3.2.5 Vue 3.4.21 Vite 5.2.0。这套组合的好处是生态成熟遇到问题搜一下基本都是现成的答案。Spring Boot 3.x 相比 2.x 最大的变化是底层基于 Spring Framework 6支持 Jakarta EE 规范默认使用 Java 17 及以上版本开发体验和性能都有明显提升。Vue 3 的组合式 API 写起来比 Options API 更符合直觉配合 Vite 的开发服务器热更新速度能缩短到一两秒。数据库我用 MySQL 8.0持久层框架选了 MyBatis Plus 而不是纯 MyBatis原因很简单——单表操作完全不需要手写 SQL自带的方法够用复杂查询再看情况写 XML。鉴权方案采用 JWT Spring Security但做了简化处理只用了 Spring Security 的加密工具类没有引入完整的安全过滤链。很多全栈项目的通病是过度设计明明是个小项目却把微服务、分布式那套全上了结果光搭建环境就花了大半时间。这次的实战特意保持克制让你能专注于理解核心链路。提示如果你的环境是 JDK 8暂时不想升级建议使用 Spring Boot 2.7.x 配合 Vue 2但同样建议尽早升级。JDK 17 的垃圾回收性能和容器支持都比 8 好很多IntelliJ IDEA 2023 版本对 JDK 17 的支持也更顺手。2. 整体设计与架构拆解2.1 前后端分离架构的边界划分全栈项目最容易犯的错误是职责混乱。前端代码里塞后端逻辑后端代码里混页面模板时间一长代码就成浆糊了。这次项目采用严格的前后端分离模式前端是纯静态资源通过 Axios 异步调用后端接口后端只负责提供 JSON 数据不做页面渲染。简单来说你可以把整套系统想象成一家餐厅前端是服务员负责记录顾客需求、展示菜品渲染数据后端是后厨负责准备菜品处理业务数据库就是储藏室。服务员不需要知道菜怎么做后厨也不需要直接和顾客沟通两者之间的点菜单就是 API 接口。拿一个具体的业务场景来说用户在前端页面上登录填完表单点击提交按钮Vue 组件把表单数据通过 POST 请求发送到后端的/api/login接口后端 Controller 接收请求调 Service 层校验用户名密码再调 Mapper 层查数据库校验通过后生成一个 JWT token 返回给前端。前端收到 token 后存在 localStorage 里之后每次请求都在请求头带上这个 token后端用拦截器验证 token 是否有效。整个链路中前端和后端各自独立开发、独立测试、独立部署契约就是接口文档。2.2 后端分层设计的职责划分后端项目的包结构按照经典的三层架构来组织Controller 层负责接收请求和参数校验Service 层负责业务逻辑Mapper 层负责数据库操作。在这个之上再加一个 Entity 包放实体类一个 DTO 包放数据传输对象一个 Config 包放配置类一个 Common 包放统一返回结果和异常处理。每个层之间通过接口和类来协作。Controller 不直接操作数据库Service 不写 SQLMapper 只做数据映射。刚入门的开发者容易把业务代码全堆在 Controller 里一个方法写上百行看起来很“高效”实际上后面需求一变更就得重写。我曾经接手过一个项目登录接口里直接拼 SQL 查数据库后来要加验证码功能那叫一个费劲。打个比方三层架构就像公司的三级审批——Controller 是前台接待负责接需求和判断诉求是否合理Service 是部门主管负责制定处理方案并协调资源Mapper 是具体执行人负责落实细节。每一层只做自己该做的事出问题时能快速定位是哪个环节的锅。2.3 前端组件化设计的模块划分前端项目按功能模块划分目录结构api目录放接口请求封装router目录放路由配置stores目录放 Pinia 状态管理views目录放页面组件components目录放公共组件。组件化设计的关键是找到合适的拆分粒度。举个具体的例子后台管理系统里常见的“用户管理”页面可以拆成搜索栏、表格、弹窗表单三个部分。搜索栏接收用户输入的查询条件通过事件把条件抛给父组件父组件拿到条件后更新列表数据表格展示数据并监听行点击弹窗表单负责新增和编辑。组件拆分有个度阿里推出的前端规范里提到一个原则如果一个组件太长超过合理范围还没法拆分出可复用的子组件可能就是职能不单一。反过来把组件拆得太碎也有问题——有一次我把一个简单的列表页拆成了十多个组件每个组件只有几十行代码结果传参传得头晕调试一个 bug 得在文件间跳来跳去。一般的原则是只在组件被两个以上地方复用或者单个组件的复杂度已经明显影响可读性时才需要拆分。3. 环境准备与项目初始化3.1 开发环境的版本选型与安装清单先过一遍环境清单。后端需要 JDK 17、Maven 3.9、MySQL 8.0前端需要 Node.js 18我用的 20.12.0、npm或 pnpm/yarn这里用 npm、Vite 作为构建工具。IDE 方面我推荐后端用 IntelliJ IDEA前端用 VS Code两个都装也没问题。几个需要注意的坑先提前说JDK 版本必须和环境变量匹配。装了多个 JDK 的机器上IDEA 里配置的 Project SDK 和系统环境变量最好统一否则 Maven 编译时可能用错版本。Node.js 尽量用偶数版本。Node 官方对奇数次版本的支持时间较短有些依赖可能不兼容。当年 Node 15 时代装 Vite 就是各种问题后来学聪明了直接固定用最新 LTS 版本。MySQL 8.0 的认证插件。默认的caching_sha2_password在旧版连接驱动下会报错Spring Boot 3.x 自带的 MySQL Connector/J 8.x 没问题但如果你的其他老项目还在用 5.x 驱动连接 MySQL 8 会失败。安装顺序没有硬性要求但建议先装后端环境再装前端环境。因为后端跑起来之后你可以先用 Postman 测试接口确认没问题再写前端页面这样排查问题时范围更清晰。3.2 后端项目初始化三步走先用 IntelliJ IDEA 创建 Spring Boot 项目。如果你用的是 IDEA 2023 版本可以直接在 New Project 里选 Spring InitializrGroup 填com.exampleArtifact 填fullstackJava 版本选 17依赖勾选 Spring Web、Spring Security可选也可以后面手动加、MySQL Driver、Lombok、Validation。需要补充说明的是Spring Initializr 生成的项目默认带一个mvnwMaven Wrapper它允许你在没有全局安装 Maven 的机器上直接执行./mvnw命令来构建项目这对保证构建环境一致性很有帮助。如果你的机器没有 Maven可以用它来代替mvn命令。项目生成后先别急着写代码把目录补齐。基础结构大概是src/main/java/com/example/fullstack ├── controller ├── service │ ├── impl ├── mapper ├── entity ├── dto ├── config └── common然后在pom.xml里加入 MyBatis Plus 的依赖。有个地方容易踩坑MyBatis Plus 在 Spring Boot 3.x 下需要引入mybatis-plus-spring-boot3-starter而不是mybatis-plus-boot-starter那是给 Spring Boot 2.x 用的。如果你用错版本启动时大概率会报Invalid value type for attribute factoryBeanObjectType之类的错误。配置文件application.yml按以下内容配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/fullstack_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: your_password jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto这里有个细节Jackson 的日期格式设置很重要。如果前端传来的日期字符串是2024-05-20 10:30:00格式后端 LocalDateTime 字段不加JsonFormat注解或全局配置默认是 ISO 格式反序列化时就会报错。全局配好省得每个字段都写注解。3.3 前端项目初始化与依赖配置前端我用 Vite 创建项目命令如下npm create vitelatest frontend -- --template vue cd frontend npm install然后安装项目需要的依赖npm install vue-router4 pinia axios element-plus选 Element Plus 做 UI 组件库是国内全栈项目的常见选择它对中文场景支持好表格、表单、分页这些后台管理常用的组件都有现成的。同时安装它的图标库element-plus/icons-vue可以在全局注册方便在菜单和按钮里直接用图标。依赖装完之后先配置 Vite 的代理和构建选项。开发阶段前端和后端分离运行需要把请求代理到后端服务生产阶段前端构建后的静态文件由 Nginx 托管也需要配置转发规则。在开发环境vite.config.js里加代理import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端代码里请求/api/login时Vite 会把请求转发到http://localhost:8080/api/login绕过了开发环境下的跨域限制。生产环境则交给 Nginx 配置 location 块做反向代理。提示配置代理解决的是开发阶段的跨域问题。如果你的前后端部署在同一台服务器、同一个域名下生产环境一般不需要跨域处理但如果部署在不同域名或端口下则要在后端加 CORS 配置。这个细节等到部署章节详细讲。4. 核心功能开发实操4.1 用户认证模块的完整实现方案用户认证是几乎所有全栈系统的刚性需求。我用 JWT 实现了这套认证流程核心思路是用户登录成功后后端签发一个有效期为一定时间的 token前端存储这个 token在每次请求的 Authorization 请求头里带上来后端通过拦截器统一校验。先说数据表结构用户表设计为CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, email varchar(100) DEFAULT NULL COMMENT 邮箱, status tinyint(4) DEFAULT 1 COMMENT 状态1启用 0禁用, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;密码存储用的是 BCrypt 加密算法这是 Spring Security 里自带的工具类不需要额外引依赖// 加密 String encodedPassword BCryptPasswordEncoder().encode(rawPassword); // 校验 boolean matches encoder.matches(rawPassword, encodedPassword);这里有一个很容易被忽略的安全细节matches方法第一个参数是明文密码第二个是数据库里取出来的密文。我第一次写的时候参数顺序搞反了调试了半小时才发现是参数顺序问题。另外注意 BCrypt 的加密结果是随机的同一个明文每次加密出来的密文都不一样所以不能用常规字符串比较必须用matches方法校验。登录接口的逻辑不复杂伪代码是这样的Controller 接收LoginRequest用户名密码Service 层根据用户名查用户如果用户不存在直接抛异常提示“用户名或密码错误”注意这里的提示不能太具体否则会暴露用户是否存在存在则用 BCrypt 校验密码失败同样提示“用户名或密码错误”成功后用用户 ID、用户名和当前时间戳生成 JWT返回 token 用户信息。4.2 JWT 拦截器的实现与请求链路JWT 拦截器是一个关键的“横切关注点”。我用 Spring 的HandlerInterceptor实现配合 WebMvcConfigurer 注册让所有/api/**下的接口都经过校验登录接口本身要放行。核心逻辑如下Component public class JwtInterceptor implements HandlerInterceptor { Autowired private JwtUtils jwtUtils; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (request.getMethod().equals(OPTIONS)) { return true; } // 前端约定 token 放在请求头的 Authorization 字段中 String token request.getHeader(Authorization); if (token null || token.isEmpty()) { return this.unauthorized(response, 未登录或登录已过期); } // 校验 token 合法性 try { String userId jwtUtils.getUserIdFromToken(token); request.setAttribute(userId, userId); return true; } catch (Exception e) { return this.unauthorized(response, token 无效或已过期); } } private boolean unauthorized(HttpServletResponse response, String msg) throws IOException { response.setStatus(401); response.setContentType(application/json;charsetutf-8); response.getWriter().write(JSON.toJSONString(Result.error(401, msg))); return false; } }注意一个细节预检请求OPTIONS一定要放行。浏览器在跨域 POST 请求前会自动发送一个 OPTIONS 预检请求如果你的拦截器把它拦下来了前端会报 CORS 错误而且这个错误在浏览器控制台看起来像是后端 CORS 配置有问题实际上却是拦截器没放行 OPTIONS。拦截器只是一个粗粒度的路由守卫对于不同接口的细粒度权限比如只有管理员能调用的接口可以在 Controller 方法上加自定义注解再在拦截器里读取注解做二次判断这个属于权限系统的进阶内容本次项目不做展开。4.3 用户管理模块的 CRUD 与分页查询用户管理模块虽然简单但把前后端交互的典型模式都覆盖了查询列表、新增、编辑、删除。我用 MyBatis Plus 的IService和BaseMapper框架代码量比传统 MyBatis 少很多。Controller 层代码示例RestController RequestMapping(/api/user) public class UserController { Autowired private UserService userService; GetMapping(/list) public ResultIPageUserVO list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, String keyword) { PageUser page new Page(pageNum, pageSize); LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.like(User::getUsername, keyword).or().like(User::getNickname, keyword); } wrapper.orderByDesc(User::getCreateTime); IPageUser userPage userService.page(page, wrapper); // 转 VO 返回不直接暴露实体 return Result.success(PageUtil.convert(userPage, UserVO.class)); } // 新增、编辑、删除逻辑省略遵循同样的模式 }细节问题来了新增用户密码需要加密存储编辑用户如果密码为空说明不修改密码。这些分情况讨论的业务逻辑都放在 Service 层。Controller 里写太多 if-else后面维护起来就想骂人。前端对应这个接口的调用也是固定套路封装在api/user.js模块里import request from ../utils/request export function getUserList(data) { return request({ url: /user/list, method: get, params: data }) } export function addUser(data) { return request({ url: /user/add, method: post, data }) } // ...4.4 Axios 请求封装的几个关键决策点Axios 封装的好坏直接决定整个前端项目请求代码的整洁程度。我的做法是单独建一个utils/request.js文件生成一个配置了基础 URL、超时时间和拦截器的 Axios 实例。核心代码import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动附加 token request.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }, error Promise.reject(error) ) // 响应拦截器统一处理错误码 request.interceptors.response.use( response { const res response.data // 如果后端返回了业务错误码 if (res.code ! 200) { ElMessage.error(res.message || 请求失败) if (res.code 401) { // token 过期跳转登录页 localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(error.message || 网络错误) return Promise.reject(error) } ) export default request几个决策点说一下。baseURL 设为/api配合 Vite 代理或 Nginx 反向代理前端代码里就不用写死后端地址环境切换时只需要改代理配置就行。响应拦截器里把后端的code/message/data统一解开前端调用接口时直接拿到业务数据不需要每个调用处都写.then(res res.data)。还有一个细节上的选择token 存 localStorage 还是 sessionStorage。有人建议存 sessionStorage因为关闭浏览器就自动清掉安全一些但用户希望刷新页面后保持登录状态所以我用 localStorage。另外逻辑上要区分业务错误码比如 500和 HTTP 状态码比如 404上述封装里对这两类错误都做了处理。5. 前端关键页面与交互实现5.1 登录页的实现与表单校验登录页是系统的“门面”虽然逻辑简单但涉及的交互细节并不少。我用 Element Plus 的el-form组件实现配合它的表单校验规则用户名必填格式校验长度 3-20 个字符。密码必填长度 6-32 个字符。点击登录时先校验表单通过后才发请求。登录成功后跳转到首页并提示“欢迎回来xxx”。登录页有一个容易被忽视的用户体验问题请求失败时表单要保留用户输入不能整个页面刷新重置。Vue 的单页应用优势在这里体现得比较明显。另外登录按钮在请求期间要处于 loading 状态防止用户重复提交。这些都是小细节但拿捏好才是专业的表现。关于“记住密码”功能严谨的开发不会把密码存在前端通常只记住用户名。这个项目我没有做这个功能因为涉及敏感信息不做比做更安全。5.2 布局与路由守卫后台管理系统的典型布局是左侧菜单栏 右侧内容区。我用 Element Plus 的el-container组件实现菜单数据直接写死在路由配置里通过v-for循环渲染。路由配置的要点是嵌套路由const routes [ { path: /login, component: Login }, { path: /, component: Layout, redirect: /dashboard, children: [ { path: dashboard, name: Dashboard, component: Dashboard, meta: { title: 首页, icon: HomeFilled } }, { path: user, name: User, component: UserManagement, meta: { title: 用户管理, icon: User } } ] } ]路由守卫是前端鉴权的另一道防线。在router.beforeEach里检查是否登录未登录则重定向到登录页router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() } else { if (!token) { next({ path: /login }) } else { next() } } })提醒一句前端路由守卫只是提升用户体验的手段不是安全措施。真正校验 token 的有效性还得靠后端的拦截器因为用户可以直接改前端代码绕过路由守卫。一切前端校验都是“防君子不防小人”。5.3 表格、分页与表单的联动模式用户管理页面的核心交互是搜索栏输入条件 → 点击查询 → 表格展示数据 → 点击编辑弹出表单 → 保存后刷新列表。这是后台管理系统最高频的交互模式。页面数据结构大概是searchForm搜索条件的响应式数据tableData当前页的数据列表pageInfo页码、每页条数、总条数dialogVisible弹窗显隐formModel表单数据formRules表单校验规则交互逻辑是点击搜索时把页码重置为 1然后调用列表接口把返回的数据和总条数赋值给响应式变量。前端代码里分页组件和表格的状态同步是需要注意的地方el-pagination v-model:current-pagepageInfo.pageNum v-model:page-sizepageInfo.pageSize :totalpageInfo.total layouttotal, prev, pager, next, sizes changeloadData /需要注意el-pagination在 Element Plus 3.x 里事件名已经改成了change旧版是current-change和size-change两个事件分别处理。如果团队里有老手用新版时还写旧事件表格点击分页没反应排查老半天最后发现是事件名不对。弹出表单的编辑功能有一个常见坑编辑用户时打开弹窗要把当前行的数据深拷贝到表单模型里避免直接修改表格数据源。如果不做深拷贝弹窗里改字段时表格数据也会跟着变因为引用的是同一个对象。用JSON.parse(JSON.stringify(...))做深拷贝最简单或者用展开运算符新建对象。5.4 状态管理与跨组件通信的方案选择Vue 3 中状态管理的选择比 Vue 2 时代丰富多了。Pinia 是官方推荐方案用它替代 Vuex 写起来舒服得多没有 mutations、actions 那套冗余的流程store 里定义的 state 直接用actions 就是一个普通函数。这个项目用到 Pinia 的场景用户登录后存基本信息和 token。在登录成功时调用userStore.setLoginInfo(...)然后在多个组件里通过userStore.username展示当前登录用户。示例代码import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userId: null, username: , nickname: }), actions: { setLoginInfo(data) { this.token data.token this.userId data.userId this.username data.username this.nickname data.nickname localStorage.setItem(token, data.token) }, logout() { this.token this.userId null this.username this.nickname localStorage.removeItem(token) } } })在这个系统里用户信息需要全局共享但很多业务组件之间的通信其实用 props 和 emit 就够。过度引入全局状态管理会让代码难追踪——某个字段不知道被谁修改了找起来很费劲。我的原则是跨页面、跨层级深组件才考虑 Pinia父子组件之间能用 props/emit 就用 props/emit。6. 前后端联调与常见问题排查6.1 联调前的接口约定与准备前后端分离开发模式下联调阶段往往是问题集中爆发的时期。前端认为接口该这么返回后端认为前端该那么传参经常对不上。经验是先定接口文档再动代码哪怕是简单的表格约定、字段类型、命名风格、错误码都提前对齐。接口文档我用 YApi 或者 Apifox 维护简单项目也可以在项目根目录放一个 Markdown 格式的接口说明文档。文档要约定如下内容请求方式GET/POST/PUT/DELETE请求路径和参数路径参数、查询参数、请求体参数参数类型string/number/boolean/object/array返回格式code/message/data 的结构鉴权方式token 放在哪里怎么传错误码语义200 成功、401 未授权、500 服务器错误、业务错误从 400 开始联调前先用 Postman 把后端接口全部测一遍。实测下来后端写接口时引入一个全局性的统一返回结构ResultT联调效率能提升不少。前端看到code/message/data就知道怎么处理省去了解析各种怪异结构的时间。我在项目里定义了一个通用返回体Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }任何接口都返回Result包装后的对象异常处理用全局RestControllerAdvice捕获并统一转成这个格式前端就永远只需要处理一种结构代码逻辑非常简单。6.2 跨域问题的三种表现与对应解法跨域是全栈联调最常遇到的问题但它有不同层次的原因症状相同解法不同。第一种开发环境跨域。前端在http://localhost:3000后端在http://localhost:8080。浏览器拦截了跨域请求。通用的解法是后端加 CORS 配置让后端允许前端来源访问。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:3000) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }但更推荐用 Vite 代理解决前端配置代理后前端请求的地址看起来是同源的。代理方式更好是因为它把跨域问题的处理留在了构建层后端不需要关心前端运行在哪个端口而且开发环境和生产环境的代理配置方式保持一致生产环境用 Nginx。第二种后端接口报 CORS 错误但后端没有 CORS 配置。这种情况多半是浏览器请求直接打到了后端地址而请求方式是非简单请求比如 PUT/DELETE或者带了自定义请求头比如 Authorization。这时候浏览器会先发 OPTIONS 预检请求如果后端拦截器没放行 OPTIONS就会报 CORS 错。解决办法是拦截器放行 OPTIONS。第三种生产环境跨域。前端和后端部署在不同域名。这种情况下最稳妥的做法是用 Nginx 做反向代理让前后端在同一个域名下前端请求/api路径时 Nginx 转发给后端服务。6.3 开发过程中遇到的高频报错速查表下面整理一份联调阶段的常见报错和解决建议都是实操中反复出现的问题报错现象可能原因解决方法前端请求挂起很久后报 timeout后端服务未启动或接口路径不对检查后端日志确认服务启动检查请求的路径和 Controller 映射是否一致404 Not FoundURL 路径写错或 Controller 方法没有映射核对前端 api 模块中的 URL 和后端RequestMapping是否有出入401 Unauthorizedtoken 缺失或无效确认登录是否成功、token 是否在请求头中被正确携带500 后端报 NullPointerException后端代码对 null 值没有处理看异常堆栈定位到具体行号检查参数是否为空数据库中文乱码数据库连接串没有指定字符集或者表默认字符集错误在 JDBC URL 后加characterEncodingutf8建表时指定DEFAULT CHARSETutf8mb4MySQL 连接失败 Public Key Retrieval is not allowed连接串缺少 allowPublicKeyRetrieval 参数JDBC URL 中加入allowPublicKeyRetrievaltrue打包前端后访问页面空白路由模式或静态资源路径配置问题如果使用 history 模式Nginx 要配置 try_files 重写规则跨域报 Access-Control-Allow-OriginCORS 配置不当按照上文跨域问题的三种表现依次排查6.4 前后端联调中的时间同步与数据格式问题还有一种不报错但结果不对的坑最典型的是时间字段。后端 LocalDateTime 序列化后得到的是2024-05-20T10:30:00而前端希望显示的是2024-05-20 10:30:00。我见过不少项目因为时间格式对不上前端硬生生写了个字符串替换函数来转格式相当丑陋。正确做法是在后端做好全局配置让所有时间字段统一输出为固定格式前端直接拿到标准字符串后展示或传给后端。JSON 序列化这里再提一次在application.yml里加 Jackson 的时间格式配置代码里不需要再给每个LocalDateTime字段加JsonFormat注解。不过要注意的是如果分页返回的数据里包含时间排序序列化后的字符串格式影响不大只要保证比较的逻辑放在数据库层面做就行。另一个常见问题是前端传日期字符串给后端时后端用DateTimeFormat注解或全局配置 Jackson 的LocalDateTime反序列化格式来接收。我遇到过前端传2024-05-20 10:30:00后端接收后解析失败报 400 错误。这是因为 Fastjson 和 Jackson 对 LocalDateTime 支持有差异解决方式是统一用 Jackson并在全局配置里声明日期格式。7. 项目打包与生产环境部署7.1 前端构建与单页应用路由配置生产部署的第一步是前端构建。执行npm run buildVite 会生成dist目录里面是压缩后的静态资源文件。这里要特别关注两个点。第一构建产物的路径问题。如果你把前端部署在域名根路径/下那baseURL保持/api没问题但如果部署在子路径比如https://example.com/admin下Vite 构建时要在vite.config.js里配置base: /admin/否则静态资源的引用路径全部 404。第二前端路由的 history 模式需要服务端配合。Vue Router 的 history 模式URL 中不带#刷新在子路由页面比如/user时如果不配置服务端重写规则Nginx 只会看文件系统里有没有对应的user目录结果就是 404。Nginx 配置如下location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }try_files的$uri先看请求路径对应的文件是否存在如果不存在就回退到/index.html由前端路由接管。7.2 后端打包与启动脚本后端用 Maven 打包mvn clean package -DskipTests命令执行完会在target目录下生成fullstack-0.0.1-SNAPSHOT.jar。如果你用的是 Java 17 和 Spring Boot 3.x还有一个可执行 jar 的特性Spring Boot 的 Maven 插件会自动配置 Main-Class你不需要额外配置直接java -jar就能运行。启动命令java -jar fullstack-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod生产环境的配置建议单独建一个application-prod.yml里面配置生产环境用的数据库地址、端口、日志级别等和开发环境区分开。日志输出也建议配置到文件中不要只输出到控制台服务重启后日志就丢了。一个典型的日志配置logging: level: com.example.fullstack: info file: name: logs/fullstack.log线上环境有个小建议用nohup启动服务避免终端断开时进程被杀掉。或者更规范的写法是注册为 systemd 服务让操作系统管理进程生命周期开机自启、崩溃重启都能自动处理。7.3 Nginx 反向代理与服务部署细节Nginx 在这套系统中的角色是托管前端静态文件 反向代理后端 API。完整配置参考如下server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /var/www/frontend/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端 API 反向代理 location /api/ { proxy_pass http://127.0.0.1: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; } }这里有个细节location /api/加尾斜杠proxy_pass http://127.0.0.1:8080;不带尾斜杠那 请求/api/user/list会转发到http://127.0.0.1:8080/api/user/list路径和原来保持一致。如果把proxy_pass写成http://127.0.0.1:8080/请求会被转发到http://127.0.0.1:8080/user/list/api前缀会被吞掉。这个细节容易踩坑务必确认后端 Controller 的映射上有/api前缀否则会报 404。HTTPS 配置线上环境一般用 Certbot 自动申请证书并配置到 Nginx这里先不展开但强调一句生产环境不要裸奔 HTTP涉及登录和 token 的接口不上 HTTPS 相当于把门锁挂在门外面。7.4 数据库初始化与迁移策略生产环境数据库初始化不能靠手动一条条执行 SQL。我用 Flyway 做数据库版本管理每次 SQL 变更写一个带版本号的文件放到resources/db/migration目录应用启动时会自动执行未执行过的脚本。打比方说明一下Flyway 就像数据库的 Git每次变更都是一个 commit版本号递增它会在数据库中建一张flyway_schema_history表来记录已经执行过的版本下次启动时自动跳过已执行的只执行新增的迁移脚本。示例迁移文件-- V1__create_user_table.sql CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, email varchar(100) DEFAULT NULL COMMENT 邮箱, status tinyint(4) DEFAULT 1 COMMENT 状态1启用 0禁用, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;使用 Flyway 有两点要非常谨慎已经提交给团队的迁移脚本不要修改哪怕里面 SQL 写错了也要靠新的迁移脚本来修正否则团队的数据库版本就对不上了。另外生产库不要轻易执行flyway clean它会重建整个数据库操作前就算写满警告也不为过。8. 常用工具与效率提升清单8.1 接口调试工具的选择联调阶段接口测试工具的作用很大。Postman 是最经典的选择优势是功能齐全、支持 Collection 管理、环境变量设置。Apifox 是国内团队常用的方案优势是集成了接口文档管理、Mock 数据、自动化测试。如果项目中后端还没有完全开发完Apifox 可以直接在接口文档上生成 Mock 数据给前端先行开发这种前后端并行开发的模式能省不少时间。实际使用建议如果你是一个人做全栈直接用 IDEA 的 HTTP Client 就行不需要额外装软件。IDEA 里新建.http文件写请求可以配合{{var}}使用环境变量还可以在后端代码左侧直接 Run 生成对应的测试请求效率很高。8.2 数据库连接与查询工具数据库客户端方面Navicat 和 DataGrip 都常用。Navicat 功能全面、界面友好DataGrip 更适合写复杂 SQL它提供的智能提示和格式化功能非常好用。个人项目简单查询用 VS Code 的 MySQL 插件也可以凑合。我的建议是对于持久层查询用 MyBatis Plus 的LambdaQueryWrapper写条件查询已经非常直观大多数场景不需要打开数据库客户端。只有在排查数据问题时才需要连数据库。遇到数据库连接不上的情况先检查服务是否启动、端口是否被占用、认证插件版本是否匹配这三个点大多数问题都能解决。8.3 联调排查常用的浏览器开发者工具前端调试离不开浏览器开发者工具。Network 面板能看到每个请求的状态码、耗时、请求头和响应体Console 面板直接报错信息Vue 专门有 Vue DevTools 插件可以查看组件树、Pinia 状态和路由信息。排查问题建议按照这个顺序来看先在 Network 里确认请求发出去了没有、状态码是多少再看请求头里的 Content-Type 和 Authorization 对不对然后看响应体里的具体数据最后结合 Console 的报错信息定位问题。有的开发者一上来就在代码里打断点其实大多数联调问题在网络请求这一层就能定位。8.4 代码生成器的合理使用到了项目后期增删改查的代码写多了会产生明显的机械重复感。手动复制粘贴再改字段名容易出错我保留了一条规则多表关联、复杂业务逻辑的代码必须手写但单表 CRUD 的代码交给生成器生成生成完再人工审查和微调。MyBatis Plus 官方提供了一个代码生成器mybatis-plus-generator可以根据数据库表自动生成 Entity、Mapper、Service、Controller 代码。用它的时机设置在项目初始化阶段把表结构设计好一键生成基础代码后面再在 Service 中补充业务逻辑。这能省去大量重复劳动但你一定要会改生成的代码不然会陷入“低级错误看不出来”的局面。9. 项目扩展方向与进阶建议9.1 从单体应用到模块化演进当前项目的结构是单体应用这个结构适合大多数中小型项目。随着业务量增长你可以按模块拆分比如用户模块、订单模块、商品模块每个模块独立成一个 Maven 模块甚至独立成一个 Spring Boot 服务。拆分的关键依据是团队规模和部署复杂度不要因为趋势而盲目拆分。如果项目超过 10 个接口、3 个以上开发人员合理的模块化拆分能明显减少合并冲突和协作成本。反过来如果就是一个人的项目拆成微服务纯属自找麻烦。9.2 引入 Redis 缓存与接口性能优化项目中如果有热点数据比如用户信息、系统配置可以引入 Redis 做缓存层。实现方式是在原有的 Service 逻辑包一层先查缓存缓存未命中再查数据库同时把结果写入缓存并设置过期时间。用 Redis 的StringRedisTemplate就够不需要引入复杂的缓存框架。这个实践我建议放到项目稳定之后再做第二个版本迭代。先保证功能正确再考虑性能。接口响应时间慢先看是不是数据库查询的问题有没有索引、有没有 N1 查询再决定要不要加缓存。过早引入缓存会让 bug 排查多一层维度代价不小。9.3 安全加固与权限模型的进阶方向做全栈项目时很多人忽略了权限设计。目前项目只有“登录/未登录”两层。如果你的系统需要不同角色管理员、普通用户、访客可以引入 RBAC 权限模型用户-角色-权限三层结构。表设计上新建sys_role、sys_menu、sys_user_role、sys_role_menu四张表后端在拦截器或注解层面校验是否有指定权限。前端对应地可以做动态路由用户登录后根据后端返回的权限列表动态生成可访问的路由配置。这个是很多全栈项目的进阶难点会接触到更深的前后端正交互。建议把当前项目跑通后再往这个方向升级每一步都是新的技术内容。9.4 前端工程化的进一步打磨前端部分可以继续打磨的点很多代码规范ESLint Prettier、提交规范Commitlint husky、单元测试Vitest、自动化部署GitHub Actions。这些属于前端工程化范畴。举一个很实用的例子在项目里加 ESLint 和 Prettier 配置团队协作时代码风格统一少了无谓的争吵。不建议一开始就上全量工程化配置先把项目跑通再逐步加上这些工具学习曲线更平滑。10. 完整的实操流程对照表下表整理了这个项目从零到上线的完整操作清单按顺序执行每完成一步打一个勾阶段关键操作产出物耗时预估环境准备JDK 17、Maven、Node.js 20、MySQL 8开发环境跑通0.5 天数据库设计创建数据库和表、初始化测试数据SQL 脚本0.5 天后端骨架创建 Spring Boot 项目、MyBatis Plus 配置可启动空项目0.5 天后端核心接口登录、CRUD、拦截器、全局异常处理可测试的 REST API2-3 天前端骨架Vite 创建项目、配置路由、Pinia、Axios可运行前端基础工程1 天前端页面开发登录页、布局页、用户管理页可交互的完整页面2-3 天前后端联调测试登录流程、CRUD 流程、异常流程完整的业务闭环1 天打包部署前端 build、后端 jar、Nginx 配置生产环境可访问系统1 天上线检查安全、日志、备份、监控稳定运行的系统0.5 天这是一个典型的个人全栈项目的工期预估。如果你是第一次做时间会翻倍正常现象。如果遇到卡壳超过半天的问题建议跳过去先把主流程跑通再回头看卡住的部分。我个人的经验是全栈开发能力的提升不是线性的而是靠一个个完整的项目串起来的。这个项目虽然从技术角度看每个环节都是“常用技术”但当它们组合在一起形成完整闭环时你对前端和后端协作模式的理解会上一个台阶。当你遇到任何业务需求时脑子里能浮现出完整的请求链路和数据流转图而不是只盯着自己熟悉的那一层这就算真正入门了。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表