Vue 3 + SpringBoot前后端分离项目实战:从搭建到部署全流程
2026/9/20 21:00:27 网站建设 项目流程

简介:一份面向初中级开发者的前后端分离实战资源,基于Vue 3与Element Plus构建前端界面,后端采用Spring Boot 3,并整合Spring Security、JWT、Redis及文件上传下载等常用能力,适合希望快速掌握企业级项目搭建流程的学习者。资源共2000个文件,其中以1811个JavaScript文件为主,辅以Java、JSON、XML及SQL脚本,分别对应前端页面逻辑、后端服务代码、配置文件与数据库表结构,压缩包整体22.59MB,便于下载与检索。目前已有98人学习,可作为从零搭建Vue+SpringBoot项目的参考模板。除核心源码外,还包含数据库设计、接口设计和安全性设计思路,能够帮助读者理解JWT认证机制、Redis缓存应用、文件传输实现等关键环节,具备较强的实战参考价值。

1. 项目概述与整体设计

1.1 这个项目到底在做什么

说句实在话,前后端分离这个词已经被念叨了好几年,但真到了自己动手从零搭一个 Vue + SpringBoot 项目的时候,不少朋友还是会被各种细节卡住。这个项目本身并不复杂——前端用 Vue 3 + Vite,后端用 SpringBoot 2.7 + MyBatis-Plus,实现一套最典型的用户登录注册 + 数据列表查询 + 文件上传下载的小系统。但麻雀虽小五脏俱全,它能完整覆盖从开发到联调再到部署的一条链路。

我之所以选这个组合来做实例,是因为 Vue 和 SpringBoot 是目前国内中小团队使用率最高的前后端技术栈。Vue 上手曲线平缓、中文文档齐全、生态丰富;SpringBoot 则让 Java 后端的开发变得异常轻量,内置 Tomcat、自动配置、起步依赖这些特性,几乎就是为快速交付业务接口而生的。两个东西加在一起,能解决的问题非常广:企业内部管理系统、电商后台、内容管理平台、小程序管理端,基本上都能用这套骨架去套。

适合谁来参考呢?我建议有三类人认真看完:第一类是刚学完 Vue 和 SpringBoot 基础语法、想做一个完整项目练手的同学;第二类是公司里被迫从零搭建前后端分离工程、但没人带的新人开发;第三类是准备面试、需要把前后端交互链路讲清楚的朋友。这篇内容不是教科书,是我实际开发中一步步踩出来的经验总结,跟着做能少走很多弯路。

1.2 为什么选择 Vue 3 + Vite + SpringBoot 这套组合

选型这事情,没有绝对的最好,只有适合当前场景的方案。我最初也纠结过要不要用 Vue 2,毕竟公司老项目里 Vue 2 的存量很大。但考虑到 Vue 3 的组合式 API(Composition API)确实让逻辑复用变得干净很多,而且 Vite 带来的冷启动速度提升是肉眼可见的——以前用 Vue CLI 启动一个中型项目要等二三十秒,换成 Vite 之后基本秒开。对于开发体验来说,这个差距太明显了。

后端用 SpringBoot 2.7 而非 3.x,主要考虑的是生态兼容性。很多常用的第三方库(比如一些工作流引擎、代码生成器)在 SpringBoot 3.x 上还在适配期,而 2.7 是 2.x 系列的最后一个稳定版本,坑相对少得多。如果你的项目刚起步、不需要用那些冷门库,直接上 3.x 也没问题,但作为一个教学型实例,我选择更稳妥的版本。

数据库方面我用的是 MySQL 8.0 + MyBatis-Plus。MyBatis-Plus 对单表 CRUD 的增强非常实用,不需要手写 XML 就能完成大部分数据操作,非常适合快速搭建原型项目。同时我还配置了 MyBatis-Plus 的自动建表功能——这个功能在某些场景下极其好用,比如演示环境、测试环境、快速交付演示项目时,完全不需要手动去执行 SQL 脚本。

整个项目的前后端交互走的是 RESTful API + JSON,前端开发时通过 Vite 的代理(Proxy)解决跨域问题,生产环境则用 Nginx 做反向代理,将/api路径的请求转发到后端服务。这套方案是我在多个生产项目中验证过的,稳定性和可维护性都很好。

2. 前端 Vue 部分的关键搭建与实现

2.1 Vue 安装及环境配置的注意事项

很多新手在第一步就栽跟头——不是 Node 版本不对,就是 npm 源太慢导致依赖安装失败。我先说一下我自己比较推荐的配置流程。

安装 Node.js 的时候,尽量选择 LTS 版本。不要追新,有些 Vite 插件在最新的 Node 奇数版本上可能存在兼容性问题。装完 Node 顺手把 npm 镜像切到国内源:

npm config set registry https://registry.npmmirror.com

这一步能节省大量等待时间,实测下来安装依赖的速度能快三倍以上。

创建项目我推荐用 Vite 的官方脚手架:

npm create vite@latest frontend -- --template vue

这里解释一下为什么要用 Vite 而不是 Vue CLI。Vite 基于原生 ES Module,开发模式下不需要打包整个应用,而是按需编译浏览器请求的模块,所以启动速度和热更新速度都非常快。我印象最深的一次是,一个项目有三百多个组件,用 Vue CLI 改一行代码要等两秒左右才能看到热更新效果,换成 Vite 之后几乎是即时刷新。

创建完成后进入项目目录,安装路由和状态管理相关的依赖。这里我用了 Vue Router 4 和 Pinia:

npm install vue-router@4 pinia

然后安装 UI 组件库。如果做后台管理系统,推荐 Ant Design Vue;如果做面向用户的网站,推荐 Element Plus。这个项目我选的是 Element Plus,因为它的表格、表单、弹窗组件写起来非常顺手。

npm install element-plus @element-plus/icons-vue

这里有一个我自己总结的小经验:Element Plus 的图标需要单独安装,而且如果按需引入,需要在 main.js 里把所有用到的图标统一注册。很多人忘了这一步,结果页面上图标显示不出来,排查半天才发现是注册的问题。

2.2 路由设计:从登录页到业务页面的完整跳转逻辑

路由是前端项目的骨架,我见过很多项目路由写得很随意,结果后面加权限控制的时候痛不欲生。这里我推荐一套清晰的路由组织方式。

src/router/index.js下定义路由,至少要有两种路由:基础路由和业务路由。基础路由包括登录页、404 页面;业务路由放在一个统一的 Layout 布局组件之下,方便统一加载导航栏和侧边栏。

import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/login', name: 'Login', component: () => import('../views/Login.vue') }, { path: '/', component: () => import('../layout/Index.vue'), redirect: '/dashboard', children: [ { path: 'dashboard', name: 'Dashboard', component: () => import('../views/dashboard/index.vue') }, { path: 'user', name: 'UserList', component: () => import('../views/user/index.vue') } ] } ] const router = createRouter({ history: createWebHistory(), routes }) // 全局前置守卫,未登录跳转到登录页 router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') } else { next() } }) export default router

路由懒加载这个点要重点提一下——用() => import()的方式导入组件,Vite 会自动把每个页面拆成独立的 chunk,首屏只加载当前页面需要的代码。之前有个项目没有做懒加载,首屏体积到了 2MB 多,白屏时间差不多两秒;改成懒加载之后首屏压到了 500KB 以内,体验提升非常明显。

还有一个细节是路由模式。我这里用的是createWebHistory,也就是 HTML5 History 模式,URL 看起来是http://xxx.com/user而不是http://xxx.com/#/user。但要注意,使用 History 模式后,线上的 Nginx 需要配置try_files把所有路径都重定向到index.html,否则刷新页面会报 404。这个在后文部署部分会细说。

2.3 登录注册模块:与后端接口的第一次握手

登录注册是整个前后端项目里第一次真正意义上的数据交互,也是很多人第一次被跨域问题卡住的地方。我先说我前端这边的写法。

登录页模板里放一个表单,两个字段:用户名、密码,加一个登录按钮。提交的时候调用封装好的登录接口:

import request from '../utils/request' export function login(data) { return request({ url: '/api/auth/login', method: 'post', data }) }

这里的request是基于 Axios 封装的一个实例,统一配置了 baseURL 和拦截器。重点说一下响应拦截器的写法,它决定了你在组件里代码能少写多少:

import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 10000 }) // 请求拦截器:统一携带 token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器:统一处理异常 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { ElMessage.error(error.response?.data?.message || '网络异常') return Promise.reject(error) } ) export default request

这样做的好处是,组件里调用接口只需要关心返回的数据,不需要每写一个接口都处理一遍code !== 200的逻辑分支。在登录页面,拿到后端返回的 token 后存到 localStorage,然后调用router.push('/dashboard')跳转到首页。整个交互链路就完成了。

3. 后端 SpringBoot 部分的搭建与核心接口开发

3.1 快速创建一个 SpringBoot 项目,避免超时问题的三个办法

后端的项目创建我一般用 IDEA 的 Spring Initializr,但很多人都会遇到一个很烦的问题——创建项目时连接 start.spring.io 超时,转半天圈然后报错。这里讲三个我自己验证过的方法。

第一个方法,改 IDEA 的 HTTP 代理设置,让它能顺利访问外部网络。但这办法受限于公司网络策略,有些环境下不好使。

第二个方法,使用国内的 Spring Initializr 镜像。阿里云提供了一个,地址是https://start.aliyun.com。在 IDEA 的 Server URL 处替换掉默认地址,创建速度就是一个字——快。需要注意的是,阿里云镜像里提供的依赖版本可能会比官方源旧一点,但对大多数项目没有影响。

第三个方法,是离线创建。在 IDEA 的 Maven 配置里设置好本地仓库路径,然后自己手动在项目里添加pom.xml,最后右键选择 Add as Maven Project。这种方式适合网络环境极端恶劣的情况。

我平时最常用的是第二个方法,基本上选好 Spring Web、MyBatis-Plus、MySQL Driver、Lombok 这几个依赖,十几秒项目就创建完了。

3.2 核心依赖与配置文件:yml 里的静态资源和密码处理经验

项目创建好之后,第一步是改配置文件。我习惯把默认的application.properties改成application.yml,因为 YAML 的层级结构更清晰,尤其是配置多数据源、Redis 这类需要嵌套的信息时,可读性高得多。

一个需要注意的地方是 SpringBoot 2.7 以后的配置项变更。比如spring.redis改成了spring.data.redis,以及 MyBatis 配置里的mapper-locations路径写法。版本不同,配置不同,搜资料的时候要先确认自己用的版本,不然被旧资料坑了会花很多时间调莫名其妙的问题。

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: ${DB_PASSWORD:123456} mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: table-prefix: t_

关于密码配置我多说一句。生产环境极度不建议把数据库密码明文写在 yml 里,这属于安全底线问题。SpringBoot 本身没有内置的加密方案,但可以引入 jasypt-spring-boot-starter,对配置文件中的密码进行加密。基本用法是在配置项外面套一层ENC()标记,然后在启动参数里传入解密密钥:

password: ENC(加密后的密文)

启动时:

java -jar demo.jar --jasypt.encryptor.password=你的密钥

这套方案在 demo 里可以不搞,但放到公司项目里,建议一定要加上。安全问题越早考虑,后面成本越低。

3.3 表结构自动创建:MyBatis-Plus 中的建表工具类实现

这个项目的数据表很少,总共就用户表、日志表几张,所以我没有引入专门的数据库迁移工具(比如 Flyway),而是写了一个简单的表结构自动创建工具类。

原理其实很简单:项目启动时读取一个schema.sql文件,检查数据库中是否存在目标表,如果不存在就执行建表语句。实现方式是在 SpringBoot 的 ApplicationRunner 启动类里做处理:

@Component public class TableInitRunner implements ApplicationRunner { @Resource private JdbcTemplate jdbcTemplate; @Override public void run(ApplicationArguments args) { String checkTableSql = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'demo_db' AND table_name = 't_user'"; Integer count = jdbcTemplate.queryForObject(checkTableSql, Integer.class); if (count == null || count == 0) { executeSqlScript(new ClassPathResource("sql/schema.sql")); } } }

这样带来的好处是很直观的——把项目拉下来运行,不必手动去 Navicat 里执行建表 SQL,前后端同学都省心。尤其是快速搭建演示环境的时候,这个细节能省掉至少十分钟的沟通成本。

不过要说清楚,这种方案只适合表结构不频繁变动的场景。如果项目进入迭代期,表字段经常调整,还是得回归 Flyway 或者 Liquibase,否则增量 SQL 的执行顺序会成为新的麻烦。

3.4 用户登录接口:从 Controller 到 Service 的完整代码路径

后端接口开发我遵循三层结构:Controller(接收请求)、Service(业务逻辑)、Mapper(数据访问)。以登录接口为例,代码路径大概是这样的。

首先是 Controller 层:

@RestController @RequestMapping("/api/auth") public class AuthController { @Resource private AuthService authService; @PostMapping("/login") public ResultVO<String> login(@RequestBody LoginDTO loginDTO) { String token = authService.login(loginDTO); return ResultVO.success(token); } }

然后是 Service 层。注意这里有一个细节——我用了ResultVO统一包装返回结果,结构是{ code, message, data }这样的格式。这样前后端约定好了交互协议,前端拦截器里的res.code !== 200判断才有意义。

@Service public class AuthServiceImpl implements AuthService { @Resource private UserMapper userMapper; @Override public String login(LoginDTO loginDTO) { // 1. 查询用户 User user = userMapper.selectByUsername(loginDTO.getUsername()); // 2. 校验密码(BCrypt加密存储) if (user == null || !BCrypt.checkpw(loginDTO.getPassword(), user.getPassword())) { throw new BizException("用户名或密码错误"); } // 3. 生成token并返回 return JwtUtil.generateToken(user.getId(), user.getUsername()); } }

密码为什么一定要用 BCrypt 加密存储?因为 MD5 撞库太容易了,反查一个弱密码几乎是秒级的事。BCrypt 自带随机盐,能有效对抗彩虹表攻击,而且计算成本可以调整,这是目前业界比较推荐的密码存储方案。

用户表结构很简单,对应字段就是 id、username、password、created_time 几个,通过 MyBatis-Plus 的注解就能完成映射:

@Data @TableName("t_user") public class User { @TableId(type = IdType.AUTO) private Long id; private String username; private String password; @TableField("created_time") private LocalDateTime createdTime; }

3.5 跨域问题解析:前后端分离后最常踩的一个坑

跨域问题我单独拿出来说,因为十个前后端联调项目里有九个会被它卡一下。跨域的本质是浏览器的同源策略——只有当请求的协议、域名、端口都一样时,浏览器才允许 JavaScript 读取响应。

在开发环境下,前端跑在http://localhost:5173,后端跑在http://localhost:8080,端口不同,浏览器就会拦截跨域请求。三种解决方案我都用过,说下各自的适用场景。

第一种是后端开 CORS 全局配置。在 SpringBoot 里写一个配置类:

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

这种方案配置简单,适合生产环境前后端域名不同的情况。但我个人不太建议在生产环境把允许来源设为*,最好显式写成你前端的域名,特别是如果网站要存 Cookie 做会话保持,*会导致前端请求无法携带凭证,必须精确指定来源。

第二种是前端开发环境下的代理方案。在 Vite 的vite.config.js里配置:

export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

这样前端发请求时,浏览器看到的请求目标是http://localhost:5173/api/auth/login,同源,不触发跨域。Vite 把请求转发给后端的http://localhost:8080/api/auth/login。这是开发环境最推荐的方式,不需要后端做任何额外配置,同时还能灵活地切换后端地址。

第三种是生产环境用 Nginx 反向代理。这种方式本质上和第二种类似,只是转发职责从 Vite 交给 Nginx 来做。在 Nginx 配置里加一段 location:

location /api/ { proxy_pass http://127.0.0.1:8080; }

三种方案可以在同一个项目里组合使用:开发走 Vite 代理,生产走 Nginx 代理,后端保持不提供跨域配置。这样后端的接口更为独立,也方便直接供第三方系统调用。

4. 前后端联调:从零到一打通全流程

4.1 联调前的三个准备工作

前后端联调不是拉个群两边把代码一跑就算完的。我经历过太多次联调现场因为准备不足而浪费一两天时间的情况。做足准备,联调效率能提升一倍。

第一个准备是接口文档先行。不要后端写完接口再补文档,要在动手写代码之前就把接口的路径、请求参数、返回结构定义清楚。工具上我推荐 Apifox 或者 YApi,可以在线管理接口文档并生成 Mock 数据。前端根据文档写页面,后端根据文档开发接口,两边并行推进,联调的时候只需要对细节,而不是争论字段叫什么名字。

第二个准备是统一错误码规范。项目里返回结构统一是code、message、data,但 code 的取值范围和含义要提前约定。比如 200 是成功,401 是未认证,403 是权限不足,500 是服务器内部错误。前端拦截器根据 code 做统一处理,比如遇到 401 就跳转登录页,遇到 500 就弹出错误提示。如果每个接口返回的错误码风格不同,前端拦截器就形同虚设。

第三个准备是接口字段命名约束。Java 后端习惯用驼峰命名(userName),数据库习惯用下划线(user_name),而前端 JavaScript 两种风格都有人写。我建议前后端接口传输的统一用驼峰命名,数据库字段映射交给 MyBatis-Plus 的下划线转驼峰配置,前端永远只面对驼峰字段,这样最省心。

4.2 如何快速区分前后端 Bug

联调阶段每天都要面对各种报错,如何快速判断问题出在前端还是后端,这是每个开发都必须掌握的技能。我有几个高效的定位手段。

第一招是看 Network 面板。按 F12 打开浏览器开发者工具,切到 Network 标签页,找到对应的接口请求。如果请求状态码是 404 或 405,说明路径或请求方法不对,大概率是后端问题;如果状态码是 200,但返回数据里的 code 是 500,说明后端内部抛异常了,点开 Response 看具体错误信息;如果请求根本没有发出,或者请求 vender 里标着(failed) net::ERR_FAILED,那是前端的问题,先检查代理配置和接口地址。

第二招是分阶段确认。前端能显示的页面结构、交互逻辑属于前端问题;数据是否正确、接口是否返回符合预期的内容,属于接口问题;接口返回的内容是否被正确处理,属于前端问题。很多人会在这三层之间来回横跳,我的习惯是先在 Network 面板里确认后端返回的原始 JSON 数据是否符合预期,如果正确,那问题一定在前端处理数据的代码上,从那儿往下查很快。

第三招是巧用后端日志。SpringBoot 默认会输出 WEB 层的访问日志,可以看到每个请求的路径、参数、处理耗时以及异常堆栈。如果前端说接口报错,但后端日志里压根没有相关请求记录,那多半是请求没到后端,问题出在代理或路由配置上。这个判断方法在排查跨域和代理问题时尤其高效。

4.3 文件上传下载这类耗时接口的联调细节

这个项目里我还加了一个文件上传下载的接口。初看很简单,但实际联调时有不少细节容易出问题。

前端的文件上传,我用 Element Plus 的el-upload组件,配置好 action 地址和请求头即可。但有个坑是,文件上传接口一般不需要也不应该携带认证 header,而我在 Axios 请求拦截器里统一加了Authorization头,这会导致部分浏览器触发一次额外的 OPTIONS 预检请求。后端如果没处理好预检请求,就会报跨域错误。解决办法是在后端过滤器里对 OPTIONS 请求直接放行:

if ("OPTIONS".equals(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); return; }

大文件上传则是另一个话题。如果文件超过 100MB,直接上传很容易超时,我推荐做分片上传。前端把文件切割成固定大小的分片(比如每片 5MB),逐个上传,后端接收后合并。断点续传也依赖这个基础——记录已上传的分片列表,下次上传时跳过已完成的分片。

我在项目里实现了一个简单的分片上传接口:前端用Blob.prototype.slice切分文件,后端收到分片后暂存在临时目录,最后一片上传成功后通知后端合并。代码本身不复杂,但能覆盖绝大多数大文件上传的业务需求。

4.4 Vite 代理配置技巧与常见联调报错汇总

前面已经提过 Vite 代理的基础配置,这里补充一些实际使用中的进阶技巧。

如果你需要同时访问多个后端服务,可以通过配置多个代理规则实现:

proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true }, '/file': { target: 'http://localhost:9090', changeOrigin: true } }

changeOrigin: true这个选项的作用是修改请求头中的 Host 字段为目标地址的 Host,如果不设置,有些后端服务器会校验 Host 并拒绝请求。

联调中最常见的报错我整理成了一张表,方便对照排查:

现象可能原因解决办法
浏览器报 CORS error后端未配置CORS或代理规则不匹配检查代理配置,或在后端添加CORS配置
请求404前端代理路径与后端接口路径不一致核对接口URL前缀
请求返回500后端代码异常查看后端日志定位异常堆栈
前端拿不到data字段后端返回结构不统一约定统一ResultVO结构
Token失效但登录页不跳转拦截器未处理401状态码在响应拦截器中加错误跳转处理

4.5 调试工具与接口测试的实用技巧

联调阶段有两类工具是效率神器。

第一类是前端 Vue 调试工具。Vue 3 对应的浏览器插件是Vue.js devtools,在 Chrome 商店就能安装。它能直观查看组件的 props、data、computed 值,还能追踪事件和路由。排查响应式数据没更新、computed 计算错误这类问题非常高效。调试的时候先在 devtools 里检查组件 data 的值是否正确,如果不正确再看是不是接口数据的问题,逐层排查比瞎猜有用得多。

第二类是接口测试工具。Apifox 有个很好用的功能——从一个接口的测试用例里直接生成前后端联调时的 Mock 数据。前端页面开发阶段不需要等待后端接口就绪,直接调用 Mock 数据就能正常渲染;后端接口真正写好后,只要把测试环境地址切换到真实后端,页面就能无缝衔接真实数据。这个工作流我用了很久,确实能有效减少联调等待时间。

5. 前后端项目的部署与上线

5.1 前端打包常见错误与 Nginx 配置指南

前端开发完后的部署,我一般分四步:打包、上传、配 Nginx、验证。

打包前建议确认环境变量配置。Vite 默认区分开发环境和生产环境,通过.env.development.env.production文件管理。生产环境里的VITE_API_BASE_URL我习惯设为空字符串,让所有请求都走同源相对路径/api,然后交给 Nginx 做代理转发。这样部署路径灵活,不需要根据不同机房改前端代码。

执行npm run build之后,产物在dist目录。上传到服务器后,Nginx 配置的核心部分长这样:

server { listen 80; server_name your-domain.com; root /var/www/frontend/dist; index index.html; # 前端路由 history 模式刷新支持 location / { try_files $uri $uri/ /index.html; } # 接口反向代理 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; } }

try_files $uri $uri/ /index.html;这一行极其关键。如果没有这一行,刷新http://your-domain.com/user页面时,Nginx 会去物理目录找 user 文件,找不到就报 404。加上之后,Nginx 发现文件不存在会回退到index.html,由前端路由接管渲染。这个坑我在初学阶段栽过一次,后来每次部署都要确认一遍。

5.2 使用 Docker Compose 一键编排前后端服务

如果服务器是全新的,我推荐直接用 Docker Compose 来编排服务。把 MySQL、后端、前端 Nginx 打成三个容器,一条命令启动所有服务,可复现性和可维护性都很好。

先说镜像构建。前端需要一个带 Nginx 的自定义镜像,Dockerfile 大概长这样:

FROM node:18-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm install --registry=https://registry.npmmirror.com COPY . . RUN npm run build FROM nginx:alpine COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]

这里用了多阶段构建——第一阶段用 Node 镜像编译前端资源,第二阶段把编译产物拷贝进干净的 Nginx 镜像。这样最终镜像体积小,也不包含编译工具链的冗余文件。

后端同理:

FROM maven:3.8-openjdk-8 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests FROM openjdk:8-jre-alpine COPY --from=build /app/target/demo.jar /app/demo.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app/demo.jar"]

然后写docker-compose.yml

version: '3.8' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: demo_db ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql restart: always backend: build: ./backend depends_on: - mysql environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: root123 ports: - "8080:8080" restart: always frontend: build: ./frontend depends_on: - backend ports: - "80:80" restart: always volumes: mysql_data:

这里要特别注意的是 Docker Compose 内部网络的 DNS 解析——后端容器里访问数据库不能写localhost,而要写服务名mysql。同理,前端 Nginx 容器里反向代理后端,也不应该写127.0.0.1,而要写后端服务名backend。这个细节如果忽略,部署上去就会发现容器之间网络不通。

启动命令很简单:

docker-compose up -d --build

整个前后端项目在三分钟内就能在任意一台安装好 Docker 的服务器上跑起来。这对于交付演示环境、临时测试环境来说非常省事。

5.3 云服务器部署的完整步骤与常见坑

不习惯用 Docker 的话,在云服务器上直接部署也更常规。流程一般是:服务器装好 JDK 和 Nginx,上传后端 jar 包运行,再上传前端 dist 目录到 Nginx 站点目录。

后端 jar 包启动推荐用nohup加日志输出:

nohup java -jar demo.jar --spring.profiles.active=prod > app.log 2>&1 &

这里有两个注意点。第一,--spring.profiles.active=prod指定配置文件,生产环境走生产环境的数据库配置。第二,> app.log 2>&1将标准输出和错误输出都重定向到日志文件,这样排查问题的时候可以在日志文件里看到完整的异常信息。

启动后验证接口是否正常:

curl http://127.0.0.1:8080/api/auth/login

如果后端正常,再检查 Nginx 配置是否生效:

nginx -t nginx -s reload

最后从浏览器访问http://服务器公网IP,能看到前端页面说明部署成功。

云服务器部署常见的坑我来盘一盘:

第一,安全组端口没开。阿里云和腾讯云的服务器默认只有 22 端口是开放的,要在控制台安全组里放行 80 和 8080 端口,否则外部无法访问。这个问题非常常见,我曾经在一次项目演示前五分钟才发现端口没开,当场排查了半天。

第二,数据库访问权限问题。云服务器的 MySQL 默认只监听本地回环地址,后端在与数据库连接时要注意账号是否有远程访问权限,但在单机部署中通常都用localhost连接,这时问题不大。如果数据库单独部署在另一台机器上,就要到 MySQL 用户权限表里给应用账号授权。

第三,防火墙冲突。云服务器上可能同时存在云安全组和系统内部的 firewalld/iptables 两层防护,两层都要放行对应端口。经常有人配好了安全组仍然访问不了,一查发现是机器内防火墙把端口拒了。

6. 常见问题与排查技巧实录

6.1 Maven 与 npm 依赖下载超时的终极解决方案

几乎每个新人都被这个问题折磨过。npm 依赖装一半卡住不动,Maven 依赖下载一直报超时,本质原因都是默认源在国外,网络不稳定。

Maven 源修改很简单,打开 Maven 安装目录下的conf/settings.xml,在<mirrors>节点里加入阿里云镜像:

<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>

npm 源前面已经说过,改成registry.npmmirror.com就行。这俩改完之后,下载速度提升一个量级,基本上不会再有超时问题。

还有一种情况,本地网络本身没问题,但依赖就是下载慢。可以试试先手动下载 jar 包放到本地仓库里,再用mvn install:install-file命令安装到本地。这种方式在应急场景下比较有用,比如某一天某个依赖分发出问题了,从中央仓库拿不到包。

6.2 SpringBoot 版本太高导致的各种兼容性问题

版本选型是后端项目成败的关键细节之一。我见过不少项目因为用了刚发布的最新版 SpringBoot,结果引入某个第三方库时,发现对方还没有适配新版本,导致项目启动失败或者运行时出现古怪异常。

SpringBoot 3.x 最大的变化是底层从javax.*切换到了jakarta.*命名空间,这意味着所有依赖这个命名空间的第三方库都需要升级适配。如果你在项目中还在用一些老牌的、维护不频繁的库(某些公司内部的框架、老版本的工作流引擎),升级到 SpringBoot 3.x 基本就是劝退。

我的经验是:公司项目跟着稳定版本走,个人学习可以跟着最新版本走。目前 2.7 和 3.2 都是相对稳妥的选择。如果项目里有大量历史遗留依赖,2.7 会更合适;如果是全新项目,直接 3.2 也没问题,毕竟技术总要往前迭代。

6.3 Vue 常见面试考点与项目实战的结合

这个项目写完之后,最大的收益其实是面试。面试官问 Vue 相关的问题,基本都绕不开这几个:v-ifv-show的区别、Vue 3 组合式 API 和选项式 API 的区别、key的作用、路由守卫的用法。如果只是背概念,答不出亮点,但如果结合项目实战来讲,效果会好很多。

比如v-ifv-show——实际项目中,登录状态、权限按钮这种频繁切换的场景我一般用v-show,因为它只是切换 CSS 的display属性,不需要重新渲染组件,开销小;而像低概率出现的弹窗、不同角色的大块页面内容,我会用v-if,因为它能彻底销毁组件,避免内存泄漏。

再比如路由守卫,我的项目里不仅做了登录校验,还做了一个小的权限控制:根据后端返回的角色信息,在路由守卫里判断用户是否有权访问某个页面,没有权限就跳转 403 页面。把这个逻辑讲出来,面试官会觉得你是真的在项目里解决过问题,而不是背概念。

还有一个必问的:Vue 3 组合式 API 和选项式 API 的区别。我在项目里两种都写过,实际体会是——组合式 API 对复杂逻辑的复用确实方便,用computedwatchref这些组合函数能把原本散落在datamethodswatch里的代码组织到一起。但简单页面用选项式 API 也完全没问题,代码反而更直观。面试官问这个问题的时候,你就可以说:两种都实践过,我的选型标准是逻辑复杂度,而不是盲目追新。

7. 项目扩展方向与我的实操心得

项目跑通之后,完全可以基于这个骨架继续扩展。我列几个我实际做过或者觉得很有价值的方向。

第一个方向是引入工作流引擎。SpringBoot 集成 Flowable 是做审批类系统最常见的需求。比如请假审批、费用报销这些场景,用 Flowable 定义好流程模板,后端通过 API 启动流程实例,在任务完成节点做业务处理,前端通过接口查询待办任务、已办任务。这套能力在很多企业级系统里都是刚需。Flowable 7 有相对好用的 REST API,可以直接在前端调用,也可以包一层自己的接口做权限控制。

第二个方向是音视频处理。Vue 前端播放 HLS 视频流(m3u8 格式)的需求越来越多,特别是直播回放、监控视频这类场景。前端可以用hls.js这个库,几行代码就能实现 m3u8 的播放。需要注意的是,浏览器原生的 video 标签并不支持 m3u8 格式(除了 Safari),必须通过hls.js做转封装,或者使用带 MSE 支持的播放器组件。这个功能我建议在有相关业务需求时再引入,不要一开始就把项目的复杂度抬高。

第三个方向是系统集成能力。项目里可以接入第三方平台的开放能力。前端做网页版 H5 应用时,经常需要对接 IM 工具和内部平台的免登录流程,需要遵循对应的 OAuth 授权协议。开发方式大同小异:拿到授权码之后请求后端交换 token,后端记录用户信息并生成自己的登录态。

整个项目从搭建到部署走下来,我最大的体会是:前后端分离项目真正的难点不在技术栈本身,而在工程化意识。环境怎么统一、接口怎么约定、错误怎么排查、部署怎么做,这些能力比单纯会写几个组件和接口重要得多。希望这篇内容能帮你把这条链路完整地走一遍,后续不管换什么框架、换什么语言,底层的思路都是相通的。

最后分享一个我这些年一直在用的习惯:每次新项目动工前,先花半小时把接口文档和目录结构定好,本地能少熬两天的夜。这个投入产出比,是我踩过无数坑之后最想告诉你的。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询