頁(yè)面實(shí)戰(zhàn):axios+apifox模擬接口實(shí)現(xiàn)前后端聯(lián)調(diào))
先別著急寫(xiě)代碼。很多新手學(xué)前端第一個(gè)練手項(xiàng)目就是注冊(cè)頁(yè)面但做完之后總有一種“這頁(yè)面能動(dòng)但我說(shuō)不清它為什么能動(dòng)”的感覺(jué)。問(wèn)題往往不在于HTML和CSS而在于請(qǐng)求到底發(fā)到哪去了、后端到底收到了什么、返回的數(shù)據(jù)前端又是怎么處理的。這次這篇實(shí)戰(zhàn)我把整套鏈路拆開(kāi)揉碎前端用原生的HTML/CSS/JavaScript寫(xiě)一個(gè)超簡(jiǎn)潔的注冊(cè)頁(yè)面請(qǐng)求部分用axios發(fā)POST后端接口直接用apifox來(lái)模擬。也就是說(shuō)我們不依賴(lài)真實(shí)的后端服務(wù)先在apifox里把一個(gè)“假接口”造出來(lái)再讓前端頁(yè)面真實(shí)地請(qǐng)求它、拿到返回結(jié)果。整個(gè)過(guò)程完全可以在一臺(tái)電腦上復(fù)現(xiàn)適合剛學(xué)完前端基礎(chǔ)、想搞明白前后端聯(lián)調(diào)是怎么回事的讀者。1. 動(dòng)手前的思路拆解注冊(cè)頁(yè)面為什么值得認(rèn)真做1.1 注冊(cè)功能的前后端協(xié)作邏輯注冊(cè)頁(yè)面的本質(zhì)是用戶(hù)在瀏覽器里填寫(xiě)信息前端收集并校驗(yàn)這些信息然后通過(guò)網(wǎng)絡(luò)請(qǐng)求提交給服務(wù)器服務(wù)器處理完后返回一個(gè)結(jié)果前端再根據(jù)這個(gè)結(jié)果決定下一步動(dòng)作。這一來(lái)一回就是一次完整的前后端交互。拆開(kāi)看它包含三個(gè)核心角色前端頁(yè)面負(fù)責(zé)信息采集、格式校驗(yàn)、請(qǐng)求發(fā)送、響應(yīng)渲染。網(wǎng)絡(luò)協(xié)議用HTTP協(xié)議里的POST方法把數(shù)據(jù)放在請(qǐng)求體里傳給服務(wù)器。后端接口接收參數(shù)、校驗(yàn)數(shù)據(jù)、落庫(kù)最后返回成功或失敗的JSON數(shù)據(jù)。這里面容易忽略的是角色邊界。很多新手寫(xiě)注冊(cè)頁(yè)習(xí)慣在前端把密碼強(qiáng)度、手機(jī)號(hào)格式全校驗(yàn)完了就以為萬(wàn)事大吉。但真實(shí)的后端接口同樣會(huì)做一套完整的校驗(yàn)。前端校驗(yàn)是為了用戶(hù)體驗(yàn)后端校驗(yàn)才是為了安全和數(shù)據(jù)可靠。apifox模擬接口的時(shí)候我們可以通過(guò)Mock規(guī)則簡(jiǎn)單模擬這一層校驗(yàn)比如約定用戶(hù)名長(zhǎng)度、密碼位數(shù)不符合規(guī)則就返回錯(cuò)誤碼。理解了這個(gè)角色劃分后面所有的代碼寫(xiě)起來(lái)思路都會(huì)清晰很多。1.2 為什么選apifox做模擬接口說(shuō)實(shí)話能模擬接口的工具不少Postman、Apifox、YApi、Rap2都干得動(dòng)這件事。我選擇apifox主要是因?yàn)樗诮涌诠芾?、Mock數(shù)據(jù)、文檔生成、本地調(diào)試這幾個(gè)環(huán)節(jié)上做得足夠一體化對(duì)個(gè)人開(kāi)發(fā)者和前端初學(xué)者尤其友好。apifox的核心邏輯是“先定義接口再生成數(shù)據(jù)”。你在界面里把接口的路徑、請(qǐng)求方法、請(qǐng)求參數(shù)、返回?cái)?shù)據(jù)結(jié)構(gòu)定義好它可以自動(dòng)生成一份可訪問(wèn)的Mock接口地址。前端拿著這個(gè)地址就能發(fā)請(qǐng)求。這比傳統(tǒng)方式省事在什么地方呢傳統(tǒng)方式去搭建一個(gè)真實(shí)的Node.js或Java后端光配置環(huán)境、寫(xiě)接口邏輯就得占掉大半時(shí)間對(duì)只想練前端的人來(lái)說(shuō)負(fù)擔(dān)太重。apifox把這個(gè)過(guò)程壓縮到了幾分鐘。另外apifox可以在定義接口時(shí)就確定好字段名和字段類(lèi)型這相當(dāng)于提前和“后端”約定了數(shù)據(jù)格式。很多前端開(kāi)發(fā)中的聯(lián)調(diào)沖突根源就是字段名沒(méi)商量好——前端傳username后端要userName前端傳phone后端要mobile類(lèi)型也對(duì)不上排查起來(lái)極其耗費(fèi)時(shí)間。用apifox先定義等于先把契約固定下來(lái)。1.3 為什么用axios而不是fetch瀏覽器原生提供了fetch方法一樣可以發(fā)請(qǐng)求那為什么還要額外引入axios如果你只是發(fā)一次簡(jiǎn)單的GET請(qǐng)求兩者區(qū)別不大。但是注冊(cè)功能涉及請(qǐng)求攔截、響應(yīng)攔截、超時(shí)設(shè)置、錯(cuò)誤處理、統(tǒng)一加headers這些需求axios把這些問(wèn)題全部封裝成了簡(jiǎn)潔的API寫(xiě)起來(lái)更直觀心智負(fù)擔(dān)小得多。舉個(gè)例子頁(yè)面里可能會(huì)給每個(gè)請(qǐng)求統(tǒng)一加上一個(gè)token字段或者統(tǒng)一處理HTTP狀態(tài)碼401的情況。用fetch你得每個(gè)請(qǐng)求都寫(xiě)一遍處理邏輯或者自己封裝一層工具函數(shù)。而axios有攔截器機(jī)制可以在請(qǐng)求發(fā)出前統(tǒng)一處理在響應(yīng)回來(lái)后統(tǒng)一處理錯(cuò)誤代碼復(fù)用性明顯更好。axios基于XMLHttpRequest這在兼容性上也有天然優(yōu)勢(shì)老版本瀏覽器也能用。fetch雖然是標(biāo)準(zhǔn)API但對(duì)一些老環(huán)境的支持要打折扣新手調(diào)起來(lái)容易踩坑。既然要做一個(gè)“拿來(lái)就能用”的注冊(cè)頁(yè)面axios顯然更合適。2. 用apifox先把“假后端”搭起來(lái)2.1 創(chuàng)建項(xiàng)目與接口定義第一次打開(kāi)apifox會(huì)看到幾個(gè)內(nèi)置示例項(xiàng)目。我建議你別直接用示例而是新建一個(gè)空項(xiàng)目命名成“注冊(cè)模塊demo”從零開(kāi)始定義接口這樣對(duì)接口結(jié)構(gòu)的印象更深刻。新建項(xiàng)目之后在左側(cè)“接口管理”里添加一個(gè)接口。關(guān)鍵配置如下請(qǐng)求方法POST路徑/api/register接口名稱(chēng)用戶(hù)注冊(cè)這里要多說(shuō)一句路徑的命名規(guī)范。很多新手隨手寫(xiě)一個(gè)/register就完事了但在真實(shí)項(xiàng)目里接口路徑通常會(huì)帶上前綴比如/api代表這是后端接口有的項(xiàng)目還會(huì)帶版本號(hào)/v1。這不是形式主義而是為了后續(xù)維護(hù)方便。前端代碼里寫(xiě)死路徑之后如果后端調(diào)整了前綴你只需要在axios的baseURL里統(tǒng)一改不用去頁(yè)面里一個(gè)個(gè)找這個(gè)設(shè)計(jì)習(xí)慣建議從一開(kāi)始就養(yǎng)成。HTTP方法的選擇也值得講清楚。GET和POST雖然都能把數(shù)據(jù)傳給服務(wù)器但語(yǔ)義完全不同。GET適合獲取數(shù)據(jù)參數(shù)拼在URL后面會(huì)被瀏覽器記錄進(jìn)歷史、被服務(wù)器記進(jìn)日志數(shù)據(jù)裸奔且長(zhǎng)度受限。POST適合提交數(shù)據(jù)參數(shù)放在請(qǐng)求體里相對(duì)隱蔽長(zhǎng)度限制也寬松很多。注冊(cè)這種場(chǎng)景用戶(hù)名、密碼、手機(jī)號(hào)都屬于敏感信息用POST是必然選擇。2.2 配置請(qǐng)求參數(shù)與Mock返回規(guī)則切換到“Body”標(biāo)簽頁(yè)選擇JSON格式定義請(qǐng)求體參數(shù)。我這次設(shè)計(jì)注冊(cè)功能需要三個(gè)核心字段{ username: zhangsan, password: abc123456, email: zhangsanexample.com }這個(gè)JSON結(jié)構(gòu)就是我們和“后端”約定的請(qǐng)求格式。注意字段風(fēng)格這里用的是小駝峰命名。曾經(jīng)有團(tuán)隊(duì)前端用user_name、后端用userName聯(lián)調(diào)時(shí)查了半天才發(fā)現(xiàn)是字段名對(duì)不上。所以字段命名雖然小事但最好在接口定義階段就統(tǒng)一好。返回?cái)?shù)據(jù)同樣用JSON定義。我計(jì)劃讓接口在成功時(shí)返回這樣的結(jié)構(gòu){ code: 200, message: 注冊(cè)成功, data: { userId: 1001 } }失敗時(shí)返回{ code: 400, message: 用戶(hù)名已存在, data: null }很多同學(xué)看到這里會(huì)問(wèn)直接用HTTP狀態(tài)碼不就行了為什么還要在JSON里放一個(gè)code字段這個(gè)問(wèn)題問(wèn)得很好。真實(shí)項(xiàng)目里確實(shí)存在這種雙軌并行的設(shè)計(jì)HTTP狀態(tài)碼是傳輸層的狀態(tài)標(biāo)識(shí)而業(yè)務(wù)code是業(yè)務(wù)邏輯的狀態(tài)標(biāo)識(shí)。比如接口通了但用戶(hù)名重復(fù)HTTP狀態(tài)碼可能是200請(qǐng)求本身成功了但業(yè)務(wù)層通過(guò)code400告訴你“注冊(cè)沒(méi)成功”。前端拿到響應(yīng)后不能只看HTTP狀態(tài)碼更要看業(yè)務(wù)code。這個(gè)習(xí)慣越早建立后面看真實(shí)接口越不吃力。apifox的Mock規(guī)則能幫你做一件事定義好返回?cái)?shù)據(jù)的格式后它會(huì)在你發(fā)請(qǐng)求時(shí)根據(jù)字段類(lèi)型自動(dòng)生成模擬數(shù)據(jù)。不一定需要手動(dòng)設(shè)置太復(fù)雜的動(dòng)態(tài)規(guī)則直接用靜態(tài)示例值也行夠前端聯(lián)調(diào)用了。2.3 本地Mock服務(wù)的啟動(dòng)與調(diào)試apifox有一個(gè)殺手級(jí)功能一鍵啟動(dòng)本地Mock服務(wù)。它會(huì)在你的電腦上開(kāi)一個(gè)本地服務(wù)地址類(lèi)似http://127.0.0.1:4523/m1/123456。這個(gè)地址就是我們可以直接請(qǐng)求的接口地址。為什么需要本地Mock服務(wù)而不是直接用apifox網(wǎng)頁(yè)提供的臨時(shí)Mock地址因?yàn)楸镜胤?wù)啟動(dòng)后前端axios請(qǐng)求本地地址不用走公網(wǎng)速度更快而且不會(huì)遇到公網(wǎng)Mock地址在某些網(wǎng)絡(luò)環(huán)境下的穩(wěn)定性問(wèn)題。調(diào)試體驗(yàn)和真實(shí)聯(lián)調(diào)非常接近。啟動(dòng)之后打開(kāi)瀏覽器訪問(wèn)一下接口地址或者直接在apifox里點(diǎn)“發(fā)送”就能看到模擬的返回結(jié)果。這個(gè)步驟很重要相當(dāng)于先確認(rèn)“假后端”沒(méi)死再去寫(xiě)前端代碼。我見(jiàn)過(guò)太多同學(xué)前端代碼寫(xiě)了一大堆回頭發(fā)現(xiàn)接口地址配錯(cuò)了排查了半天浪費(fèi)大量時(shí)間。另外apifox里還可以配置環(huán)境變量。比如你可以創(chuàng)建“本地環(huán)境”和“測(cè)試環(huán)境”兩組變量把不同的Mock地址分別存好。axios的baseURL讀取環(huán)境變量以后切換環(huán)境只需要在apifox里切換前端代碼完全不用動(dòng)。雖然是模擬階段但這套思路和真實(shí)項(xiàng)目里的環(huán)境管理是一脈相承的。3. 注冊(cè)頁(yè)面代碼實(shí)現(xiàn)寫(xiě)一個(gè)真正能用的表單3.1 HTML結(jié)構(gòu)設(shè)計(jì)頁(yè)面要簡(jiǎn)潔但簡(jiǎn)潔不等于簡(jiǎn)陋。設(shè)計(jì)目標(biāo)是信息清晰、操作路徑短、視覺(jué)干凈。表單包含四個(gè)元素用戶(hù)名輸入框、密碼輸入框、郵箱輸入框、注冊(cè)按鈕。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title用戶(hù)注冊(cè)/title link relstylesheet href./style.css /head body div classregister-container h2創(chuàng)建賬號(hào)/h2 p classsubtitle只需填寫(xiě)以下信息即可完成注冊(cè)/p form idregisterForm div classform-item label forusername用戶(hù)名/label input typetext idusername nameusername placeholder請(qǐng)輸入用戶(hù)名 autocompleteusername /div div classform-item label forpassword密碼/label input typepassword idpassword namepassword placeholder請(qǐng)輸入密碼 autocompletenew-password /div div classform-item label foremail郵箱/label input typeemail idemail nameemail placeholder請(qǐng)輸入郵箱 autocompleteemail /div button typesubmit idregisterBtn注 冊(cè)/button /form p idmessage classmessage/p /div script srchttps://cdn.jsdelivr.net/npm/axios1.6.0/dist/axios.min.js/script script src./register.js/script /body /html幾個(gè)細(xì)節(jié)說(shuō)說(shuō)為什么這么寫(xiě)。輸入框的autocomplete屬性容易被忽略。這個(gè)屬性控制瀏覽器要不要自動(dòng)填充歷史輸入值。注冊(cè)頁(yè)面的密碼框如果填了new-password瀏覽器就不會(huì)把之前登錄過(guò)的密碼自動(dòng)帶出來(lái)這對(duì)用戶(hù)來(lái)說(shuō)是正常體驗(yàn)。很多注冊(cè)頁(yè)密碼自動(dòng)被瀏覽器的老密碼覆蓋用戶(hù)自己都不知道輸了一堆以為是對(duì)的結(jié)果提交一直失敗這個(gè)坑就是沒(méi)設(shè)置autocomplete導(dǎo)致的。typeemail也是同理它不光是前端樣式上有區(qū)別移動(dòng)端會(huì)彈出對(duì)應(yīng)鍵盤(pán)瀏覽器還會(huì)做基礎(chǔ)格式校驗(yàn)。這個(gè)校驗(yàn)雖然在后端面前不值一提但對(duì)用戶(hù)體驗(yàn)來(lái)說(shuō)是一個(gè)低成本的正向反饋。form標(biāo)簽里沒(méi)有加action屬性也沒(méi)有method屬性。因?yàn)檫@次我們不打算走傳統(tǒng)表單的同步提交方式而是由JavaScript攔截submit事件用axios異步發(fā)送請(qǐng)求。如果這里寫(xiě)了actionhttp://127.0.0.1:4523/m1/xxx用戶(hù)點(diǎn)擊注冊(cè)按鈕后瀏覽器會(huì)直接跳轉(zhuǎn)到接口地址頁(yè)面刷新體驗(yàn)非常糟糕。所以明確一點(diǎn)使用axios異步請(qǐng)求時(shí)表單的同步提交行為要禁掉。3.2 CSS樣式與交互細(xì)節(jié)樣式不追求花哨但要有最基本的視覺(jué)反饋。我盡量控制在60行以?xún)?nèi)的核心樣式* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Microsoft YaHei, sans-serif; background: #f5f6fa; display: flex; justify-content: center; align-items: center; min-height: 100vh; } .register-container { background: #ffffff; padding: 40px 48px; border-radius: 12px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08); width: 400px; } .register-container h2 { font-size: 24px; color: #333333; text-align: center; margin-bottom: 6px; } .subtitle { text-align: center; color: #888888; font-size: 14px; margin-bottom: 28px; } .form-item { margin-bottom: 20px; } .form-item label { display: block; font-size: 14px; color: #555555; margin-bottom: 8px; font-weight: 500; } .form-item input { width: 100%; padding: 10px 14px; border: 1px solid #dddddd; border-radius: 8px; font-size: 14px; outline: none; transition: border-color 0.2s ease; } .form-item input:focus { border-color: #4a7aff; box-shadow: 0 0 0 3px rgba(74, 122, 255, 0.1); } #registerBtn { width: 100%; padding: 11px 0; border: none; border-radius: 8px; background: #4a7aff; color: #ffffff; font-size: 16px; font-weight: 500; cursor: pointer; transition: background-color 0.2s ease; margin-top: 8px; } #registerBtn:hover { background: #3867d6; } #registerBtn:disabled { background: #a0b4f0; cursor: not-allowed; } .message { margin-top: 16px; text-align: center; font-size: 14px; min-height: 20px; } .message.success { color: #07a35a; } .message.error { color: #e04b4b; }這里面有兩個(gè)交互細(xì)節(jié)值得展開(kāi)。輸入框的:focus狀態(tài)加了邊框變色和一圈淡藍(lán)色光暈這能直觀告訴用戶(hù)“當(dāng)前光標(biāo)在這個(gè)輸入框里”。沒(méi)有焦點(diǎn)反饋的表單用戶(hù)在填多個(gè)字段時(shí)很容易迷失尤其是一次性就要填好幾項(xiàng)信息的時(shí)候。按鈕的:disabled樣式對(duì)應(yīng)的是“防止重復(fù)提交”邏輯。用戶(hù)點(diǎn)了一次注冊(cè)后在請(qǐng)求返回之前按鈕會(huì)被置為禁用狀態(tài)等返回結(jié)果后再恢復(fù)。如果沒(méi)有這層控制手快的用戶(hù)連續(xù)點(diǎn)擊五六次頁(yè)面會(huì)發(fā)出好幾個(gè)相同的注冊(cè)請(qǐng)求后端可能創(chuàng)建多個(gè)賬號(hào)也可能因?yàn)椴l(fā)問(wèn)題報(bào)錯(cuò)。這個(gè)防抖操作是真實(shí)項(xiàng)目里前端必做的基本功。3.3 前端校驗(yàn)規(guī)則前端校驗(yàn)的作用是提前攔截明顯不合理的輸入讓用戶(hù)立刻改正而不是等到請(qǐng)求發(fā)出去再等后端打回來(lái)。這次設(shè)計(jì)三個(gè)字段的校驗(yàn)規(guī)則用戶(hù)名2到16個(gè)字符不能包含特殊字符。密碼6到20個(gè)字符必須同時(shí)包含字母和數(shù)字。郵箱符合基本的郵箱格式。function validateForm(formData) { const username formData.get(username).trim(); const password formData.get(password).trim(); const email formData.get(email).trim(); if (username.length 2 || username.length 16) { return 用戶(hù)名長(zhǎng)度應(yīng)在2到16個(gè)字符之間; } if (!/^[a-zA-Z0-9_\u4e00-\u9fa5]$/.test(username)) { return 用戶(hù)名只能包含字母、數(shù)字、下劃線或中文; } if (password.length 6 || password.length 20) { return 密碼長(zhǎng)度應(yīng)在6到20個(gè)字符之間; } if (!/[a-zA-Z]/.test(password) || !/[0-9]/.test(password)) { return 密碼必須同時(shí)包含字母和數(shù)字; } if (!/^[^\s][^\s]\.[^\s]$/.test(email)) { return 郵箱格式不正確; } return null; }用正則做用戶(hù)名和密碼校驗(yàn)是效率最高的方式。密碼復(fù)雜度要求聽(tīng)起來(lái)簡(jiǎn)單但用正則實(shí)現(xiàn)時(shí)一定要拆開(kāi)看先校驗(yàn)長(zhǎng)度再分別判斷是否包含字母和數(shù)字而不是用一個(gè)大正則一步到位。大正則看著唬人但報(bào)錯(cuò)信息沒(méi)法細(xì)致區(qū)分“長(zhǎng)度不夠”和“缺數(shù)字”而在實(shí)際使用中用戶(hù)最需要的是明確的提示哪個(gè)規(guī)則沒(méi)滿(mǎn)足你直接告訴他就行。順便說(shuō)一句正則這事。很多人一看到正則就頭疼覺(jué)得難記。我的經(jīng)驗(yàn)是把常用的幾個(gè)正則存成自己的代碼片段庫(kù)手機(jī)號(hào)、郵箱、身份證、純數(shù)字、純字母以后哪里用到直接復(fù)制再微調(diào)??磕X子硬背正則性?xún)r(jià)比太低。3.4 axios封裝與請(qǐng)求配置axios的使用分兩種層次一種是直接在頁(yè)面里axios.post(url, data)一把梭另一種是封裝一個(gè)實(shí)例統(tǒng)一配置baseURL、超時(shí)時(shí)間、攔截器。這個(gè)項(xiàng)目雖然只有一個(gè)接口我更建議直接按第二種方式來(lái)寫(xiě)因?yàn)橐院蠼诱鎸?shí)項(xiàng)目的時(shí)候無(wú)非就是把Mock地址換成線上地址其余代碼幾乎不用改。先看封裝部分的代碼const request axios.create({ baseURL: http://127.0.0.1:4523/m1/3956000, timeout: 10000, headers: { Content-Type: application/json } }); request.interceptors.request.use(config { console.log(請(qǐng)求發(fā)出, config); return config; }, error { return Promise.reject(error); }); request.interceptors.response.use(response { const res response.data; if (res.code ! 200) { return Promise.reject(new Error(res.message || 請(qǐng)求失敗)); } return res; }, error { return Promise.reject(error); });這里要重點(diǎn)說(shuō)兩個(gè)概念。baseURL的作用是給所有請(qǐng)求路徑加前綴。后面發(fā)起request.post(/api/register)的時(shí)候?qū)嶋H請(qǐng)求的完整地址就是http://127.0.0.1:4523/m1/3956000/api/register。這樣做的好處是當(dāng)接口地址整體遷移時(shí)只改這一個(gè)變量就行。開(kāi)發(fā)環(huán)境、測(cè)試環(huán)境、生產(chǎn)環(huán)境的baseURL不同通過(guò)環(huán)境變量動(dòng)態(tài)切換即可不用動(dòng)業(yè)務(wù)代碼。headers里的Content-Type決定了請(qǐng)求體以什么格式傳輸。這里用的是application/json對(duì)應(yīng)的是axios把JS對(duì)象轉(zhuǎn)換成JSON字符串放在請(qǐng)求體里。還有一種常見(jiàn)格式是application/x-www-form-urlencoded那是把數(shù)據(jù)編碼成usernamexxxpasswordxxx的鍵值對(duì)形式。這兩種格式各有用武之地但在RESTful API設(shè)計(jì)里JSON是默認(rèn)選擇。因?yàn)镴SON結(jié)構(gòu)清晰、支持嵌套后端解析也方便。如果你用axios默認(rèn)的POST請(qǐng)求且傳入的是對(duì)象axios會(huì)自動(dòng)幫你轉(zhuǎn)成JSON格式但你最好還是顯式聲明一下避免有些環(huán)境下因?yàn)闆](méi)有正確設(shè)置Content-Type導(dǎo)致后端拿到的是空數(shù)據(jù)。再看看注冊(cè)頁(yè)面自己的邏輯層const form document.getElementById(registerForm); const messageEl document.getElementById(message); const registerBtn document.getElementById(registerBtn); form.addEventListener(submit, async function (event) { event.preventDefault(); const formData new FormData(form); const formDataObj { username: formData.get(username).trim(), password: formData.get(password).trim(), email: formData.get(email).trim() }; const validateMsg validateForm(formData); if (validateMsg) { showMessage(validateMsg, error); return; } registerBtn.disabled true; registerBtn.textContent 注冊(cè)中...; try { const res await request.post(/api/register, formDataObj); if (res.code 200) { showMessage(注冊(cè)成功, success); } } catch (err) { showMessage(err.message || 網(wǎng)絡(luò)異常請(qǐng)稍后重試, error); } finally { registerBtn.disabled false; registerBtn.textContent 注 冊(cè); } }); function showMessage(text, type) { messageEl.textContent text; messageEl.className message type; }這里有幾個(gè)關(guān)鍵點(diǎn)值得反復(fù)強(qiáng)調(diào)。event.preventDefault()是表單處理里最容易漏的一行代碼。表單默認(rèn)行為是提交后刷新頁(yè)面如果你沒(méi)攔住這個(gè)默認(rèn)行為axios的異步請(qǐng)求剛發(fā)出去頁(yè)面立刻刷新了結(jié)果什么都看不到。很多新手排了很久的錯(cuò)最后發(fā)現(xiàn)就是少了這一行。按鈕的狀態(tài)切換放在請(qǐng)求前和請(qǐng)求后。這不是可有可無(wú)的錦上添花而是絕對(duì)必要的交互控制。請(qǐng)求期間用戶(hù)連續(xù)點(diǎn)擊會(huì)產(chǎn)生重復(fù)注冊(cè)的請(qǐng)求輕則后端多創(chuàng)建賬號(hào)重則觸發(fā)并發(fā)沖突。用一個(gè)簡(jiǎn)單的disabled狀態(tài)就能在純前端層面把這個(gè)風(fēng)險(xiǎn)降到最低。try...catch...finally的結(jié)構(gòu)是異步編程的黃金組合。try里放正常請(qǐng)求流程catch捕獲請(qǐng)求失敗或業(yè)務(wù)失敗finally保證無(wú)論結(jié)果如何都要恢復(fù)按鈕狀態(tài)。還有一點(diǎn)攔截器里已經(jīng)把code ! 200的情況通過(guò)Promise.reject拋出來(lái)了所以在catch里拿到的err.message就是后端返回的業(yè)務(wù)錯(cuò)誤信息用戶(hù)能看到“用戶(hù)名已存在”這樣有意義的提示而不是冷冰冰的“Network Error”。4. 聯(lián)調(diào)實(shí)戰(zhàn)從頁(yè)面到接口的完整請(qǐng)求鏈路4.1 axios實(shí)例配置與POST調(diào)用實(shí)戰(zhàn)到這里前端的靜態(tài)頁(yè)面和apifox里的假后端都已經(jīng)就緒了。打開(kāi)HTML頁(yè)面填入信息點(diǎn)擊注冊(cè)。這瞬間發(fā)生了什么我按時(shí)間線拆解瀏覽器攔截表單默認(rèn)提交行為。執(zhí)行前端校驗(yàn)函數(shù)通過(guò)后繼續(xù)。axios實(shí)例創(chuàng)建POST請(qǐng)求URL拼接為http://127.0.0.1:4523/m1/3956000/api/register。設(shè)置請(qǐng)求頭Content-Type: application/json。請(qǐng)求體序列化為{username:zhangsan,password:abc123456,email:zhangsanexample.com}。瀏覽器發(fā)送請(qǐng)求到本地Mock服務(wù)。apifox服務(wù)端匹配到接口定義執(zhí)行數(shù)據(jù)校驗(yàn)。返回JSON響應(yīng)到前端。axios響應(yīng)攔截器拿到數(shù)據(jù)判斷業(yè)務(wù)code。頁(yè)面根據(jù)結(jié)果展示成功或失敗信息。這條鏈路里每一步的職責(zé)都很單一任何一個(gè)環(huán)節(jié)出問(wèn)題都有明確的排查方向。比如前端校驗(yàn)沒(méi)通過(guò)請(qǐng)求根本不會(huì)發(fā)出如果請(qǐng)求發(fā)出去了但沒(méi)返回問(wèn)題在接口或網(wǎng)絡(luò)層如果返回了但頁(yè)面沒(méi)反應(yīng)問(wèn)題在響應(yīng)處理邏輯。4.2 前端收到的響應(yīng)長(zhǎng)什么樣用瀏覽器的開(kāi)發(fā)者工具切到Network標(biāo)簽頁(yè)點(diǎn)一下注冊(cè)按鈕能看到一條名為register的請(qǐng)求記錄。點(diǎn)開(kāi)它可以看到請(qǐng)求詳情Headers里就是Content-Type、User-Agent這些Payload里就是請(qǐng)求體JSONResponse里就是接口返回的JSON。這一步建議每個(gè)讀者都親自做一次親眼看一遍比看十篇教程都管用。響應(yīng)體長(zhǎng)這樣{ code: 200, message: 注冊(cè)成功, data: { userId: 1001 } }前端拿到這個(gè)JSON后響應(yīng)攔截器判斷code 200于是整個(gè)請(qǐng)求流程走成功分支頁(yè)面顯示綠色“注冊(cè)成功”文案。如果不滿(mǎn)足注冊(cè)條件比如用戶(hù)名已經(jīng)存在apifox按Mock規(guī)則返回code: 400響應(yīng)攔截器發(fā)現(xiàn)業(yè)務(wù)code不對(duì)直接reject一個(gè)Error錯(cuò)誤信息被catch捕獲頁(yè)面顯示紅色錯(cuò)誤提示。這個(gè)過(guò)程沒(méi)有刷新頁(yè)面但有完整的成功和失敗反饋這就是前后端分離結(jié)構(gòu)下典型的交互方式。4.3 調(diào)試過(guò)程中網(wǎng)絡(luò)面板的使用Network面板是聯(lián)調(diào)時(shí)最重要的工具沒(méi)有之一。我來(lái)說(shuō)說(shuō)怎么看它。打開(kāi)面板后先清空之前的日志再操作頁(yè)面這樣面板里只保留你這次操作產(chǎn)生的請(qǐng)求線索不會(huì)被歷史請(qǐng)求干擾。然后看三條信息Status CodeHTTP狀態(tài)碼200代表請(qǐng)求到達(dá)了接口并正常返回404是路徑?jīng)]匹配上500是服務(wù)端內(nèi)部出錯(cuò)了。Request Payload確認(rèn)請(qǐng)求體里的數(shù)據(jù)格式和值很多聯(lián)調(diào)問(wèn)題都出在這一步比如字段名拼錯(cuò)了、傳了undefined。Response確認(rèn)返回?cái)?shù)據(jù)和預(yù)期是否一致。有一類(lèi)很詭異的問(wèn)題頁(yè)面明明顯示請(qǐng)求失敗了但面板里Response看起來(lái)是正常的。這種情況十有八九是響應(yīng)攔截器里做了特殊判斷把業(yè)務(wù)code非200的情況當(dāng)成了錯(cuò)誤拋出。所以看問(wèn)題要把前后端連起來(lái)看不能只看一頭。5. 踩坑記錄與排查思路合集5.1 請(qǐng)求能發(fā)出但Status Code是404404是路徑問(wèn)題。在Network面板里看請(qǐng)求URL和apifox接口定義里的路徑比對(duì)是不是漏了/api前綴或者是mock服務(wù)地址的路徑段不對(duì)。apifox本地服務(wù)的完整路徑包含項(xiàng)目標(biāo)識(shí)如果復(fù)制的時(shí)候漏掉了一段就會(huì)出現(xiàn)404。這里我建議養(yǎng)成一個(gè)習(xí)慣字不要手打全部從apifox里復(fù)制。人眼識(shí)別地址里的數(shù)字段很容易出錯(cuò)復(fù)制粘貼能規(guī)避大部分這類(lèi)低級(jí)問(wèn)題。5.2 請(qǐng)求發(fā)出后Response是CORS error跨域問(wèn)題是本地聯(lián)調(diào)最常見(jiàn)的一道坎。頁(yè)面文件在file://協(xié)議下打開(kāi)或者前端用的是某個(gè)本地端口而接口服務(wù)在另一個(gè)端口瀏覽器的同源策略就會(huì)攔截響應(yīng)。解決辦法有兩個(gè)方向。一是在前端啟動(dòng)一個(gè)本地開(kāi)發(fā)服務(wù)器比如用VSCode的Live Server插件把頁(yè)面跑起來(lái)這樣前端來(lái)自http://127.0.0.1:5500請(qǐng)求http://127.0.0.1:4523就屬于跨域。二是去apifox的Mock服務(wù)設(shè)置里找到CORS相關(guān)配置把允許跨域打開(kāi)。需要注意的是如果你直接雙擊HTML文件用file://方式打開(kāi)NetWork面板里request可能顯示為(cors)或(failed)這是瀏覽器的硬限制。我個(gè)人的習(xí)慣是所有前端聯(lián)調(diào)一律用Live Server這種本地服務(wù)方式跑起來(lái)和線上環(huán)境更接近也少踩很多莫名其妙的錯(cuò)。5.3 apifox發(fā)送成功但前端一直請(qǐng)求失敗可以先確認(rèn)一下apifox里發(fā)送測(cè)試是否正常。如果apifox本身能正常返回說(shuō)明接口定義沒(méi)問(wèn)題問(wèn)題出在前端到接口之間的鏈路上。常見(jiàn)情況是你用的是apifox云端Mock地址但這個(gè)接口默認(rèn)需要鑒權(quán)沒(méi)有帶token所以被拒了。我剛才寫(xiě)的代碼里就沒(méi)有處理鑒權(quán)所以這里有個(gè)重要提醒如果apifox創(chuàng)建的項(xiàng)目開(kāi)啟了“接口鑒權(quán)”功能前端請(qǐng)求會(huì)收到401或403。解決方式是在apifox的項(xiàng)目設(shè)置里關(guān)閉鑒權(quán)或者在前端請(qǐng)求中加上對(duì)應(yīng)的鑒權(quán)頭。初學(xué)者做模擬聯(lián)調(diào)建議直接關(guān)掉鑒權(quán)避免在非核心問(wèn)題上耗費(fèi)時(shí)間。5.4 表單重置問(wèn)題注冊(cè)成功后表單數(shù)據(jù)沒(méi)有清空這雖然不影響功能但體驗(yàn)上差了點(diǎn)。真實(shí)項(xiàng)目里注冊(cè)成功后一般會(huì)跳轉(zhuǎn)到登錄頁(yè)或首頁(yè)所以表單清不清空影響不大。但如果你做了一個(gè)單頁(yè)demo希望在注冊(cè)成功后清空表單只需要在成功分支里調(diào)用form.reset()就行。放在成功分支而不是finally里是怕請(qǐng)求失敗時(shí)把用戶(hù)填好的信息清掉那就得不償失了。5.5 字段名或數(shù)據(jù)類(lèi)型不一致前端傳字符串后端要數(shù)字或者前端傳userName后端要username。這種問(wèn)題在聯(lián)調(diào)里極其常見(jiàn)有時(shí)候能折磨人一下午。規(guī)避的辦法就是在apifox定義接口時(shí)把字段名和類(lèi)型固定好前端代碼嚴(yán)格對(duì)照接口文檔來(lái)寫(xiě)。記住apifox里定義的請(qǐng)求參數(shù)就是事實(shí)標(biāo)準(zhǔn)前端照著抄不要發(fā)揮創(chuàng)造力。我見(jiàn)過(guò)有同學(xué)把email寫(xiě)成mail接口一下子返回參數(shù)缺失這就是典型的低級(jí)錯(cuò)誤但也只有靠細(xì)心才能完全避免。6. 進(jìn)階擴(kuò)展注冊(cè)頁(yè)面還能怎么變得更強(qiáng)6.1 添加Loading狀態(tài)與按鈕防重復(fù)提交按鈕防重復(fù)提交我在上面的代碼里已經(jīng)寫(xiě)了但如果你追求更好一點(diǎn)的體驗(yàn)還可以把按鈕上的文字改成帶動(dòng)畫(huà)的加載狀態(tài)。最簡(jiǎn)單的做法是給按鈕加一個(gè)CSS類(lèi)里面放一段旋轉(zhuǎn)的邊框動(dòng)畫(huà)視覺(jué)上告訴用戶(hù)“請(qǐng)求正在路上”。還有一種更細(xì)膩的狀態(tài)處理根據(jù)請(qǐng)求結(jié)果給按鈕設(shè)置不同的文案。請(qǐng)求中顯示“提交中...”成功后可以短暫顯示“注冊(cè)成功”再跳轉(zhuǎn)失敗則恢復(fù)原樣。這些細(xì)節(jié)單獨(dú)看不值錢(qián)但組合起來(lái)用戶(hù)的感受完全是兩個(gè)檔次。6.2 密碼加密與安全策略我在demo里直接明文傳輸密碼這對(duì)模擬項(xiàng)目沒(méi)有影響但到了真實(shí)項(xiàng)目里是絕對(duì)不行的。前端在發(fā)送密碼之前至少要用HTTPS保證傳輸安全更進(jìn)一步可以在前端對(duì)密碼做哈希處理后再發(fā)送。當(dāng)然哈希算法有很多細(xì)節(jié)什么加鹽、什么算法選擇屬于安全領(lǐng)域的專(zhuān)門(mén)知識(shí)這里提出來(lái)是希望大家有這個(gè)意識(shí)不要以為JSON提交就萬(wàn)事大吉了。實(shí)際項(xiàng)目中前端做哈希處理是一種常見(jiàn)做法但要注意后端必須明確知道前端傳過(guò)來(lái)的是哈希值還是原文要有對(duì)應(yīng)的方案來(lái)配合處理避免前后端各自為政最后兩邊擰巴。6.3 增加圖形驗(yàn)證碼這是現(xiàn)實(shí)中注冊(cè)功能幾乎必備的環(huán)節(jié)目的是防止機(jī)器人批量注冊(cè)。圖形驗(yàn)證碼的實(shí)現(xiàn)牽扯到后端生成圖片、前端顯示圖片、校驗(yàn)邏輯等好幾個(gè)環(huán)節(jié)代碼量會(huì)成倍增加。用apifox模擬圖形驗(yàn)證碼接口可以定義兩個(gè)接口一個(gè)獲取驗(yàn)證碼圖片一個(gè)校驗(yàn)驗(yàn)證碼。前端把用戶(hù)填的驗(yàn)證碼和注冊(cè)信息一起提交由“后端”驗(yàn)證。這個(gè)擴(kuò)展非常推薦在掌握基本注冊(cè)流程后再做因?yàn)樗牧鞒谈暾哺咏鎸?shí)業(yè)務(wù)場(chǎng)景。6.4 表單狀態(tài)管理如果使用了Vue或React注冊(cè)表單的狀態(tài)管理可以更進(jìn)一步。以Vue為例用reactive定義表單數(shù)據(jù)watch監(jiān)聽(tīng)字段變化實(shí)時(shí)校驗(yàn)computed根據(jù)表單整體合法性控制按鈕可用狀態(tài)。這套組合拳能讓交互反饋更即時(shí)用戶(hù)不用等到點(diǎn)擊按鈕才知道哪里填錯(cuò)了。不過(guò)我的建議是用原生JavaScript完整實(shí)現(xiàn)一遍注冊(cè)頁(yè)之前不要急著上框架。原生的實(shí)現(xiàn)過(guò)程會(huì)把請(qǐng)求鏈路、DOM操作、事件處理的每個(gè)細(xì)節(jié)都暴露在你面前這些經(jīng)驗(yàn)在框架里會(huì)被隱藏得很好但對(duì)理解系統(tǒng)本質(zhì)極為關(guān)鍵。寫(xiě)在最后注冊(cè)頁(yè)面看起來(lái)是前端入門(mén)的小項(xiàng)目但它的價(jià)值在于把“用戶(hù)交互、前端校驗(yàn)、網(wǎng)絡(luò)請(qǐng)求、接口模擬、異常處理”這條完整的鏈路串了起來(lái)。我個(gè)人折騰這個(gè)demo時(shí)最大的體會(huì)是其實(shí)難的不是某個(gè)具體技術(shù)點(diǎn)而是搞清楚“誰(shuí)負(fù)責(zé)什么、數(shù)據(jù)從哪來(lái)、到哪里去、每一步失敗在哪里”。當(dāng)你用apifool把后端接口提前定義好再回頭看前端代碼你會(huì)突然發(fā)現(xiàn)請(qǐng)求邏輯異常透明——這大概就是接口先行的魅力。最后再分享一個(gè)小習(xí)慣每次調(diào)試請(qǐng)求時(shí)先在apifox里確認(rèn)接口返回正常再去頁(yè)面上操作。如果頁(yè)面報(bào)錯(cuò)打開(kāi)Network面板看請(qǐng)求URL、狀態(tài)碼和載荷。按這個(gè)順序排查百分之八十的問(wèn)題五分鐘內(nèi)都能定位到。