實戰(zhàn):表單校驗、加載態(tài)與全局異常處理)
1. 從改個密碼說起為什么這個頁面比想象中難做修改密碼這四個字放在任何后臺系統(tǒng)里都像是最不起眼的一個功能。產品經理畫原型的時候可能只給了一個彈窗前端拿到需求的第一反應往往是半小時搞定。但真做過幾個中后臺項目的人都知道這個頁面是典型的看著簡單、坑全在細節(jié)里——它同時牽扯到表單校驗、接口安全、全局異常處理、加載態(tài)管理、路由守衛(wèi)這幾條線任何一條沒處理好用戶就會遇到點了沒反應報錯看不懂改完密碼還能用舊密碼登錄這類問題。這篇內容圍繞一個基于 Element UI 的修改密碼頁面展開核心要解決三件事頁面本身怎么設計才既安全又好用、加載態(tài)怎么管理才不閃爍不卡頓、異常怎么全局捕捉才能讓用戶看到人話而不是一串堆棧。適合正在做中后臺系統(tǒng)的前端開發(fā)者尤其是用 Vue 2 Element UI 這套經典組合的團隊。哪怕你現(xiàn)在用的是 Vue 3 Element Plus思路也是通的只是 API 名字換一換。我先把結論擺前面修改密碼這個功能前端能做的安全防護其實很有限但體驗優(yōu)化和異常兜底的空間非常大。很多人把精力全花在密碼強度正則上卻忽略了加載態(tài)和異常處理結果就是用戶改密碼改得一肚子火。下面我按實際開發(fā)順序把每個環(huán)節(jié)拆開講。2. 修改密碼頁面的字段設計與校驗邏輯2.1 三個輸入框背后的真實需求一個標準的修改密碼表單通常是三個字段原密碼、新密碼、確認新密碼。看起來平平無奇但每個字段的設計都有講究。原密碼字段的作用不只是驗證身份它還有一個隱藏價值防止用戶在公共設備上被他人惡意改密。所以這個字段必須要求輸入且不能做任何記住上次輸入的處理autocomplete屬性要顯式設置為off或者new-password避免瀏覽器自動填充把舊密碼暴露出來。新密碼字段是校驗的重災區(qū)。常見的強度規(guī)則包括長度 8-20 位、必須包含大小寫字母和數(shù)字、不能包含連續(xù)字符如 123456、abcdef、不能和原密碼相同。這里有個經驗規(guī)則不要一次性全堆給用戶看而是在輸入過程中動態(tài)提示。Element UI 的el-form配合自定義validator就能做到用戶每輸入一個字符就實時反饋哪條規(guī)則還沒滿足比提交后彈一堆紅字友好得多。確認密碼字段的核心是一致性校驗但要注意一個細節(jié)當用戶修改新密碼后確認密碼的校驗狀態(tài)需要重新觸發(fā)。很多人只寫了validator卻忘了在newPassword變化時手動validateField(confirmPassword)導致用戶改了新密碼、確認框還顯示通過提交時才報錯。2.2 用自定義 validator 把校驗寫扎實Element UI 的el-form校驗依賴async-validator內置規(guī)則能覆蓋大部分場景但密碼強度這種復合規(guī)則必須自己寫。下面是一段我實際項目里用過的校驗邏輯做了簡化data() { const validateNewPassword (rule, value, callback) { if (!value) { return callback(new Error(請輸入新密碼)) } if (value this.form.oldPassword) { return callback(new Error(新密碼不能與原密碼相同)) } const hasLetter /[a-zA-Z]/.test(value) const hasNumber /\d/.test(value) if (!hasLetter || !hasNumber) { return callback(new Error(密碼需同時包含字母和數(shù)字)) } if (/(.)\1{2,}/.test(value)) { return callback(new Error(密碼不能包含三個及以上連續(xù)相同字符)) } callback() } const validateConfirm (rule, value, callback) { if (value ! this.form.newPassword) { return callback(new Error(兩次輸入的密碼不一致)) } callback() } return { form: { oldPassword: , newPassword: , confirmPassword: }, rules: { oldPassword: [{ required: true, message: 請輸入原密碼, trigger: blur }], newPassword: [{ required: true, validator: validateNewPassword, trigger: blur }], confirmPassword: [{ required: true, validator: validateConfirm, trigger: blur }] } } }這里有個容易忽略的點trigger用blur還是change。密碼強度校驗建議用blur因為用戶輸入過程中頻繁觸發(fā)校驗會讓界面一直閃紅字體驗很差而確認密碼的一致性校驗可以用change讓用戶改完立刻知道對不對。同一個表單里不同字段用不同 trigger是提升體驗的細節(jié)。2.3 密碼可見性切換與輸入框的交互細節(jié)Element UI 的el-input支持show-password屬性一行代碼就能加上小眼睛切換。但要注意show-password在舊版本里叫show-password新版本 Element Plus 里也是這個名字別寫成showPassword或者password否則不生效。另外三個密碼框建議都加上maxlength20防止用戶粘貼超長字符串導致后端截斷或報錯。clearable屬性看情況加原密碼框不建議加避免用戶誤點清空后還得重新輸。3. 加載態(tài)管理別讓用戶對著白屏發(fā)呆3.1 頁面級加載與按鈕級加載的區(qū)別加載頁面這個需求很多人第一反應是加個v-loading就完事了。但實際場景里加載態(tài)分兩個層級頁面級加載進入修改密碼頁面時可能需要先拉取一些初始化數(shù)據(jù)比如用戶的安全問題、密碼最后修改時間。這時候用v-loading蓋住整個表單區(qū)域是合理的。按鈕級加載用戶點擊確認修改后請求發(fā)出到返回的這段時間應該只讓提交按鈕進入loading狀態(tài)而不是整個頁面轉圈。因為整個頁面轉圈會讓用戶覺得系統(tǒng)卡死了而按鈕轉圈傳達的是正在處理你的請求心理感受完全不同。Element UI 的el-button自帶loading屬性直接綁定一個布爾值即可el-button typeprimary :loadingsubmitting clickhandleSubmit {{ submitting ? 提交中... : 確認修改 }} /el-button注意loading狀態(tài)下按鈕會自動禁用點擊不需要額外寫:disabled否則可能出現(xiàn)樣式沖突。3.2 防止重復提交的三重保險加載態(tài)最核心的作用其實是防止重復提交。用戶網(wǎng)絡慢的時候會瘋狂點按鈕如果不做處理可能發(fā)出五六個請求后端如果沒做冪等密碼就被改了多次甚至出現(xiàn)新密碼是第幾次提交的這種詭異問題。我的做法是三重保險按鈕 loading視覺上禁用這是第一層。方法入口攔截在handleSubmit開頭判斷if (this.submitting) return這是第二層防止鍵盤回車等繞過按鈕的操作。請求取消用 axios 的CancelToken或者 AbortController在組件銷毀或新請求發(fā)起時取消上一個未完成的請求這是第三層。第三層很多人不做但在用戶快速切換頁面時特別有用。如果不取消請求返回后組件已經銷毀回調里訪問this可能報錯或者觸發(fā)一些莫名其妙的全局提示。3.3 加載態(tài)的最小展示時間有個反直覺的經驗加載態(tài)不要一閃而過。如果接口響應特別快比如內網(wǎng) 50ms 就返回loading 圖標閃一下反而讓用戶覺得是不是沒點上。我一般會做一個最小展示時間比如 300ms用Promise.all配合一個延時 Promise 實現(xiàn)const minDelay new Promise(resolve setTimeout(resolve, 300)) const request axios.post(/api/change-password, this.form) await Promise.all([request, minDelay])這樣無論接口多快loading 至少顯示 300ms視覺上更穩(wěn)定。當然這個值別設太大超過 500ms 用戶就會覺得慢了。4. 全局異常捕捉讓報錯說人話4.1 為什么需要全局異常處理修改密碼接口可能返回各種錯誤原密碼錯誤、新密碼不符合后端規(guī)則、賬號被鎖定、token 過期、服務器 500。如果每個錯誤都在catch里寫一遍this.$message.error(err.message)代碼會又臭又長而且不同開發(fā)者寫的提示風格不統(tǒng)一有的顯示請求失敗有的顯示Error: Request failed with status code 400用戶看了完全懵。全局異常捕捉的價值就在于統(tǒng)一攔截、統(tǒng)一轉換、統(tǒng)一展示。前端拿到錯誤后先判斷錯誤類型再決定是彈全局提示、跳登錄頁還是交給業(yè)務代碼自己處理。4.2 axios 攔截器里的錯誤分類處理我一般會在 axios 的響應攔截器里做第一層處理按 HTTP 狀態(tài)碼和業(yè)務碼分類axios.interceptors.response.use( response { const res response.data if (res.code ! 0) { // 業(yè)務錯誤交給全局提示 handleBusinessError(res) return Promise.reject(res) } return res }, error { if (error.response) { const status error.response.status if (status 401) { // token 失效跳登錄 store.dispatch(user/logout) router.push(/login) Message.error(登錄已過期請重新登錄) } else if (status 403) { Message.error(沒有權限執(zhí)行此操作) } else if (status 500) { Message.error(服務器開小差了請稍后重試) } else { Message.error(error.response.data.message || 請求失敗) } } else if (error.code ECONNABORTED) { Message.error(請求超時請檢查網(wǎng)絡) } else { Message.error(網(wǎng)絡異常請稍后重試) } return Promise.reject(error) } )這里的關鍵是區(qū)分能自動處理的錯誤和需要業(yè)務代碼處理的錯誤。比如 401 直接跳登錄業(yè)務代碼不用管但原密碼錯誤這種業(yè)務錯誤可能需要把焦點定位到原密碼輸入框這就得讓業(yè)務代碼自己處理。所以攔截器里處理完通用錯誤后仍然要reject讓業(yè)務層的catch有機會介入。4.3 業(yè)務錯誤的提示策略修改密碼接口最常見的業(yè)務錯誤就是原密碼錯誤。這個提示不能只彈一個 toast因為用戶很可能需要重新輸入。我的做法是toast 提示 清空原密碼框 聚焦。async handleSubmit() { if (this.submitting) return try { await this.$refs.form.validate() } catch (e) { return } this.submitting true try { await changePassword(this.form) this.$message.success(密碼修改成功請重新登錄) setTimeout(() { this.$router.push(/login) }, 1000) } catch (err) { if (err.code OLD_PASSWORD_WRONG) { this.form.oldPassword this.$refs.oldPasswordInput.focus() } } finally { this.submitting false } }注意finally里一定要把submitting置回 false否則一旦出錯按鈕就永遠轉圈了。這是新手最容易犯的錯誤之一。4.4 Vue 全局錯誤捕獲兜底除了接口錯誤還有一類錯誤是代碼運行時錯誤比如某個組件里訪問了 undefined 的屬性。這類錯誤如果不捕獲整個頁面可能白屏。Vue 提供了Vue.config.errorHandler做全局兜底Vue.config.errorHandler function (err, vm, info) { console.error(全局捕獲:, err, info) // 上報到監(jiān)控平臺 reportError(err, info) // 給用戶一個友好提示避免白屏 Message.error(頁面出現(xiàn)異常請刷新重試) }這個兜底不是萬能的它捕獲不了 Promise 里未 catch 的錯誤所以還需要監(jiān)聽unhandledrejectionwindow.addEventListener(unhandledrejection, event { console.error(未處理的 Promise 錯誤:, event.reason) reportError(event.reason) })兩層加起來基本能覆蓋絕大多數(shù)異常場景。5. 接口安全與前端能做的防護邊界5.1 前端加密到底有沒有用很多項目要求密碼傳輸前加密常見做法是用 RSA 或 AES 在前端加密后再發(fā)給后端。這里必須說清楚前端加密不能替代 HTTPS。如果沒上 HTTPS中間人可以直接抓包拿到加密后的密文雖然不能直接還原明文但可以重放攻擊。前端加密的真正價值是防止日志和監(jiān)控系統(tǒng)記錄明文密碼。很多公司的接口日志會把請求體打出來如果不加密運維和開發(fā)就能在日志里看到用戶密碼這是合規(guī)大忌。所以前端加密更多是防內部泄露而不是防外部攻擊。如果要做推薦用 RSA 公鑰加密公鑰由后端下發(fā)私鑰只在后端保存。前端用jsencrypt這類庫即可但要注意公鑰的獲取時機和緩存策略別每次請求都去拉公鑰。5.2 修改成功后的強制下線密碼修改成功后必須讓當前 token 失效并強制重新登錄。原因很簡單如果舊 token 還能用那攻擊者拿到舊 token 依然能操作改密碼就失去了意義。實現(xiàn)上有兩種方式一是后端在改密成功后主動使該用戶所有 token 失效前端收到成功響應后跳登錄頁二是前端在成功后主動調登出接口。推薦第一種因為更安全前端只需要負責跳轉。跳轉前給用戶一個明確的提示比如密碼修改成功即將重新登錄然后延遲 1-2 秒跳轉讓用戶有時間看到提示。直接瞬間跳轉會讓用戶困惑我剛才到底改成功沒有。5.3 密碼字段的自動填充與瀏覽器記憶瀏覽器對typepassword的輸入框有自動填充和記住密碼的行為這在修改密碼場景里是負面的。除了設置autocompletenew-password還可以用一些技巧把原密碼框的name屬性設成一個不常見的值避免瀏覽器識別為登錄表單。在表單外層包一個autocompleteoff的 form。如果還是被填充可以在mounted里延遲清空一次。這些手段不能 100% 阻止瀏覽器行為但能大幅降低誤填充概率。6. 踩坑實錄那些文檔里不會寫的細節(jié)6.1 el-form 校驗的異步陷阱this.$refs.form.validate()返回的是一個 Promise但如果你傳了回調函數(shù)它就不返回 Promise 了。很多人混用兩種寫法導致await拿不到結果// 錯誤寫法傳了回調await 拿到的是 undefined await this.$refs.form.validate((valid) { ... }) // 正確寫法一不傳回調用 try/catch try { await this.$refs.form.validate() } catch (e) { return } // 正確寫法二傳回調不用 await this.$refs.form.validate((valid) { if (valid) { this.submit() } })這個坑我見過至少五個同事踩過表現(xiàn)就是校驗沒通過但請求還是發(fā)出去了。6.2 loading 與路由跳轉的時序問題改密成功后跳登錄頁如果跳轉太快loading 狀態(tài)還沒重置登錄頁可能繼承一些奇怪的全局狀態(tài)。我的做法是先重置 loading再跳轉并且用setTimeout給一個緩沖this.submitting false this.$message.success(修改成功) setTimeout(() this.$router.push(/login), 800)另外如果用了 keep-alive 緩存了這個頁面跳轉前記得把表單重置否則用戶下次進來還看到上次的密碼雖然密碼框顯示的是圓點但值還在。6.3 全局異常處理與業(yè)務提示的沖突有時候全局攔截器已經彈了一個錯誤提示業(yè)務代碼的catch里又彈了一個用戶就看到兩個 toast 疊在一起。解決辦法是在錯誤對象上打標記// 攔截器里 error.handled true Message.error(...) // 業(yè)務層 catch (err) { if (!err.handled) { Message.error(err.message) } }這樣全局處理過的錯誤業(yè)務層就不再重復提示。這個模式在大型項目里幾乎是標配。6.4 密碼強度提示的實時性前面提到用blur觸發(fā)校驗但強度提示比如還需包含數(shù)字最好用input事件實時更新而不是等失焦。可以單獨維護一個strengthTips數(shù)組在input時計算computed: { strengthTips() { const pwd this.form.newPassword return [ { text: 至少8位, ok: pwd.length 8 }, { text: 包含字母, ok: /[a-zA-Z]/.test(pwd) }, { text: 包含數(shù)字, ok: /\d/.test(pwd) } ] } }然后在模板里用不同顏色渲染用戶輸入時就能看到哪條變綠了體驗比提交后報錯好太多。7. 一套可復用的實現(xiàn)骨架把上面的內容串起來一個完整的修改密碼頁面大致包含這些部分表單結構、校驗規(guī)則、加載態(tài)管理、提交邏輯、異常處理、成功后的跳轉。我把它整理成一個對照表方便你按圖索驥模塊關鍵點常見坑表單字段原密碼、新密碼、確認密碼忘記關閉自動填充校驗規(guī)則自定義 validator、動態(tài) trigger改新密碼后未重校驗確認框加載態(tài)按鈕級 loading、最小展示時間出錯后未重置 loading防重復提交入口攔截 請求取消只靠按鈕禁用回車可繞過全局異常攔截器分類處理、錯誤標記全局與業(yè)務重復彈提示成功后處理強制下線、延遲跳轉跳轉過快、狀態(tài)未重置這套骨架我在幾個中后臺項目里反復用過基本能覆蓋 90% 的修改密碼場景。剩下的 10% 是各家的特殊需求比如有的要求改密后發(fā)郵件通知有的要求二次驗證那些就在這個骨架上加鉤子即可。最后分享一個我自己的習慣修改密碼頁面寫完一定要用慢網(wǎng)絡模擬測一遍。Chrome DevTools 里把網(wǎng)絡調到 Slow 3G然后瘋狂點提交按鈕看看會不會重復請求、loading 會不會卡死、錯誤提示會不會疊。這個測試花不了五分鐘但能提前發(fā)現(xiàn)大部分加載態(tài)和異常處理的問題。比等到測試同學提 bug 再改成本低得多。