1. JavaWeb 与 Vue 的前后端协同底层逻辑
这几年做 JavaWeb 项目,最绕不开的一个话题就是前后端分离。我刚接触这套技术栈的时候,也是一股脑把 Java 后端代码和 Vue 前端代码塞在同一个项目里,用 idea 直接启动一个 Tomcat,然后前端页面靠 JSP 渲染。那时候确实很方便,但随着业务复杂起来,JSP 的维护成本越来越高,动不动就是一整个页面堆积了大量 Java 代码逻辑和 HTML 标签,改一个样式都要小心翼翼,生怕动了某个隐藏的 Java 代码块。
后来我转向前后端分离架构,后端用 Spring Boot 提供纯接口,前端用 Vue 构建独立应用。这个转变带来最直观的变化就是职责清晰:Java 只负责数据接口、事务处理和权限控制,Vue 只负责页面交互、路由跳转和状态管理。这样两边可以并行开发,前端不需要等后端把页面模板写好,后端也不需要关心某个按钮到底长什么样。
不过前后端分离不是简单地把代码拆开就行,真正的难点在于双方如何约定数据结构、如何处理跨域、如何保证联调效率。我见过不少团队,前后端分是分了,但接口文档全靠口头传,字段名改来改去,前端拿到的时间字段是一串数字时间戳,后端又期望前端传一个固定格式的字符串,联调阶段简直是一场灾难。
这里我给大家一个很实在的建议:无论项目大小,先把接口规范定下来。拿一个用户管理模块来说,后端返回的数据格式可以统一设计成下面这样。
{ "code": 200, "message": "操作成功", "data": { "userId": 1, "username": "zhangsan", "createTime": "2025-01-12 10:30:00" } }code 用来标识业务状态,200 是成功,500 是服务器异常,401 是未登录,403 是无权限。message 用来展示给用户看的提示信息。data 才是真正的业务数据。这个结构虽然简单,但能让前后端对接时的沟通成本降到最低。
时间字段这个问题也应该引起重视。Java 后端默认返回的时间可能是2025-01-12T10:30:00,也可能是时间戳,Vue 这边如果不做处理直接展示,用户看到的就是一串数字。我的习惯是后端统一返回yyyy-MM-dd HH:mm:ss格式化后的字符串,前端拿到直接渲染就行。只要约定好了,同类问题就不会反复出现。
跨域问题也是前后端分离绕不开的坎。我在 idea 里启动后端项目,端口是 8080,Vue 开发服务器跑在 5173,前端页面去请求后端接口,浏览器会直接拦截,因为端口不同就属于跨域。解决方案有很多,我偏向于在后端写一个 CORS 全局配置类。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }这个配置类的作用就是把后端所有接口都放开跨域限制。开发阶段这样配没问题,但到了生产环境,建议把 allowedOriginPatterns 改成具体的域名,不然谁都能跨域请求你的接口,安全上会存在隐患。
前后端分离的核心不是技术选型,而是协作方式。Java 后端和 Vue 前端各司其职,又通过统一的接口约定紧密协作,这才是这套技术栈真正高效运转的关键。理解了这层关系,后面不管是配置环境、编写代码还是打包部署,思路都会顺畅很多。
2. 从零搭建 JavaWeb 与 Vue 的开发环境
很多新手在环境配置这一关就被卡住了,网上的教程东拼西凑,不是版本对不上,就是步骤缺胳膊少腿。我自己一路踩坑过来,把关键环境配置整理成一套可以直接照做的流程,不说百分之百覆盖所有情况,但至少能帮你少走大半弯路。
2.1 idea 运行 JavaWeb 项目配置要点
idea 里跑 JavaWeb 项目,最常见的组合是 JDK 8、Maven 3.6、Spring Boot 2.x。这三个版本搭配起来最稳,网上的资料也最多,遇到问题随便一搜就能找到解决方案。JDK 8 虽然年头久了,但在企业级项目里依然占据主流,很多老系统的运行环境就是它。
先检查 Maven 配置。idea 自带的 Maven 版本不一定合适,我建议用自己下载的 Maven,并且把 settings.xml 里的本地仓库路径改成一个容易找到的位置。具体操作是打开 idea 的设置,搜索 Maven,分别设置 Maven home path、User settings file 和 Local repository。很多人忽略了这一步,结果 Maven 依赖下载到了 C 盘的系统目录里,越积越多,最后磁盘空间告急。
Maven 依赖下载慢的问题也很常见。国内访问中央仓库经常超时,我的解决办法是在 settings.xml 里配置阿里云的镜像。这个操作属于常规开发实践,能极大提升依赖下载速度,减少等待时间。
<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>配好镜像后,新建 Spring Boot 项目时,idea 会自动从镜像仓库拉取依赖,速度比之前快了好几倍。另外建议给 idea 的 Maven 设置里勾选上自动导入依赖,这样 pom.xml 一改动,依赖就会自动更新,省去手动刷新的麻烦。
运行配置也不难。Spring Boot 项目本质上是一个带 main 方法的 Java 应用,直接在 idea 里运行 application 类就行。如果你想用 Tomcat 来跑,也不是不可以,但建议用 Spring Boot 内置的 Tomcat,毕竟把项目打包成 jar 直接java -jar运行,比部署 war 包要简单得多。
2.2 Vue 安装及环境配置保姆级流程
Vue 的环境配置看着麻烦,其实核心就三样:Node.js、npm 和 Vue CLI。Node.js 是 JavaScript 的运行环境,npm 是包管理工具,Vue CLI 是脚手架工具。装好这三样,一个 Vue 项目就能跑起来了。
Node.js 的版本要注意一下,Vue CLI 5.x 要求 Node.js 版本不低于 12,但也不要太高。我遇到过 Node.js 版本过新导致某些依赖编译报错的情况,目前用 Node.js 16 LTS 版本最稳,兼容性最好。去官网下载对应安装包,一路下一步就行,没什么特别的操作。
装完 Node.js 后,npm 也跟着装好了。不过 npm 默认的下载源在国外,安装依赖时经常卡住,和 Maven 一样,我们需要配置一下镜像源。
npm config set registry https://registry.npmmirror.com这个命令执行完后,可以输入npm config get registry验证一下,只要能打印出刚才设置的地址,就说明镜像源配置成功了。
接着安装 Vue CLI。
npm install -g @vue/cli全局安装完成后,输入vue --version检查版本。看到版本号输出,说明 Vue CLI 已经就绪。接下来创建一个项目。
vue create my-project创建过程中会让你选择预设配置,新手建议直接选 Default,也就是 Vue 3 + Babel + ESLint 的基本组合。项目创建完成后,进入项目目录,执行npm run serve,浏览器打开终端提示的地址,就能看到 Vue 的欢迎页面了。
到这里,Vue 的基础环境就算搭建完毕。后面你就可以在这个项目里编写组件、配置路由、引入 UI 库,一步步搭建自己的前端应用。
2.3 环境配置常见坑位排雷
环境配置这块我踩过不少坑,挑几个最典型的分享一下。
第一个坑是 JDK 版本不匹配。Spring Boot 2.x 依赖的某些库在 JDK 17 上会报错,而 Spring Boot 3.x 又强制要求 JDK 17 及以上。如果你的项目是照着老教程做的,用了 Spring Boot 2.7,却装了 JDK 17,启动时大概率会碰到UnsupportedClassVersionError。解决办法很简单,把 JDK 切换回 8,或者在 pom.xml 里升级 Spring Boot 版本并同步调整依赖。
第二个坑是 Node.js 与依赖版本冲突。创建 Vue 项目时,如果遇到Node.js version 12.0.0 is not supported这类提示,一方面要检查 Node.js 版本是否过低,另一方面也可能是依赖库版本过新,对 Node 版本要求高。我习惯把 Node.js 锁定在 LTS 版本,能省掉很多麻烦。
第三个坑是端口冲突。后端接口占用了 8080,Vue 开发服务器占用了 5173,但当两个项目同时运行时,如果其中某个端口被其他进程抢占,启动就会失败。Windows 下可以用下面这个命令查看端口占用情况。
netstat -ano | findstr 8080找到占用端口的进程 PID 后,在任务管理器里结束对应进程,或者修改项目的端口配置。后端是在 application.properties 里修改 server.port,前端是在 vite.config.js 里修改 server.port。
环境配置其实就是一层窗户纸,捅破了就没什么神秘的。按照上面的流程走一遍,正常情况下半小时内就能把 JavaWeb 后端和 Vue 前端的环境都跑通。
3. Vue 核心知识点实战拆解
Vue 的学习曲线相比其他前端框架算是比较平滑的,但有一些核心知识点是必须吃透的。这些知识点不搞明白,写出来的代码可能也能跑,但一旦遇到复杂场景,就会手忙脚乱。我把 Vue 开发中最高频的几个知识点拿出来逐一拆解。
3.1 vue 指令简写与样式冲突处理
Vue 指令是模板渲染的基石。最常用的 v-bind 和 v-on 都有简写形式,v-bind 简写成冒号,v-on 简写成 @。刚开始写的时候可能不习惯,但用顺手了会发现代码简洁很多。
<!-- 完整写法 --> <img v-bind:src="imageUrl" v-on:click="handleClick"> <!-- 简写写法 --> <img :src="imageUrl" @click="handleClick">v-model 用于表单双向绑定,v-if 和 v-show 用于条件渲染,v-for 用于列表渲染,v-html 用于渲染 HTML 字符串。这里提醒一下项目实战中的安全意识,尽量不要用 v-html 渲染用户输入的内容,很容易引发 XSS 攻击。如果非用不可,也要在后端做好数据清洗和过滤。
样式冲突是组件化开发中绕不开的话题。默认情况下,Vue 组件的样式是全局生效的,也就是说 A 组件里定义的类名,在 B 组件里也能用,这就可能导致样式互相覆盖。解决办法是在 style 标签上加上 scoped 属性。
<template> <div class="card"> <p>文章内容</p> </div> </template> <style scoped> .card { border: 1px solid #eee; padding: 16px; } </style>scoped 的原理是给当前组件的元素添加一个特殊的 data 属性,然后通过属性选择器来限定样式作用域。加了 scoped 后,组件内部的样式就不会污染到外部,外部的样式也影响不到组件内部,模块化效果非常好。
如果你在项目里发现明明加了 scoped,子组件里的样式还是被改了,那通常是因为你用到了子组件的根元素,或者用了第三方组件的内部类。Vue 的 scoped 样式对子组件根元素是生效的,但子组件内部的 DOM 结构样式就不会渗透进去了。这种情况下,要么给子组件也加上和父组件相同的类名,要么用 :deep() 深度选择器来穿透作用域。
<style scoped> .parent :deep(.child-inner) { color: red; } </style>:deep() 在 Vue 3 里就是这么写的,Vue 2 里对应的是 ::v-deep。遇到需要覆盖第三方组件内部样式的情况,这个选择器特别有用。
3.2 vue 插槽机制与组件复用
插槽是 Vue 组件化开发中非常强大的功能。它解决的问题是:组件的外部结构和内部逻辑如何灵活组合。
最简单的情况是匿名插槽。我在做一个卡片组件,卡片的结构是固定的,但卡片内容是不同的。这时候卡片组件可以写成这样。
<template> <div class="card"> <div class="card-header">{{ title }}</div> <div class="card-body"> <slot></slot> </div> </div> </template>在使用卡片组件的地方,组件标签中间的内容就会被填充到 slot 的位置。
<MyCard title="用户信息"> <p>用户名:张三</p> <p>年龄:25</p> </MyCard>有时候一个组件需要多个插槽位置,比如页头、主体、页脚。这时候就需要给插槽命名,也就是具名插槽。
<template> <div class="layout"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div> </template>使用的时候,用 template 标签搭配 v-slot 来指定插槽名称。
<MyLayout> <template v-slot:header> <h1>页面标题</h1> </template> <p>主体内容</p> <template v-slot:footer> <p>版权信息</p> </template> </MyLayout>具名插槽还有一种简写方式,用 # 替代 v-slot。上面的写法可以简写成这样。
<MyLayout> <template #header> <h1>页面标题</h1> </template> <p>主体内容</p> <template #footer> <p>版权信息</p> </template> </MyLayout>还有一种作用域插槽,它的作用是让父组件能够访问子组件的数据。比如表格组件把当前行的数据通过插槽传递出来,让父组件可以决定每行如何渲染。这种场景在做数据表格的时候特别常见,尤其是需要自定义操作列的时候。
3.3 vue 路由与动态路由参数传递
Vue 路由是单页应用的核心。没有路由,就没法实现多页面的跳转体验。路由配置看似简单,但真正写起来有不少细节需要注意。
基础路由配置是定义一个路由数组,然后调用 createRouter 创建路由实例。
import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', component: Home }, { path: '/user/:id', component: UserDetail } ] const router = createRouter({ history: createWebHistory(), routes })这里注意/user/:id这种写法,冒号表示 id 是动态参数。当用户在地址栏输入/user/1001时,路由就能匹配到 UserDetail 组件,并且组件里可以通过route.params.id获取到 1001 这个值。
路由参数传递有两种方式,一种是路径参数,另一种是查询参数。路径参数是上面的写法,查询参数是地址栏里常见的?id=1001这种形式。
<!-- 路径参数 --> <router-link to="/user/1001">用户详情</router-link> <!-- 查询参数 --> <router-link to="/user?id=1001">用户详情</router-link>在组件里分别通过route.params和route.query来获取。
动态路由还有一个高级应用,就是根据用户权限生成路由表。在做后台管理系统的时候,不同角色的用户能访问的页面不一样。管理员能看到用户管理、权限设置,普通员工只能看到自己的工单列表。这时候不能把所有路由都配置在前端,而是等用户登录后,根据后端返回的权限列表,动态添加路由。
router.addRoute({ path: '/admin', component: AdminLayout, children: [ { path: 'users', component: UserManage } ] })addRoute 可以随时动态添加路由记录。这个功能在做权限控制时非常关键,可以避免用户通过修改路由直接越权访问页面。当然,真正的安全控制不能只靠前端路由,后端的接口也要做权限校验,前端路由隐藏只是用户体验层面的优化。
另外提醒一下路由守卫。在跳转前判断用户是否登录、是否有权限,都应该在全局前置守卫里做。
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() } else { if (!token) { next('/login') } else { next() } } })这段逻辑虽然简单,但非常实用。未登录用户访问任何受保护页面,都会自动跳到登录页。
3.4 vue 打包放进 Spring Boot 中的完整流程
项目开发完成后,前端代码最终要部署到服务器上。最常规的做法有两种:一种是前端单独部署到 Nginx,通过反向代理转发请求到后端;另一种是把 Vue 打包后的静态资源放进 Spring Boot 项目中,让后端统一托管。很多前辈更喜欢后一种方式,因为部署时只需要动一个进程,管理起来更省事。
打包 Vue 项目需要先执行构建命令。
npm run build构建完成后,项目根目录下会生成一个 dist 文件夹,里面是打包后的静态文件。接下来要做两件事:一是把 dist 文件夹里的内容复制到 Spring Boot 项目的 src/main/resources/static 目录下,二是确保后端对前端路由的支持。
这里有个关键问题,Vue Router 使用的是 HTML5 History 模式,路径如/user/1001是真实的浏览器地址。当用户直接在浏览器地址栏输入这个地址访问时,后端会去找对应的接口或资源,如果找不到就会返回 404。解决办法是在后端配置一个错误页面跳转,将所有非接口请求都转发到 index.html。
在 Spring Boot 里可以做这样一个简单的控制器,实现路径转发。
@Controller public class ViewController { @RequestMapping(value = {"/", "/user/**", "/admin/**", "/login/**"}) public String forward() { return "forward:/index.html"; } }需要注意的是,接口路径不能被转发到 index.html,否则接口请求会变成 HTML 页面返回,前端拿到一堆字符串无法解析。所以要确保这个控制器只匹配前端路由路径,不要覆盖后端业务接口的路径。
还有一种方案是把 Vue Router 切换成 hash 模式。hash 模式的路径是/#/user/1001,这种形式不会触发后端路由,因为#后面的内容浏览器不会发送给服务器。所以如果不想配置后端转发,直接使用 hash 模式最简单,但地址栏会多一个#,不太美观。在踩过几次坑之后,我的习惯是不推荐用 hash 模式,因为历史记录和分享链接的体验不如 History 模式。体验需要的话,还是配一个后端转发比较好。
打包放进 Spring Boot 的方法还有一个好处,就是前后端同源部署,不再有跨域问题。开发环境要靠 CORS 配置来解决跨域,生产环境部署在一起后,反而更简单。
4. 高价值 Vue 进阶场景与前端面试精华
在 JavaWeb 和 Vue 的结合使用中,有一些进阶场景非常高频,也是面试官重点考察的方向。我把这些内容整理一下,无论是为了做项目还是准备面试,都能用得上。
4.1 vue 中使用 ECharts 绘制统计图
ECharts 是数据可视化领域用得最多的图表库之一。在后台管理系统中,销售统计、用户增长、流量趋势,这些都是 ECharts 的强项。用 Vue 集成 ECharts 并不复杂,不少新手的困惑是不知道数据如何从后端传递到图表中。
先安装依赖。
npm install echarts然后在需要的组件中引入。
<template> <div ref="chartRef" style="width: 600px; height: 400px"></div> </template> <script setup> import { ref, onMounted, onBeforeUnmount } from 'vue' import * as echarts from 'echarts' const chartRef = ref(null) let chartInstance = null onMounted(() => { // 初始化图表实例 chartInstance = echarts.init(chartRef.value) // 模拟从后端获取的数据 const months = ['一月', '二月', '三月', '四月', '五月', '六月'] const salesData = [120, 200, 150, 180, 240, 300] chartInstance.setOption({ title: { text: '月度销售统计' }, tooltip: {}, xAxis: { data: months }, yAxis: {}, series: [ { name: '销售额', type: 'bar', data: salesData } ] }) }) onBeforeUnmount(() => { // 组件销毁前注销实例,避免内存泄漏 if (chartInstance) { chartInstance.dispose() } }) </script>在真实项目里,上面的模拟数据会被替换成 axios 请求后端接口返回的数据。而且我觉得第一次加载完数据之后,可以做一个窗口大小变化的监听,当浏览器窗口变化时,自动调整图表尺寸,这个细节很多教程里都不会告诉,但实际项目中缺了它,图表就会变形。
真正要画两个柱状统计图进行对比的场景里,核心在于 series 数组里放两个数据源,每个数据源都有独立的颜色和 name,然后结合 legend 图例组件展示。这种设计在做同比环比对比时特别常用。
4.2 vue 播放 m3u8 格式视频的免安装方案
m3u8 是 HLS 流媒体协议中的索引文件格式,现在很多视频网站都在用。在 Vue 项目中播放 m3u8 格式的视频,最主流的方案是使用 hls.js 这个库。
先安装依赖。
npm install hls.js然后封装一个播放组件。
<template> <video ref="videoRef" controls autoplay muted></video> </template> <script setup> import { ref, onMounted } from 'vue' import Hls from 'hls.js' const videoRef = ref(null) onMounted(() => { const video = videoRef.value const videoUrl = 'https://example.com/live/stream.m3u8' if (Hls.isSupported()) { const hls = new Hls() hls.loadSource(videoUrl) hls.attachMedia(video) hls.on(Hls.Events.MANIFEST_PARSED, () => { video.play() }) } else if (video.canPlayType('application/vnd.apple.mpegurl')) { // 原生支持 HLS 的浏览器,如 Safari video.src = videoUrl } }) </script>这个方案不需要安装任何浏览器插件,只要浏览器支持 HTML5 video 标签,就能播放 m3u8 格式的视频流。在直播、点播、课程平台、监控回放等场景里非常常见。
hls.js 的优势是兼容性极好,各主流浏览器都能用。在处理视频流的过程中,它内部会自动处理网络波动、分段切换等逻辑。不过有一点要注意,播放的 m3u8 地址必须和后端接口在同一域名下,或者后端配置了正确的跨域响应头,否则视频请求会被浏览器拦截。
4.3 vue 低代码平台与拖拽组件生成页面
低代码平台这几年热度很高。它的核心玩法是拖拽组件到画布上,自动生成前端页面代码。在 Vue 技术栈里,实现拖拽生成页面主要依靠 HTML5 的拖放 API 或者第三方拖拽库。
我推荐使用 vuedraggable 这个库,它基于 Sortable.js,用起来比较顺手。
npm install vuedraggable基本用法是封装一个可拖拽的组件列表,和一个可放置的页面画布。
<template> <div class="editor-container"> <div class="component-palette"> <draggable v-model="componentList" :group="{ name: 'components' }"> <div v-for="item in componentList" :key="item.type" class="component-item"> {{ item.label }} </div> </draggable> </div> <div class="canvas"> <draggable v-model="pageComponents" :group="{ name: 'components' }"> <div v-for="item in pageComponents" :key="item.id" class="canvas-item"> <component :is="item.type" v-bind="item.props"></component> </div> </draggable> </div> </div> </template>整体思路是维护两个数组,一个存放可选组件库,另一个存放画布上已放置的组件。通过 group 属性让两个区域共享同一个拖拽组,这样组件就能从组件库拖到画布上。被拖入画布后,根据组件的 type 动态渲染对应的 Vue 组件,用户还可以在属性面板中修改组件的 props,实时预览效果。
拖拽组件的原理理解清楚后,再进阶就是为每个组件保存位置信息 x、y、宽高。这样最终可以生成一段 JSON 配置,再用自定义渲染器把 JSON 还原成页面。这种设计思路,已经接近很多低代码平台的核心原理了,自己做一个小型低代码引擎也不是不可能。
4.4 vue 前端面试高频题与答题思路
结合做 JavaWeb 项目时的实际场景,我整理几个面试中出现频率较高的 Vue 题目和自己总结的答题思路。
第一个题是 Vue 的生命周期。这个属于必考题。我的回答思路是把创建阶段、挂载阶段、更新阶段、销毁阶段各方法的作用讲清楚,然后结合一个实际场景说明。比如在 onMounted 里去请求列表数据,在 onBeforeUnmount 里清理定时器或注销事件监听。这样比干巴巴背概念更有说服力。
第二个题是 computed 和 watch 的区别。我的回答思路是 computed 依赖响应式数据,数据没变不会重新计算,适合做派生状态,比如购物车总价。watch 是监听数据变化后执行副作用操作,适合做搜索关键词防抖。两者的本质区别是一个是声明式派生,一个是命令式监听。
第三个题是 v-if 和 v-show 的区别。v-if 是真正的条件渲染,满足条件才创建 DOM,不满足就销毁,适合初始化渲染频率低的场景。v-show 是用 CSS 的 display 属性控制显隐,元素始终在 DOM 中,适合需要频繁切换的场景。如果在后台管理系统做菜单权限控制,v-if 更合适;做 Tab 页签切换,v-show 更合适。
第四个题是组件通信方式。父传子用 props,子传父用 emit 事件。跨层级通信可以用 provide/inject,也可以用 Pinia 这种状态管理库。兄弟组件之间的通信一般通过公共父组件或者 Pinia 来完成。答题时尽量结合自己做过的模块来描述,比如做了购物车,那购物车数据共享就是一个很好的例子。
第五个题是 Vue 3 和 Vue 2 的核心区别。组合式 API 和选项式 API 是最大的区别,Vue 3 的 Composition API 能把同一逻辑的代码聚在一起。其次是响应式原理从 Object.defineProperty 换成了 Proxy,这让 Vue 3 能监听新增属性和删除属性,这是 Vue 2 做不到的。还有 Fragment、Teleport、Suspense 这些新特性,都是加分项。
当然,面试不只是背题,更重要的是展示你的项目经验和思考能力。如果你能讲清楚为什么这样做、遇到问题怎么排查,那比单纯回答知识点更能加分。
5. JavaWeb 完整项目实战复盘与后续扩展
前面把理论讲了不少,这一部分我结合一个仿天猫购物系统的用户管理模块来实战复盘。这是我比较推荐的学习路径,不要上来就做一个完整的电商系统,那样容易陷入代码量过大、无从下手的困境。先做一个模块,把 JavaWeb 和 Vue 的协作链路完全跑通,再逐步扩展。
5.1 项目整体设计与数据库建模
这个模块的功能包含注册、登录、个人信息查看与修改、用户列表管理。虽然是简化版,但麻雀虽小五脏俱全,涉及了后端接口设计、数据库表设计、前端页面编写、权限控制等多个核心环节。
数据库表设计是整个项目的根基。我会把用户表设计成下面这个样子。
CREATE TABLE `t_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(加密存储)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `status` tinyint(4) DEFAULT '1' COMMENT '状态:1正常,0禁用', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;密码一定不能明文存储,我用的是 BCrypt 加密。Spring Security 里有现成的 BCryptPasswordEncoder,直接拿过来用就行。即使是自己实现注册逻辑,也建议引入这个工具类,而不是搞简单的 MD5 加盐。BCrypt 会自动加盐,而且每次加密结果不同,安全性会高很多。
项目结构上按照 Spring Boot 的标准分包方式,controller、service、mapper、entity 四层分离。每一层只做好自己那一层的事:controller 接收前端请求并校验基础参数,service 处理业务逻辑与事务,mapper 负责数据库交互,entity 对应数据表字段。
5.2 后端接口开发与接口规范落地
注册接口的核心逻辑是检查用户名是否重复,然后对密码加密,最后写入数据库。这里有个细节需要注意,判断用户名是否重复时要考虑并发场景。两个用户同时提交同一个用户名,可能都通过了检查,然后其中一条插入就会因为数据库唯一索引冲突而报错。我会在 service 层捕获这个异常,转成友好的业务提示信息,而不是直接让该死的 SQL 异常抛给前端。
登录接口稍有不同。前端提交用户名和密码后,后端先检查用户名是否存在,然后用 BCryptPasswordEncoder 的 matches 方法比对密码。登录成功后,向后端返回一个 token。这个 token 可以用 JWT 来生成,包含用户 ID、用户名和过期时间。前端拿到 token 后存储在 localStorage 中,后续每次请求都带上,后端通过拦截器校验 token 合法性。
用户列表接口的设计也值得说一说。真实场景里列表请求一般都会带分页参数和搜索关键词。后端接口定义成下面这样。
@GetMapping("/admin/users") public Result getUsers( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String keyword ) { Page<User> userPage = userService.pageQuery(page, size, keyword); return Result.success(userPage); }page 代表第几页,默认是 1。size 代表每页多少条,默认是 10。keyword 是搜索关键词,可以为空。service 层配合 MyBatis-Plus 的分页插件,可以把分页逻辑写得很简洁。
接口开发过程中,统一返回结构要坚持使用。我会建立一个 Result 类,所有 controller 方法都返回它,而不是直接返回一个 User 对象或者 Page 对象。这样前端在处理响应时,永远只用关心一个固定的数据格式,省去了大量判断逻辑。
5.3 前端页面实现与联调技巧
前端这边我做的页面有注册页、登录页、个人中心页、管理员用户列表页。用 Vue CLI 创建项目后,引入 Element Plus 作为 UI 组件库,页面写起来会快很多。
先安装 Element Plus。
npm install element-plus然后全局引入。
import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.use(ElementPlus) app.mount('#app')登录页的表单校验是前端项目中一个细节较多的部分。用户名必填、密码长度不少于 6 位、邮箱格式要合法。Element Plus 的表单校验规则可以很灵活地配置,把这些规则集中管理起来,比在方法里逐个判断要整洁得多。
axios 请求封装也是必做的一件事。我会创建 request.js,统一配置 baseURL、超时时间和请求拦截器。
import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) 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) { // 统一处理业务错误 return Promise.reject(new Error(res.message)) } return res }, error => { // 统一处理 HTTP 错误,如 401 跳转登录页 if (error.response.status === 401) { router.push('/login') } return Promise.reject(error) } )前端联调时我会强烈建议先通过 jsonmock 或本地 mock 数据把页面渲染跑通,再切换成真实接口。这样能把前端问题和后端问题解耦,联调时一旦出错,定位会更快。
jsonmock 这个工具在联调阶段非常好用。它的原理很简单,你可以配置一个假的接口地址,返回预设的 JSON 数据。前端先用假数据渲染列表、处理分页,后面联调时只需要把 baseURL 从 mock 地址切回真实后端地址就行了。
具体步骤是用 jsonmock 提供的在线服务,配置一个模拟接口,比如https://www.fastmock.site/mock/xxx/user/list,返回的数据结构设计成和真实接口完全一样。前端代码里只需要在 request.js 中修改环境变量,就能实现 mock 和真实数据的无缝切换。这个习惯能彻底打破前后端开发的同步依赖。
5.4 项目扩展方向与个人实操心得
这个模块完整跑通后,向完整项目扩展的方向就比较明确了。比如添加商品模块、订单模块、购物车模块。做这些扩展的时候,你会发现 Vue 组件复用价值大幅提升,一个商品卡片组件可以在列表页、详情页、搜索页重复使用。Java 后端的分层结构也让添加新功能变得很顺畅,新增 controller、service、mapper 三件套就完成一个业务维度。
我还想分享个人实操中的体会。第一次独立做 JavaWeb 和 Vue 结合的项目时,我犯过不少错,其中最大的教训就是没有先设计接口文档,直接开写代码,后端字段名一改再改,前端被迫跟着同步修改,浪费了大量时间。后来我在项目开始前先用表格把接口路径、请求方式、请求参数、响应字段全部列清楚,开发效率立刻提高了一个档次。
从那以后,即使是自己一个人做的小项目,我也会保持先列接口再写代码的习惯。这样看似多花了一点规划时间,但后面开发、联调、测试的速度会快很多。
环境问题我也有一个比较实用的心得。开发时把 MySQL 数据库、Redis、后端服务和前端开发服务器都保持运行状态,不要把数据库动不动就关掉。很多人一看报错就怀疑代码,结果排查半天发现是数据库没启动。我的做法是环境检查永远排在代码排查之前,端口通不通、服务启没启、数据库连不连得上,这三个问题确认没有问题再去看日志报错。
如果你也想做类似的项目,我的建议是不要贪大求全。先拿一个模块练手,把 JavaWeb 和 Vue 的生态链路彻底走通,再逐步扩展。走通一遍之后,你就会感觉整套技术栈就像组装积木一样,每个模块都能按需拼接,项目越做越顺。