目實(shí)戰(zhàn):前后端分離開發(fā)與部署指南)
1. 項(xiàng)目概述1.1 核心需求解析做全棧開發(fā)這幾年我見過(guò)太多半路出家的開發(fā)者在 Spring Boot 和 Vue 的邊界線上反復(fù)橫跳——后端能寫接口但前端拿著 JSON 不知道往哪兒塞前端能把頁(yè)面畫出來(lái)但遇到跨域、鑒權(quán)、打包部署就抓瞎。這個(gè)實(shí)戰(zhàn)指南解決的就是這個(gè)問(wèn)題把一條完整的前后端分離開發(fā)鏈路串起來(lái)從零開始搭建一個(gè)可直接運(yùn)行的全棧項(xiàng)目。注意這里的關(guān)鍵詞是“全?!辈皇恰扒昂蠖烁鲗懸话搿薄m?xiàng)目會(huì)包含一個(gè)完整的業(yè)務(wù)閉環(huán)前端 Vue 負(fù)責(zé)頁(yè)面交互和數(shù)據(jù)展示后端 Spring Boot 負(fù)責(zé)接口服務(wù)和數(shù)據(jù)持久化中間通過(guò) RESTful API 通信最終打包部署到同一臺(tái)服務(wù)器上跑通整個(gè)流程。這個(gè)過(guò)程中涉及的技術(shù)點(diǎn)非常密集——Vue 的生命周期、Axios 的請(qǐng)求攔截、Spring Boot 的分層架構(gòu)、MyBatis 的數(shù)據(jù)映射、JWT 的登錄態(tài)管理、Nginx 的反向代理——任何一個(gè)環(huán)節(jié)掉鏈子整個(gè)項(xiàng)目就跑不起來(lái)。這個(gè)項(xiàng)目的受眾其實(shí)很廣。剛?cè)胄械某跫?jí)開發(fā)者可以通過(guò)它建立全棧思維知道前端發(fā)起的請(qǐng)求在后端到底經(jīng)歷了什么寫了好幾年接口的后端開發(fā)可以補(bǔ)上前端技能獨(dú)立負(fù)責(zé)完整模塊做畢設(shè)或小團(tuán)隊(duì)項(xiàng)目的同學(xué)可以直接拿這套骨架改業(yè)務(wù)邏輯就能上線。我的建議是無(wú)論你偏向前端還是后端都完整地把項(xiàng)目過(guò)一遍收獲最大的恰恰是你知識(shí)短板那部分的內(nèi)容。1.2 技術(shù)棧全景與選型理由Spring Boot 3.x 搭配 Vue 3.x 是當(dāng)前主流選擇我用的版本組合是 Spring Boot 3.2.5 Vue 3.4.21 Vite 5.2.0。這套組合的好處是生態(tài)成熟遇到問(wèn)題搜一下基本都是現(xiàn)成的答案。Spring Boot 3.x 相比 2.x 最大的變化是底層基于 Spring Framework 6支持 Jakarta EE 規(guī)范默認(rèn)使用 Java 17 及以上版本開發(fā)體驗(yàn)和性能都有明顯提升。Vue 3 的組合式 API 寫起來(lái)比 Options API 更符合直覺配合 Vite 的開發(fā)服務(wù)器熱更新速度能縮短到一兩秒。數(shù)據(jù)庫(kù)我用 MySQL 8.0持久層框架選了 MyBatis Plus 而不是純 MyBatis原因很簡(jiǎn)單——單表操作完全不需要手寫 SQL自帶的方法夠用復(fù)雜查詢?cè)倏辞闆r寫 XML。鑒權(quán)方案采用 JWT Spring Security但做了簡(jiǎn)化處理只用了 Spring Security 的加密工具類沒有引入完整的安全過(guò)濾鏈。很多全棧項(xiàng)目的通病是過(guò)度設(shè)計(jì)明明是個(gè)小項(xiàng)目卻把微服務(wù)、分布式那套全上了結(jié)果光搭建環(huán)境就花了大半時(shí)間。這次的實(shí)戰(zhàn)特意保持克制讓你能專注于理解核心鏈路。提示如果你的環(huán)境是 JDK 8暫時(shí)不想升級(jí)建議使用 Spring Boot 2.7.x 配合 Vue 2但同樣建議盡早升級(jí)。JDK 17 的垃圾回收性能和容器支持都比 8 好很多IntelliJ IDEA 2023 版本對(duì) JDK 17 的支持也更順手。2. 整體設(shè)計(jì)與架構(gòu)拆解2.1 前后端分離架構(gòu)的邊界劃分全棧項(xiàng)目最容易犯的錯(cuò)誤是職責(zé)混亂。前端代碼里塞后端邏輯后端代碼里混頁(yè)面模板時(shí)間一長(zhǎng)代碼就成漿糊了。這次項(xiàng)目采用嚴(yán)格的前后端分離模式前端是純靜態(tài)資源通過(guò) Axios 異步調(diào)用后端接口后端只負(fù)責(zé)提供 JSON 數(shù)據(jù)不做頁(yè)面渲染。簡(jiǎn)單來(lái)說(shuō)你可以把整套系統(tǒng)想象成一家餐廳前端是服務(wù)員負(fù)責(zé)記錄顧客需求、展示菜品渲染數(shù)據(jù)后端是后廚負(fù)責(zé)準(zhǔn)備菜品處理業(yè)務(wù)數(shù)據(jù)庫(kù)就是儲(chǔ)藏室。服務(wù)員不需要知道菜怎么做后廚也不需要直接和顧客溝通兩者之間的點(diǎn)菜單就是 API 接口。拿一個(gè)具體的業(yè)務(wù)場(chǎng)景來(lái)說(shuō)用戶在前端頁(yè)面上登錄填完表單點(diǎn)擊提交按鈕Vue 組件把表單數(shù)據(jù)通過(guò) POST 請(qǐng)求發(fā)送到后端的/api/login接口后端 Controller 接收請(qǐng)求調(diào) Service 層校驗(yàn)用戶名密碼再調(diào) Mapper 層查數(shù)據(jù)庫(kù)校驗(yàn)通過(guò)后生成一個(gè) JWT token 返回給前端。前端收到 token 后存在 localStorage 里之后每次請(qǐng)求都在請(qǐng)求頭帶上這個(gè) token后端用攔截器驗(yàn)證 token 是否有效。整個(gè)鏈路中前端和后端各自獨(dú)立開發(fā)、獨(dú)立測(cè)試、獨(dú)立部署契約就是接口文檔。2.2 后端分層設(shè)計(jì)的職責(zé)劃分后端項(xiàng)目的包結(jié)構(gòu)按照經(jīng)典的三層架構(gòu)來(lái)組織Controller 層負(fù)責(zé)接收請(qǐng)求和參數(shù)校驗(yàn)Service 層負(fù)責(zé)業(yè)務(wù)邏輯Mapper 層負(fù)責(zé)數(shù)據(jù)庫(kù)操作。在這個(gè)之上再加一個(gè) Entity 包放實(shí)體類一個(gè) DTO 包放數(shù)據(jù)傳輸對(duì)象一個(gè) Config 包放配置類一個(gè) Common 包放統(tǒng)一返回結(jié)果和異常處理。每個(gè)層之間通過(guò)接口和類來(lái)協(xié)作。Controller 不直接操作數(shù)據(jù)庫(kù)Service 不寫 SQLMapper 只做數(shù)據(jù)映射。剛?cè)腴T的開發(fā)者容易把業(yè)務(wù)代碼全堆在 Controller 里一個(gè)方法寫上百行看起來(lái)很“高效”實(shí)際上后面需求一變更就得重寫。我曾經(jīng)接手過(guò)一個(gè)項(xiàng)目登錄接口里直接拼 SQL 查數(shù)據(jù)庫(kù)后來(lái)要加驗(yàn)證碼功能那叫一個(gè)費(fèi)勁。打個(gè)比方三層架構(gòu)就像公司的三級(jí)審批——Controller 是前臺(tái)接待負(fù)責(zé)接需求和判斷訴求是否合理Service 是部門主管負(fù)責(zé)制定處理方案并協(xié)調(diào)資源Mapper 是具體執(zhí)行人負(fù)責(zé)落實(shí)細(xì)節(jié)。每一層只做自己該做的事出問(wèn)題時(shí)能快速定位是哪個(gè)環(huán)節(jié)的鍋。2.3 前端組件化設(shè)計(jì)的模塊劃分前端項(xiàng)目按功能模塊劃分目錄結(jié)構(gòu)api目錄放接口請(qǐng)求封裝router目錄放路由配置stores目錄放 Pinia 狀態(tài)管理views目錄放頁(yè)面組件components目錄放公共組件。組件化設(shè)計(jì)的關(guān)鍵是找到合適的拆分粒度。舉個(gè)具體的例子后臺(tái)管理系統(tǒng)里常見的“用戶管理”頁(yè)面可以拆成搜索欄、表格、彈窗表單三個(gè)部分。搜索欄接收用戶輸入的查詢條件通過(guò)事件把條件拋給父組件父組件拿到條件后更新列表數(shù)據(jù)表格展示數(shù)據(jù)并監(jiān)聽行點(diǎn)擊彈窗表單負(fù)責(zé)新增和編輯。組件拆分有個(gè)度阿里推出的前端規(guī)范里提到一個(gè)原則如果一個(gè)組件太長(zhǎng)超過(guò)合理范圍還沒法拆分出可復(fù)用的子組件可能就是職能不單一。反過(guò)來(lái)把組件拆得太碎也有問(wèn)題——有一次我把一個(gè)簡(jiǎn)單的列表頁(yè)拆成了十多個(gè)組件每個(gè)組件只有幾十行代碼結(jié)果傳參傳得頭暈調(diào)試一個(gè) bug 得在文件間跳來(lái)跳去。一般的原則是只在組件被兩個(gè)以上地方復(fù)用或者單個(gè)組件的復(fù)雜度已經(jīng)明顯影響可讀性時(shí)才需要拆分。3. 環(huán)境準(zhǔn)備與項(xiàng)目初始化3.1 開發(fā)環(huán)境的版本選型與安裝清單先過(guò)一遍環(huán)境清單。后端需要 JDK 17、Maven 3.9、MySQL 8.0前端需要 Node.js 18我用的 20.12.0、npm或 pnpm/yarn這里用 npm、Vite 作為構(gòu)建工具。IDE 方面我推薦后端用 IntelliJ IDEA前端用 VS Code兩個(gè)都裝也沒問(wèn)題。幾個(gè)需要注意的坑先提前說(shuō)JDK 版本必須和環(huán)境變量匹配。裝了多個(gè) JDK 的機(jī)器上IDEA 里配置的 Project SDK 和系統(tǒng)環(huán)境變量最好統(tǒng)一否則 Maven 編譯時(shí)可能用錯(cuò)版本。Node.js 盡量用偶數(shù)版本。Node 官方對(duì)奇數(shù)次版本的支持時(shí)間較短有些依賴可能不兼容。當(dāng)年 Node 15 時(shí)代裝 Vite 就是各種問(wèn)題后來(lái)學(xué)聰明了直接固定用最新 LTS 版本。MySQL 8.0 的認(rèn)證插件。默認(rèn)的caching_sha2_password在舊版連接驅(qū)動(dòng)下會(huì)報(bào)錯(cuò)Spring Boot 3.x 自帶的 MySQL Connector/J 8.x 沒問(wèn)題但如果你的其他老項(xiàng)目還在用 5.x 驅(qū)動(dòng)連接 MySQL 8 會(huì)失敗。安裝順序沒有硬性要求但建議先裝后端環(huán)境再裝前端環(huán)境。因?yàn)楹蠖伺芷饋?lái)之后你可以先用 Postman 測(cè)試接口確認(rèn)沒問(wèn)題再寫前端頁(yè)面這樣排查問(wèn)題時(shí)范圍更清晰。3.2 后端項(xiàng)目初始化三步走先用 IntelliJ IDEA 創(chuàng)建 Spring Boot 項(xiàng)目。如果你用的是 IDEA 2023 版本可以直接在 New Project 里選 Spring InitializrGroup 填com.exampleArtifact 填fullstackJava 版本選 17依賴勾選 Spring Web、Spring Security可選也可以后面手動(dòng)加、MySQL Driver、Lombok、Validation。需要補(bǔ)充說(shuō)明的是Spring Initializr 生成的項(xiàng)目默認(rèn)帶一個(gè)mvnwMaven Wrapper它允許你在沒有全局安裝 Maven 的機(jī)器上直接執(zhí)行./mvnw命令來(lái)構(gòu)建項(xiàng)目這對(duì)保證構(gòu)建環(huán)境一致性很有幫助。如果你的機(jī)器沒有 Maven可以用它來(lái)代替mvn命令。項(xiàng)目生成后先別急著寫代碼把目錄補(bǔ)齊。基礎(chǔ)結(jié)構(gòu)大概是src/main/java/com/example/fullstack ├── controller ├── service │ ├── impl ├── mapper ├── entity ├── dto ├── config └── common然后在pom.xml里加入 MyBatis Plus 的依賴。有個(gè)地方容易踩坑MyBatis Plus 在 Spring Boot 3.x 下需要引入mybatis-plus-spring-boot3-starter而不是mybatis-plus-boot-starter那是給 Spring Boot 2.x 用的。如果你用錯(cuò)版本啟動(dòng)時(shí)大概率會(huì)報(bào)Invalid value type for attribute factoryBeanObjectType之類的錯(cuò)誤。配置文件application.yml按以下內(nèi)容配置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這里有個(gè)細(xì)節(jié)Jackson 的日期格式設(shè)置很重要。如果前端傳來(lái)的日期字符串是2024-05-20 10:30:00格式后端 LocalDateTime 字段不加JsonFormat注解或全局配置默認(rèn)是 ISO 格式反序列化時(shí)就會(huì)報(bào)錯(cuò)。全局配好省得每個(gè)字段都寫注解。3.3 前端項(xiàng)目初始化與依賴配置前端我用 Vite 創(chuàng)建項(xiàng)目命令如下npm create vitelatest frontend -- --template vue cd frontend npm install然后安裝項(xiàng)目需要的依賴npm install vue-router4 pinia axios element-plus選 Element Plus 做 UI 組件庫(kù)是國(guó)內(nèi)全棧項(xiàng)目的常見選擇它對(duì)中文場(chǎng)景支持好表格、表單、分頁(yè)這些后臺(tái)管理常用的組件都有現(xiàn)成的。同時(shí)安裝它的圖標(biāo)庫(kù)element-plus/icons-vue可以在全局注冊(cè)方便在菜單和按鈕里直接用圖標(biāo)。依賴裝完之后先配置 Vite 的代理和構(gòu)建選項(xiàng)。開發(fā)階段前端和后端分離運(yùn)行需要把請(qǐng)求代理到后端服務(wù)生產(chǎn)階段前端構(gòu)建后的靜態(tài)文件由 Nginx 托管也需要配置轉(zhuǎn)發(fā)規(guī)則。在開發(fā)環(huán)境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 } } } })這樣前端代碼里請(qǐng)求/api/login時(shí)Vite 會(huì)把請(qǐng)求轉(zhuǎn)發(fā)到http://localhost:8080/api/login繞過(guò)了開發(fā)環(huán)境下的跨域限制。生產(chǎn)環(huán)境則交給 Nginx 配置 location 塊做反向代理。提示配置代理解決的是開發(fā)階段的跨域問(wèn)題。如果你的前后端部署在同一臺(tái)服務(wù)器、同一個(gè)域名下生產(chǎn)環(huán)境一般不需要跨域處理但如果部署在不同域名或端口下則要在后端加 CORS 配置。這個(gè)細(xì)節(jié)等到部署章節(jié)詳細(xì)講。4. 核心功能開發(fā)實(shí)操4.1 用戶認(rèn)證模塊的完整實(shí)現(xiàn)方案用戶認(rèn)證是幾乎所有全棧系統(tǒng)的剛性需求。我用 JWT 實(shí)現(xiàn)了這套認(rèn)證流程核心思路是用戶登錄成功后后端簽發(fā)一個(gè)有效期為一定時(shí)間的 token前端存儲(chǔ)這個(gè) token在每次請(qǐng)求的 Authorization 請(qǐng)求頭里帶上來(lái)后端通過(guò)攔截器統(tǒng)一校驗(yàn)。先說(shuō)數(shù)據(jù)表結(jié)構(gòu)用戶表設(shè)計(jì)為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 狀態(tài)1啟用 0禁用, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;密碼存儲(chǔ)用的是 BCrypt 加密算法這是 Spring Security 里自帶的工具類不需要額外引依賴// 加密 String encodedPassword BCryptPasswordEncoder().encode(rawPassword); // 校驗(yàn) boolean matches encoder.matches(rawPassword, encodedPassword);這里有一個(gè)很容易被忽略的安全細(xì)節(jié)matches方法第一個(gè)參數(shù)是明文密碼第二個(gè)是數(shù)據(jù)庫(kù)里取出來(lái)的密文。我第一次寫的時(shí)候參數(shù)順序搞反了調(diào)試了半小時(shí)才發(fā)現(xiàn)是參數(shù)順序問(wèn)題。另外注意 BCrypt 的加密結(jié)果是隨機(jī)的同一個(gè)明文每次加密出來(lái)的密文都不一樣所以不能用常規(guī)字符串比較必須用matches方法校驗(yàn)。登錄接口的邏輯不復(fù)雜偽代碼是這樣的Controller 接收LoginRequest用戶名密碼Service 層根據(jù)用戶名查用戶如果用戶不存在直接拋異常提示“用戶名或密碼錯(cuò)誤”注意這里的提示不能太具體否則會(huì)暴露用戶是否存在存在則用 BCrypt 校驗(yàn)密碼失敗同樣提示“用戶名或密碼錯(cuò)誤”成功后用用戶 ID、用戶名和當(dāng)前時(shí)間戳生成 JWT返回 token 用戶信息。4.2 JWT 攔截器的實(shí)現(xiàn)與請(qǐng)求鏈路JWT 攔截器是一個(gè)關(guān)鍵的“橫切關(guān)注點(diǎn)”。我用 Spring 的HandlerInterceptor實(shí)現(xiàn)配合 WebMvcConfigurer 注冊(cè)讓所有/api/**下的接口都經(jīng)過(guò)校驗(yàn)登錄接口本身要放行。核心邏輯如下Component public class JwtInterceptor implements HandlerInterceptor { Autowired private JwtUtils jwtUtils; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行預(yù)檢請(qǐng)求 if (request.getMethod().equals(OPTIONS)) { return true; } // 前端約定 token 放在請(qǐng)求頭的 Authorization 字段中 String token request.getHeader(Authorization); if (token null || token.isEmpty()) { return this.unauthorized(response, 未登錄或登錄已過(guò)期); } // 校驗(yàn) token 合法性 try { String userId jwtUtils.getUserIdFromToken(token); request.setAttribute(userId, userId); return true; } catch (Exception e) { return this.unauthorized(response, token 無(wú)效或已過(guò)期); } } 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; } }注意一個(gè)細(xì)節(jié)預(yù)檢請(qǐng)求OPTIONS一定要放行。瀏覽器在跨域 POST 請(qǐng)求前會(huì)自動(dòng)發(fā)送一個(gè) OPTIONS 預(yù)檢請(qǐng)求如果你的攔截器把它攔下來(lái)了前端會(huì)報(bào) CORS 錯(cuò)誤而且這個(gè)錯(cuò)誤在瀏覽器控制臺(tái)看起來(lái)像是后端 CORS 配置有問(wèn)題實(shí)際上卻是攔截器沒放行 OPTIONS。攔截器只是一個(gè)粗粒度的路由守衛(wèi)對(duì)于不同接口的細(xì)粒度權(quán)限比如只有管理員能調(diào)用的接口可以在 Controller 方法上加自定義注解再在攔截器里讀取注解做二次判斷這個(gè)屬于權(quán)限系統(tǒng)的進(jìn)階內(nèi)容本次項(xiàng)目不做展開。4.3 用戶管理模塊的 CRUD 與分頁(yè)查詢用戶管理模塊雖然簡(jiǎn)單但把前后端交互的典型模式都覆蓋了查詢列表、新增、編輯、刪除。我用 MyBatis Plus 的IService和BaseMapper框架代碼量比傳統(tǒng) 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); // 轉(zhuǎn) VO 返回不直接暴露實(shí)體 return Result.success(PageUtil.convert(userPage, UserVO.class)); } // 新增、編輯、刪除邏輯省略遵循同樣的模式 }細(xì)節(jié)問(wèn)題來(lái)了新增用戶密碼需要加密存儲(chǔ)編輯用戶如果密碼為空說(shuō)明不修改密碼。這些分情況討論的業(yè)務(wù)邏輯都放在 Service 層。Controller 里寫太多 if-else后面維護(hù)起來(lái)就想罵人。前端對(duì)應(yīng)這個(gè)接口的調(diào)用也是固定套路封裝在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 請(qǐng)求封裝的幾個(gè)關(guān)鍵決策點(diǎn)Axios 封裝的好壞直接決定整個(gè)前端項(xiàng)目請(qǐng)求代碼的整潔程度。我的做法是單獨(dú)建一個(gè)utils/request.js文件生成一個(gè)配置了基礎(chǔ) URL、超時(shí)時(shí)間和攔截器的 Axios 實(shí)例。核心代碼import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) // 請(qǐng)求攔截器自動(dòng)附加 token request.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }, error Promise.reject(error) ) // 響應(yīng)攔截器統(tǒng)一處理錯(cuò)誤碼 request.interceptors.response.use( response { const res response.data // 如果后端返回了業(yè)務(wù)錯(cuò)誤碼 if (res.code ! 200) { ElMessage.error(res.message || 請(qǐng)求失敗) if (res.code 401) { // token 過(guò)期跳轉(zhuǎn)登錄頁(yè) localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(error.message || 網(wǎng)絡(luò)錯(cuò)誤) return Promise.reject(error) } ) export default request幾個(gè)決策點(diǎn)說(shuō)一下。baseURL 設(shè)為/api配合 Vite 代理或 Nginx 反向代理前端代碼里就不用寫死后端地址環(huán)境切換時(shí)只需要改代理配置就行。響應(yīng)攔截器里把后端的code/message/data統(tǒng)一解開前端調(diào)用接口時(shí)直接拿到業(yè)務(wù)數(shù)據(jù)不需要每個(gè)調(diào)用處都寫.then(res res.data)。還有一個(gè)細(xì)節(jié)上的選擇token 存 localStorage 還是 sessionStorage。有人建議存 sessionStorage因?yàn)殛P(guān)閉瀏覽器就自動(dòng)清掉安全一些但用戶希望刷新頁(yè)面后保持登錄狀態(tài)所以我用 localStorage。另外邏輯上要區(qū)分業(yè)務(wù)錯(cuò)誤碼比如 500和 HTTP 狀態(tài)碼比如 404上述封裝里對(duì)這兩類錯(cuò)誤都做了處理。5. 前端關(guān)鍵頁(yè)面與交互實(shí)現(xiàn)5.1 登錄頁(yè)的實(shí)現(xiàn)與表單校驗(yàn)登錄頁(yè)是系統(tǒng)的“門面”雖然邏輯簡(jiǎn)單但涉及的交互細(xì)節(jié)并不少。我用 Element Plus 的el-form組件實(shí)現(xiàn)配合它的表單校驗(yàn)規(guī)則用戶名必填格式校驗(yàn)長(zhǎng)度 3-20 個(gè)字符。密碼必填長(zhǎng)度 6-32 個(gè)字符。點(diǎn)擊登錄時(shí)先校驗(yàn)表單通過(guò)后才發(fā)請(qǐng)求。登錄成功后跳轉(zhuǎn)到首頁(yè)并提示“歡迎回來(lái)xxx”。登錄頁(yè)有一個(gè)容易被忽視的用戶體驗(yàn)問(wèn)題請(qǐng)求失敗時(shí)表單要保留用戶輸入不能整個(gè)頁(yè)面刷新重置。Vue 的單頁(yè)應(yīng)用優(yōu)勢(shì)在這里體現(xiàn)得比較明顯。另外登錄按鈕在請(qǐng)求期間要處于 loading 狀態(tài)防止用戶重復(fù)提交。這些都是小細(xì)節(jié)但拿捏好才是專業(yè)的表現(xiàn)。關(guān)于“記住密碼”功能嚴(yán)謹(jǐn)?shù)拈_發(fā)不會(huì)把密碼存在前端通常只記住用戶名。這個(gè)項(xiàng)目我沒有做這個(gè)功能因?yàn)樯婕懊舾行畔⒉蛔霰茸龈踩?.2 布局與路由守衛(wèi)后臺(tái)管理系統(tǒng)的典型布局是左側(cè)菜單欄 右側(cè)內(nèi)容區(qū)。我用 Element Plus 的el-container組件實(shí)現(xiàn)菜單數(shù)據(jù)直接寫死在路由配置里通過(guò)v-for循環(huán)渲染。路由配置的要點(diǎn)是嵌套路由const routes [ { path: /login, component: Login }, { path: /, component: Layout, redirect: /dashboard, children: [ { path: dashboard, name: Dashboard, component: Dashboard, meta: { title: 首頁(yè), icon: HomeFilled } }, { path: user, name: User, component: UserManagement, meta: { title: 用戶管理, icon: User } } ] } ]路由守衛(wèi)是前端鑒權(quán)的另一道防線。在router.beforeEach里檢查是否登錄未登錄則重定向到登錄頁(yè)router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() } else { if (!token) { next({ path: /login }) } else { next() } } })提醒一句前端路由守衛(wèi)只是提升用戶體驗(yàn)的手段不是安全措施。真正校驗(yàn) token 的有效性還得靠后端的攔截器因?yàn)橛脩艨梢灾苯痈那岸舜a繞過(guò)路由守衛(wèi)。一切前端校驗(yàn)都是“防君子不防小人”。5.3 表格、分頁(yè)與表單的聯(lián)動(dòng)模式用戶管理頁(yè)面的核心交互是搜索欄輸入條件 → 點(diǎn)擊查詢 → 表格展示數(shù)據(jù) → 點(diǎn)擊編輯彈出表單 → 保存后刷新列表。這是后臺(tái)管理系統(tǒng)最高頻的交互模式。頁(yè)面數(shù)據(jù)結(jié)構(gòu)大概是searchForm搜索條件的響應(yīng)式數(shù)據(jù)tableData當(dāng)前頁(yè)的數(shù)據(jù)列表pageInfo頁(yè)碼、每頁(yè)條數(shù)、總條數(shù)dialogVisible彈窗顯隱formModel表單數(shù)據(jù)formRules表單校驗(yàn)規(guī)則交互邏輯是點(diǎn)擊搜索時(shí)把頁(yè)碼重置為 1然后調(diào)用列表接口把返回的數(shù)據(jù)和總條數(shù)賦值給響應(yīng)式變量。前端代碼里分頁(yè)組件和表格的狀態(tài)同步是需要注意的地方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 里事件名已經(jīng)改成了change舊版是current-change和size-change兩個(gè)事件分別處理。如果團(tuán)隊(duì)里有老手用新版時(shí)還寫舊事件表格點(diǎn)擊分頁(yè)沒反應(yīng)排查老半天最后發(fā)現(xiàn)是事件名不對(duì)。彈出表單的編輯功能有一個(gè)常見坑編輯用戶時(shí)打開彈窗要把當(dāng)前行的數(shù)據(jù)深拷貝到表單模型里避免直接修改表格數(shù)據(jù)源。如果不做深拷貝彈窗里改字段時(shí)表格數(shù)據(jù)也會(huì)跟著變因?yàn)橐玫氖峭粋€(gè)對(duì)象。用JSON.parse(JSON.stringify(...))做深拷貝最簡(jiǎn)單或者用展開運(yùn)算符新建對(duì)象。5.4 狀態(tài)管理與跨組件通信的方案選擇Vue 3 中狀態(tài)管理的選擇比 Vue 2 時(shí)代豐富多了。Pinia 是官方推薦方案用它替代 Vuex 寫起來(lái)舒服得多沒有 mutations、actions 那套冗余的流程store 里定義的 state 直接用actions 就是一個(gè)普通函數(shù)。這個(gè)項(xiàng)目用到 Pinia 的場(chǎng)景用戶登錄后存基本信息和 token。在登錄成功時(shí)調(diào)用userStore.setLoginInfo(...)然后在多個(gè)組件里通過(guò)userStore.username展示當(dāng)前登錄用戶。示例代碼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) } } })在這個(gè)系統(tǒng)里用戶信息需要全局共享但很多業(yè)務(wù)組件之間的通信其實(shí)用 props 和 emit 就夠。過(guò)度引入全局狀態(tài)管理會(huì)讓代碼難追蹤——某個(gè)字段不知道被誰(shuí)修改了找起來(lái)很費(fèi)勁。我的原則是跨頁(yè)面、跨層級(jí)深組件才考慮 Pinia父子組件之間能用 props/emit 就用 props/emit。6. 前后端聯(lián)調(diào)與常見問(wèn)題排查6.1 聯(lián)調(diào)前的接口約定與準(zhǔn)備前后端分離開發(fā)模式下聯(lián)調(diào)階段往往是問(wèn)題集中爆發(fā)的時(shí)期。前端認(rèn)為接口該這么返回后端認(rèn)為前端該那么傳參經(jīng)常對(duì)不上。經(jīng)驗(yàn)是先定接口文檔再動(dòng)代碼哪怕是簡(jiǎn)單的表格約定、字段類型、命名風(fēng)格、錯(cuò)誤碼都提前對(duì)齊。接口文檔我用 YApi 或者 Apifox 維護(hù)簡(jiǎn)單項(xiàng)目也可以在項(xiàng)目根目錄放一個(gè) Markdown 格式的接口說(shuō)明文檔。文檔要約定如下內(nèi)容請(qǐng)求方式GET/POST/PUT/DELETE請(qǐng)求路徑和參數(shù)路徑參數(shù)、查詢參數(shù)、請(qǐng)求體參數(shù)參數(shù)類型string/number/boolean/object/array返回格式code/message/data 的結(jié)構(gòu)鑒權(quán)方式token 放在哪里怎么傳錯(cuò)誤碼語(yǔ)義200 成功、401 未授權(quán)、500 服務(wù)器錯(cuò)誤、業(yè)務(wù)錯(cuò)誤從 400 開始聯(lián)調(diào)前先用 Postman 把后端接口全部測(cè)一遍。實(shí)測(cè)下來(lái)后端寫接口時(shí)引入一個(gè)全局性的統(tǒng)一返回結(jié)構(gòu)ResultT聯(lián)調(diào)效率能提升不少。前端看到code/message/data就知道怎么處理省去了解析各種怪異結(jié)構(gòu)的時(shí)間。我在項(xiàng)目里定義了一個(gè)通用返回體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包裝后的對(duì)象異常處理用全局RestControllerAdvice捕獲并統(tǒng)一轉(zhuǎn)成這個(gè)格式前端就永遠(yuǎn)只需要處理一種結(jié)構(gòu)代碼邏輯非常簡(jiǎn)單。6.2 跨域問(wèn)題的三種表現(xiàn)與對(duì)應(yīng)解法跨域是全棧聯(lián)調(diào)最常遇到的問(wèn)題但它有不同層次的原因癥狀相同解法不同。第一種開發(fā)環(huán)境跨域。前端在http://localhost:3000后端在http://localhost:8080。瀏覽器攔截了跨域請(qǐng)求。通用的解法是后端加 CORS 配置讓后端允許前端來(lái)源訪問(wèn)。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 代理解決前端配置代理后前端請(qǐng)求的地址看起來(lái)是同源的。代理方式更好是因?yàn)樗芽缬騿?wèn)題的處理留在了構(gòu)建層后端不需要關(guān)心前端運(yùn)行在哪個(gè)端口而且開發(fā)環(huán)境和生產(chǎn)環(huán)境的代理配置方式保持一致生產(chǎn)環(huán)境用 Nginx。第二種后端接口報(bào) CORS 錯(cuò)誤但后端沒有 CORS 配置。這種情況多半是瀏覽器請(qǐng)求直接打到了后端地址而請(qǐng)求方式是非簡(jiǎn)單請(qǐng)求比如 PUT/DELETE或者帶了自定義請(qǐng)求頭比如 Authorization。這時(shí)候?yàn)g覽器會(huì)先發(fā) OPTIONS 預(yù)檢請(qǐng)求如果后端攔截器沒放行 OPTIONS就會(huì)報(bào) CORS 錯(cuò)。解決辦法是攔截器放行 OPTIONS。第三種生產(chǎn)環(huán)境跨域。前端和后端部署在不同域名。這種情況下最穩(wěn)妥的做法是用 Nginx 做反向代理讓前后端在同一個(gè)域名下前端請(qǐng)求/api路徑時(shí) Nginx 轉(zhuǎn)發(fā)給后端服務(wù)。6.3 開發(fā)過(guò)程中遇到的高頻報(bào)錯(cuò)速查表下面整理一份聯(lián)調(diào)階段的常見報(bào)錯(cuò)和解決建議都是實(shí)操中反復(fù)出現(xiàn)的問(wèn)題報(bào)錯(cuò)現(xiàn)象可能原因解決方法前端請(qǐng)求掛起很久后報(bào) timeout后端服務(wù)未啟動(dòng)或接口路徑不對(duì)檢查后端日志確認(rèn)服務(wù)啟動(dòng)檢查請(qǐng)求的路徑和 Controller 映射是否一致404 Not FoundURL 路徑寫錯(cuò)或 Controller 方法沒有映射核對(duì)前端 api 模塊中的 URL 和后端RequestMapping是否有出入401 Unauthorizedtoken 缺失或無(wú)效確認(rèn)登錄是否成功、token 是否在請(qǐng)求頭中被正確攜帶500 后端報(bào) NullPointerException后端代碼對(duì) null 值沒有處理看異常堆棧定位到具體行號(hào)檢查參數(shù)是否為空數(shù)據(jù)庫(kù)中文亂碼數(shù)據(jù)庫(kù)連接串沒有指定字符集或者表默認(rèn)字符集錯(cuò)誤在 JDBC URL 后加characterEncodingutf8建表時(shí)指定DEFAULT CHARSETutf8mb4MySQL 連接失敗 Public Key Retrieval is not allowed連接串缺少 allowPublicKeyRetrieval 參數(shù)JDBC URL 中加入allowPublicKeyRetrievaltrue打包前端后訪問(wèn)頁(yè)面空白路由模式或靜態(tài)資源路徑配置問(wèn)題如果使用 history 模式Nginx 要配置 try_files 重寫規(guī)則跨域報(bào) Access-Control-Allow-OriginCORS 配置不當(dāng)按照上文跨域問(wèn)題的三種表現(xiàn)依次排查6.4 前后端聯(lián)調(diào)中的時(shí)間同步與數(shù)據(jù)格式問(wèn)題還有一種不報(bào)錯(cuò)但結(jié)果不對(duì)的坑最典型的是時(shí)間字段。后端 LocalDateTime 序列化后得到的是2024-05-20T10:30:00而前端希望顯示的是2024-05-20 10:30:00。我見過(guò)不少項(xiàng)目因?yàn)闀r(shí)間格式對(duì)不上前端硬生生寫了個(gè)字符串替換函數(shù)來(lái)轉(zhuǎn)格式相當(dāng)丑陋。正確做法是在后端做好全局配置讓所有時(shí)間字段統(tǒng)一輸出為固定格式前端直接拿到標(biāo)準(zhǔn)字符串后展示或傳給后端。JSON 序列化這里再提一次在application.yml里加 Jackson 的時(shí)間格式配置代碼里不需要再給每個(gè)LocalDateTime字段加JsonFormat注解。不過(guò)要注意的是如果分頁(yè)返回的數(shù)據(jù)里包含時(shí)間排序序列化后的字符串格式影響不大只要保證比較的邏輯放在數(shù)據(jù)庫(kù)層面做就行。另一個(gè)常見問(wèn)題是前端傳日期字符串給后端時(shí)后端用DateTimeFormat注解或全局配置 Jackson 的LocalDateTime反序列化格式來(lái)接收。我遇到過(guò)前端傳2024-05-20 10:30:00后端接收后解析失敗報(bào) 400 錯(cuò)誤。這是因?yàn)?Fastjson 和 Jackson 對(duì) LocalDateTime 支持有差異解決方式是統(tǒng)一用 Jackson并在全局配置里聲明日期格式。7. 項(xiàng)目打包與生產(chǎn)環(huán)境部署7.1 前端構(gòu)建與單頁(yè)應(yīng)用路由配置生產(chǎn)部署的第一步是前端構(gòu)建。執(zhí)行npm run buildVite 會(huì)生成dist目錄里面是壓縮后的靜態(tài)資源文件。這里要特別關(guān)注兩個(gè)點(diǎn)。第一構(gòu)建產(chǎn)物的路徑問(wèn)題。如果你把前端部署在域名根路徑/下那baseURL保持/api沒問(wèn)題但如果部署在子路徑比如https://example.com/admin下Vite 構(gòu)建時(shí)要在vite.config.js里配置base: /admin/否則靜態(tài)資源的引用路徑全部 404。第二前端路由的 history 模式需要服務(wù)端配合。Vue Router 的 history 模式URL 中不帶#刷新在子路由頁(yè)面比如/user時(shí)如果不配置服務(wù)端重寫規(guī)則Nginx 只會(huì)看文件系統(tǒng)里有沒有對(duì)應(yīng)的user目錄結(jié)果就是 404。Nginx 配置如下location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }try_files的$uri先看請(qǐng)求路徑對(duì)應(yīng)的文件是否存在如果不存在就回退到/index.html由前端路由接管。7.2 后端打包與啟動(dòng)腳本后端用 Maven 打包mvn clean package -DskipTests命令執(zhí)行完會(huì)在target目錄下生成fullstack-0.0.1-SNAPSHOT.jar。如果你用的是 Java 17 和 Spring Boot 3.x還有一個(gè)可執(zhí)行 jar 的特性Spring Boot 的 Maven 插件會(huì)自動(dòng)配置 Main-Class你不需要額外配置直接java -jar就能運(yùn)行。啟動(dòng)命令java -jar fullstack-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod生產(chǎn)環(huán)境的配置建議單獨(dú)建一個(gè)application-prod.yml里面配置生產(chǎn)環(huán)境用的數(shù)據(jù)庫(kù)地址、端口、日志級(jí)別等和開發(fā)環(huán)境區(qū)分開。日志輸出也建議配置到文件中不要只輸出到控制臺(tái)服務(wù)重啟后日志就丟了。一個(gè)典型的日志配置logging: level: com.example.fullstack: info file: name: logs/fullstack.log線上環(huán)境有個(gè)小建議用nohup啟動(dòng)服務(wù)避免終端斷開時(shí)進(jìn)程被殺掉?;蛘吒?guī)范的寫法是注冊(cè)為 systemd 服務(wù)讓操作系統(tǒng)管理進(jìn)程生命周期開機(jī)自啟、崩潰重啟都能自動(dòng)處理。7.3 Nginx 反向代理與服務(wù)部署細(xì)節(jié)Nginx 在這套系統(tǒng)中的角色是托管前端靜態(tài)文件 反向代理后端 API。完整配置參考如下server { listen 80; server_name your-domain.com; # 前端靜態(tài)資源 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; } }這里有個(gè)細(xì)節(jié)location /api/加尾斜杠proxy_pass http://127.0.0.1:8080;不帶尾斜杠那 請(qǐng)求/api/user/list會(huì)轉(zhuǎn)發(fā)到http://127.0.0.1:8080/api/user/list路徑和原來(lái)保持一致。如果把proxy_pass寫成http://127.0.0.1:8080/請(qǐng)求會(huì)被轉(zhuǎn)發(fā)到http://127.0.0.1:8080/user/list/api前綴會(huì)被吞掉。這個(gè)細(xì)節(jié)容易踩坑務(wù)必確認(rèn)后端 Controller 的映射上有/api前綴否則會(huì)報(bào) 404。HTTPS 配置線上環(huán)境一般用 Certbot 自動(dòng)申請(qǐng)證書并配置到 Nginx這里先不展開但強(qiáng)調(diào)一句生產(chǎn)環(huán)境不要裸奔 HTTP涉及登錄和 token 的接口不上 HTTPS 相當(dāng)于把門鎖掛在門外面。7.4 數(shù)據(jù)庫(kù)初始化與遷移策略生產(chǎn)環(huán)境數(shù)據(jù)庫(kù)初始化不能靠手動(dòng)一條條執(zhí)行 SQL。我用 Flyway 做數(shù)據(jù)庫(kù)版本管理每次 SQL 變更寫一個(gè)帶版本號(hào)的文件放到resources/db/migration目錄應(yīng)用啟動(dòng)時(shí)會(huì)自動(dòng)執(zhí)行未執(zhí)行過(guò)的腳本。打比方說(shuō)明一下Flyway 就像數(shù)據(jù)庫(kù)的 Git每次變更都是一個(gè) commit版本號(hào)遞增它會(huì)在數(shù)據(jù)庫(kù)中建一張flyway_schema_history表來(lái)記錄已經(jīng)執(zhí)行過(guò)的版本下次啟動(dòng)時(shí)自動(dòng)跳過(guò)已執(zhí)行的只執(zhí)行新增的遷移腳本。示例遷移文件-- 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 狀態(tài)1啟用 0禁用, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;使用 Flyway 有兩點(diǎn)要非常謹(jǐn)慎已經(jīng)提交給團(tuán)隊(duì)的遷移腳本不要修改哪怕里面 SQL 寫錯(cuò)了也要靠新的遷移腳本來(lái)修正否則團(tuán)隊(duì)的數(shù)據(jù)庫(kù)版本就對(duì)不上了。另外生產(chǎn)庫(kù)不要輕易執(zhí)行flyway clean它會(huì)重建整個(gè)數(shù)據(jù)庫(kù)操作前就算寫滿警告也不為過(guò)。8. 常用工具與效率提升清單8.1 接口調(diào)試工具的選擇聯(lián)調(diào)階段接口測(cè)試工具的作用很大。Postman 是最經(jīng)典的選擇優(yōu)勢(shì)是功能齊全、支持 Collection 管理、環(huán)境變量設(shè)置。Apifox 是國(guó)內(nèi)團(tuán)隊(duì)常用的方案優(yōu)勢(shì)是集成了接口文檔管理、Mock 數(shù)據(jù)、自動(dòng)化測(cè)試。如果項(xiàng)目中后端還沒有完全開發(fā)完Apifox 可以直接在接口文檔上生成 Mock 數(shù)據(jù)給前端先行開發(fā)這種前后端并行開發(fā)的模式能省不少時(shí)間。實(shí)際使用建議如果你是一個(gè)人做全棧直接用 IDEA 的 HTTP Client 就行不需要額外裝軟件。IDEA 里新建.http文件寫請(qǐng)求可以配合{{var}}使用環(huán)境變量還可以在后端代碼左側(cè)直接 Run 生成對(duì)應(yīng)的測(cè)試請(qǐng)求效率很高。8.2 數(shù)據(jù)庫(kù)連接與查詢工具數(shù)據(jù)庫(kù)客戶端方面Navicat 和 DataGrip 都常用。Navicat 功能全面、界面友好DataGrip 更適合寫復(fù)雜 SQL它提供的智能提示和格式化功能非常好用。個(gè)人項(xiàng)目簡(jiǎn)單查詢用 VS Code 的 MySQL 插件也可以湊合。我的建議是對(duì)于持久層查詢用 MyBatis Plus 的LambdaQueryWrapper寫條件查詢已經(jīng)非常直觀大多數(shù)場(chǎng)景不需要打開數(shù)據(jù)庫(kù)客戶端。只有在排查數(shù)據(jù)問(wèn)題時(shí)才需要連數(shù)據(jù)庫(kù)。遇到數(shù)據(jù)庫(kù)連接不上的情況先檢查服務(wù)是否啟動(dòng)、端口是否被占用、認(rèn)證插件版本是否匹配這三個(gè)點(diǎn)大多數(shù)問(wèn)題都能解決。8.3 聯(lián)調(diào)排查常用的瀏覽器開發(fā)者工具前端調(diào)試離不開瀏覽器開發(fā)者工具。Network 面板能看到每個(gè)請(qǐng)求的狀態(tài)碼、耗時(shí)、請(qǐng)求頭和響應(yīng)體Console 面板直接報(bào)錯(cuò)信息Vue 專門有 Vue DevTools 插件可以查看組件樹、Pinia 狀態(tài)和路由信息。排查問(wèn)題建議按照這個(gè)順序來(lái)看先在 Network 里確認(rèn)請(qǐng)求發(fā)出去了沒有、狀態(tài)碼是多少再看請(qǐng)求頭里的 Content-Type 和 Authorization 對(duì)不對(duì)然后看響應(yīng)體里的具體數(shù)據(jù)最后結(jié)合 Console 的報(bào)錯(cuò)信息定位問(wèn)題。有的開發(fā)者一上來(lái)就在代碼里打斷點(diǎn)其實(shí)大多數(shù)聯(lián)調(diào)問(wèn)題在網(wǎng)絡(luò)請(qǐng)求這一層就能定位。8.4 代碼生成器的合理使用到了項(xiàng)目后期增刪改查的代碼寫多了會(huì)產(chǎn)生明顯的機(jī)械重復(fù)感。手動(dòng)復(fù)制粘貼再改字段名容易出錯(cuò)我保留了一條規(guī)則多表關(guān)聯(lián)、復(fù)雜業(yè)務(wù)邏輯的代碼必須手寫但單表 CRUD 的代碼交給生成器生成生成完再人工審查和微調(diào)。MyBatis Plus 官方提供了一個(gè)代碼生成器mybatis-plus-generator可以根據(jù)數(shù)據(jù)庫(kù)表自動(dòng)生成 Entity、Mapper、Service、Controller 代碼。用它的時(shí)機(jī)設(shè)置在項(xiàng)目初始化階段把表結(jié)構(gòu)設(shè)計(jì)好一鍵生成基礎(chǔ)代碼后面再在 Service 中補(bǔ)充業(yè)務(wù)邏輯。這能省去大量重復(fù)勞動(dòng)但你一定要會(huì)改生成的代碼不然會(huì)陷入“低級(jí)錯(cuò)誤看不出來(lái)”的局面。9. 項(xiàng)目擴(kuò)展方向與進(jìn)階建議9.1 從單體應(yīng)用到模塊化演進(jìn)當(dāng)前項(xiàng)目的結(jié)構(gòu)是單體應(yīng)用這個(gè)結(jié)構(gòu)適合大多數(shù)中小型項(xiàng)目。隨著業(yè)務(wù)量增長(zhǎng)你可以按模塊拆分比如用戶模塊、訂單模塊、商品模塊每個(gè)模塊獨(dú)立成一個(gè) Maven 模塊甚至獨(dú)立成一個(gè) Spring Boot 服務(wù)。拆分的關(guān)鍵依據(jù)是團(tuán)隊(duì)規(guī)模和部署復(fù)雜度不要因?yàn)橼厔?shì)而盲目拆分。如果項(xiàng)目超過(guò) 10 個(gè)接口、3 個(gè)以上開發(fā)人員合理的模塊化拆分能明顯減少合并沖突和協(xié)作成本。反過(guò)來(lái)如果就是一個(gè)人的項(xiàng)目拆成微服務(wù)純屬自找麻煩。9.2 引入 Redis 緩存與接口性能優(yōu)化項(xiàng)目中如果有熱點(diǎn)數(shù)據(jù)比如用戶信息、系統(tǒng)配置可以引入 Redis 做緩存層。實(shí)現(xiàn)方式是在原有的 Service 邏輯包一層先查緩存緩存未命中再查數(shù)據(jù)庫(kù)同時(shí)把結(jié)果寫入緩存并設(shè)置過(guò)期時(shí)間。用 Redis 的StringRedisTemplate就夠不需要引入復(fù)雜的緩存框架。這個(gè)實(shí)踐我建議放到項(xiàng)目穩(wěn)定之后再做第二個(gè)版本迭代。先保證功能正確再考慮性能。接口響應(yīng)時(shí)間慢先看是不是數(shù)據(jù)庫(kù)查詢的問(wèn)題有沒有索引、有沒有 N1 查詢?cè)贈(zèng)Q定要不要加緩存。過(guò)早引入緩存會(huì)讓 bug 排查多一層維度代價(jià)不小。9.3 安全加固與權(quán)限模型的進(jìn)階方向做全棧項(xiàng)目時(shí)很多人忽略了權(quán)限設(shè)計(jì)。目前項(xiàng)目只有“登錄/未登錄”兩層。如果你的系統(tǒng)需要不同角色管理員、普通用戶、訪客可以引入 RBAC 權(quán)限模型用戶-角色-權(quán)限三層結(jié)構(gòu)。表設(shè)計(jì)上新建sys_role、sys_menu、sys_user_role、sys_role_menu四張表后端在攔截器或注解層面校驗(yàn)是否有指定權(quán)限。前端對(duì)應(yīng)地可以做動(dòng)態(tài)路由用戶登錄后根據(jù)后端返回的權(quán)限列表動(dòng)態(tài)生成可訪問(wèn)的路由配置。這個(gè)是很多全棧項(xiàng)目的進(jìn)階難點(diǎn)會(huì)接觸到更深的前后端正交互。建議把當(dāng)前項(xiàng)目跑通后再往這個(gè)方向升級(jí)每一步都是新的技術(shù)內(nèi)容。9.4 前端工程化的進(jìn)一步打磨前端部分可以繼續(xù)打磨的點(diǎn)很多代碼規(guī)范ESLint Prettier、提交規(guī)范Commitlint husky、單元測(cè)試Vitest、自動(dòng)化部署GitHub Actions。這些屬于前端工程化范疇。舉一個(gè)很實(shí)用的例子在項(xiàng)目里加 ESLint 和 Prettier 配置團(tuán)隊(duì)協(xié)作時(shí)代碼風(fēng)格統(tǒng)一少了無(wú)謂的爭(zhēng)吵。不建議一開始就上全量工程化配置先把項(xiàng)目跑通再逐步加上這些工具學(xué)習(xí)曲線更平滑。10. 完整的實(shí)操流程對(duì)照表下表整理了這個(gè)項(xiàng)目從零到上線的完整操作清單按順序執(zhí)行每完成一步打一個(gè)勾階段關(guān)鍵操作產(chǎn)出物耗時(shí)預(yù)估環(huán)境準(zhǔn)備JDK 17、Maven、Node.js 20、MySQL 8開發(fā)環(huán)境跑通0.5 天數(shù)據(jù)庫(kù)設(shè)計(jì)創(chuàng)建數(shù)據(jù)庫(kù)和表、初始化測(cè)試數(shù)據(jù)SQL 腳本0.5 天后端骨架創(chuàng)建 Spring Boot 項(xiàng)目、MyBatis Plus 配置可啟動(dòng)空項(xiàng)目0.5 天后端核心接口登錄、CRUD、攔截器、全局異常處理可測(cè)試的 REST API2-3 天前端骨架Vite 創(chuàng)建項(xiàng)目、配置路由、Pinia、Axios可運(yùn)行前端基礎(chǔ)工程1 天前端頁(yè)面開發(fā)登錄頁(yè)、布局頁(yè)、用戶管理頁(yè)可交互的完整頁(yè)面2-3 天前后端聯(lián)調(diào)測(cè)試登錄流程、CRUD 流程、異常流程完整的業(yè)務(wù)閉環(huán)1 天打包部署前端 build、后端 jar、Nginx 配置生產(chǎn)環(huán)境可訪問(wèn)系統(tǒng)1 天上線檢查安全、日志、備份、監(jiān)控穩(wěn)定運(yùn)行的系統(tǒng)0.5 天這是一個(gè)典型的個(gè)人全棧項(xiàng)目的工期預(yù)估。如果你是第一次做時(shí)間會(huì)翻倍正?,F(xiàn)象。如果遇到卡殼超過(guò)半天的問(wèn)題建議跳過(guò)去先把主流程跑通再回頭看卡住的部分。我個(gè)人的經(jīng)驗(yàn)是全棧開發(fā)能力的提升不是線性的而是靠一個(gè)個(gè)完整的項(xiàng)目串起來(lái)的。這個(gè)項(xiàng)目雖然從技術(shù)角度看每個(gè)環(huán)節(jié)都是“常用技術(shù)”但當(dāng)它們組合在一起形成完整閉環(huán)時(shí)你對(duì)前端和后端協(xié)作模式的理解會(huì)上一個(gè)臺(tái)階。當(dāng)你遇到任何業(yè)務(wù)需求時(shí)腦子里能浮現(xiàn)出完整的請(qǐng)求鏈路和數(shù)據(jù)流轉(zhuǎn)圖而不是只盯著自己熟悉的那一層這就算真正入門了。