溫故而知新,本文為一時(shí)興起寫出,如有錯(cuò)誤還請指正
本文后臺(tái)基于SpringBoot2.5.6編寫,前端基于Vue2 + axios和微信小程序JS版分別編寫進(jìn)行聯(lián)調(diào)測試,用于理解前后端分離式開發(fā)的交互流程,如果沒用過axios可以點(diǎn)我看之前的帖子
如果你沒有學(xué)過SpringBoot也不要緊,把他看做成SpringMVC即可,寫法完全一致(其實(shí)我不說你也發(fā)現(xiàn)不了)
本文主要講前后端交互流程,力求幫助新人快速入門前后端分離式開發(fā),不會(huì)講關(guān)于環(huán)境搭建部分的內(nèi)容
SpringMVC接收參數(shù)的方式在文章開頭快速的過一遍SpringMVC接收參數(shù)的幾種方式,一定要記住這幾種方式,看不懂或不理解都沒關(guān)系,后續(xù)會(huì)結(jié)合前端代碼過一遍,這里就不過多解釋了,直接上代碼
1.【正常接收參數(shù)】
/** * 正常接收參數(shù) * 注意:本Controller為了演示同時(shí)寫了多個(gè)路徑相同的GetMapping,不要直接復(fù)制,啟動(dòng)會(huì)報(bào)錯(cuò) */@RestControllerpublic class IndexController { /** 通過變量接收參數(shù) */ @GetMapping("/index") public String index(String username, String password) { System.out.println(username); System.out.println(password); return "index"; } /** 通過實(shí)體類接收參數(shù) */ @GetMapping("/index") public String index(UserEntity userEntity) { System.out.println(userEntity.getUsername()); System.out.println(userEntity.getPassword()); return "index"; } /** 通過Map集合接收參數(shù) */ @GetMapping("/index") public String index(Map<String, Object> param) { System.out.println(param.get("username")); System.out.println(param.get("password")); return "index"; } /** 通過基于HTTP協(xié)議的Servlet請求對象中獲取參數(shù) */ @GetMapping("/index") public String index(HttpServletRequest req) { System.out.println(req.getParameter("username")); System.out.println(req.getParameter("password")); return "index"; } /** 變量接收參數(shù)還可以使用@RequestParam完成額外操作 */ @GetMapping("/index") public String index(@RequestParam(value = "username", required = true, defaultValue = "zhang") String username) { System.out.println(username); return "index"; }}2.【路徑占位接收參數(shù)】
/** * 路徑占位接收參數(shù),參數(shù)作為請求路徑的一部分,使用{}作為占位符 */@RestControllerpublic class IndexController { /** 路徑占位接收參數(shù),名稱相同 */ @GetMapping("/user/{id}") public String index(@PathVariable Integer id) { System.out.println(id); return "index"; } /** 路徑占位接收參數(shù),名稱不同 */ @GetMapping("/user/{id}") public String index(@PathVariable("id") Long userId) { System.out.println(userId); return "index"; }}3.【請求體接收參數(shù)】
/** * 如果請求參數(shù)在請求體中,需要使用@RequestBody取出請求體中的值 */@RestControllerpublic class IndexController { /** 使用實(shí)體類接收參數(shù) */ @GetMapping("/index") public String index(@RequestBody UserEntity userEntity) { System.out.println(userEntity.getUsername()); System.out.println(userEntity.getPassword()); return "index"; } /** 使用Map集合接收參數(shù) */ @GetMapping("/index") public String index(@RequestBody Map<String, Object> param) { System.out.println(param.get("username")); System.out.println(param.get("password")); return "index"; } /** 變量接收參數(shù) */ @GetMapping("/index") public String index(@RequestBody String username) { System.out.println(username); return "index"; }}細(xì)心的人應(yīng)該留意到了,最后使用變量接收參數(shù)的時(shí)候只接收了username這一個(gè)值,并沒有接收password,作為擴(kuò)展在這里解釋一下,不看也可以,看了不理解也沒關(guān)系,知道這個(gè)事兒就夠了,以后接觸多了就理解了
如果請求參數(shù)放在了請求體中,只有參數(shù)列表第一個(gè)變量能接收到值,這里需要站在Servlet的角度來看:
/** 通過基于HTTP協(xié)議的Servlet請求對象獲取請求體內(nèi)容 */@GetMapping("/index")public String index(HttpServletRequest req) { ServletInputStream inputStream = req.getInputStream(); return "index";}可以看到請求體內(nèi)容是存到了InputStream輸入流對象中,想要知道請求體中的內(nèi)容是什么必須讀流中的數(shù)據(jù),讀取到數(shù)據(jù)后會(huì)將值給第一個(gè)變量,而流中的數(shù)據(jù)讀取一次之后就沒了,當(dāng)?shù)诙€(gè)變量讀流時(shí)發(fā)現(xiàn)流已經(jīng)被關(guān)閉了,自然就接收不到
前后端分離式交互流程SpringMVC回顧到此為止,只需要記住那三種方式即可,在前后端交互之前先在Controller中寫個(gè)測試接口
@RestControllerpublic class IndexController { @GetMapping("/index") public Map<String, Object> index() { // 創(chuàng)建map集合對象,添加一些假數(shù)據(jù)并返回給前端 HashMap<String, Object> result = new HashMap<>(); result.put("user", "zhang"); result.put("name", "hanzhe"); result.put("arr", new int[]{1, 2, 3, 4, 5, 6}); // 返回?cái)?shù)據(jù)給前端 return result; }}這個(gè)接口對應(yīng)的是GET類型的請求,這里直接在瀏覽器地址欄訪問測試一下:
這里推薦一個(gè)Chrome瀏覽器的插件JSONView,它可以對瀏覽器顯示的JSON數(shù)據(jù)進(jìn)行格式化顯示,推薦的同時(shí)也提個(gè)醒,安裝需謹(jǐn)慎,如果JSON數(shù)據(jù)量太大的話頁面會(huì)很卡
跨域請求之前已經(jīng)寫好一個(gè)GET請求的測試接口了,這里就在前端寫代碼訪問一下試試看
VUE請求代碼
<template> <!-- 我這里為了看著好看(心情好點(diǎn)),引用了ElementUI --> <el-button-group> <el-button type="primary" size="small" @click="request1">發(fā)起普通請求</el-button> </el-button-group></template><script>export default { methods: { request1() { // 通過axios發(fā)起一個(gè)GET請求 this.axios.get("http://localhost:8080/index").then(res => { // 打印接口返回的結(jié)果 console.log("res", res); }); } }};</script>代碼已經(jīng)寫完了,接下來打開頁面試一下能不能調(diào)通:
可以看到請求代碼報(bào)錯(cuò)了,查看報(bào)錯(cuò)信息找到重點(diǎn)關(guān)鍵詞CORS,表示該請求屬于跨域請求
認(rèn)識(shí)跨域請求
什么是跨域請求?跨域請求主要體現(xiàn)在跨域兩個(gè)字上,當(dāng)發(fā)起請求的客戶端和接收請求的服務(wù)端他們的【協(xié)議、域名、端口號(hào)】有任意一項(xiàng)不一致的情況都屬于跨域請求,拿剛剛訪問的地址舉例,VUE頁面運(yùn)行在9000端口上,后臺(tái)接口運(yùn)行在8080端口上,端口號(hào)沒有對上所以該請求為跨域請求
處理跨域請求
如果在調(diào)試的時(shí)候仔細(xì)一點(diǎn)就會(huì)發(fā)現(xiàn),雖然前端提示請求報(bào)錯(cuò)了,但是后端還是接收到請求了,那為什么會(huì)報(bào)錯(cuò)呢?是因?yàn)楹蠖朔祷財(cái)?shù)據(jù)后,瀏覽器接收到響應(yīng)結(jié)果發(fā)現(xiàn)該請求跨域,然后給我們提示錯(cuò)誤信息,也就是說問題在瀏覽器這里
怎樣才能讓瀏覽器允許該請求呢?我們需要在后端動(dòng)點(diǎn)手腳,在返回結(jié)果的時(shí)候設(shè)置允許前端訪問即可
首先配置一個(gè)過濾器,配置過濾器有很多種實(shí)現(xiàn)的方法,我這里是實(shí)現(xiàn)Filter接口
@Componentpublic class CorsFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { // 將response響應(yīng)轉(zhuǎn)換為基于HTTP協(xié)議的響應(yīng)對象 HttpServletResponse resp = (HttpServletResponse) servletResponse; // 這個(gè)方法是必須調(diào)用的,不做解釋 filterChain.doFilter(servletRequest, resp); } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void destroy() { }}過濾器創(chuàng)建完成了,回來看前端提示的報(bào)錯(cuò)信息為Access-Control-Allow-Origin,意思是允許訪問的地址中并不包含當(dāng)前VUE的地址,那么我們就在響應(yīng)結(jié)果時(shí)將VUE的地址追加上
@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { // 將response響應(yīng)轉(zhuǎn)換為基于HTTP協(xié)議的響應(yīng)對象 HttpServletResponse resp = (HttpServletResponse) servletResponse; // 在允許請求的地址列表中添加VUE的地址 resp.addHeader("Access-Control-Allow-Origin", "http://localhost:9000"); // 這個(gè)方法是必須調(diào)用的,不做解釋 filterChain.doFilter(servletRequest, resp);}添加完成后重啟項(xiàng)目后臺(tái)就會(huì)發(fā)現(xiàn)請求已經(jīng)成功并且拿到了返回值
再次進(jìn)行測試,將后臺(tái)的GetMapping修改為PostMapping,修改前端請求代碼后重新發(fā)起請求進(jìn)行測試
可以看到POST請求還是提示跨域請求,對應(yīng)的錯(cuò)誤信息則是Access-Control-Allow-Headers,也就是說請求頭中包含了不被允許的信息,這里圖省事兒用*通配符把所有請求頭都放行
@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { // 將response響應(yīng)轉(zhuǎn)換為基于HTTP協(xié)議的響應(yīng)對象 HttpServletResponse resp = (HttpServletResponse) servletResponse; // 后臺(tái)接口除了VUE訪問之外微信小程序也會(huì)訪問,這里使用通配符替換 resp.addHeader("Access-Control-Allow-Origin", "*"); // 這里圖省事也允許所有請求頭訪問 resp.addHeader("Access-Control-Allow-Headers", "*"); // 這個(gè)方法是必須調(diào)用的,不做解釋 filterChain.doFilter(servletRequest, resp);}這樣處理之后,請求就可以正常訪問啦
傳參-路徑占位參數(shù)路徑占位參數(shù),就是將參數(shù)作為請求路徑的一部分,例如你現(xiàn)在正在看的這篇博客使用的就是路徑占位傳參
這種傳參方法很簡單,就不細(xì)講了,可以效仿他這種方法寫個(gè)測試案例
后臺(tái)接口的編寫
@RestControllerpublic class IndexController { // 路徑中包含user和blogId兩個(gè)占位參數(shù) @GetMapping("/{user}/p/{blogId}.html") public Map<String, Object> index(@PathVariable String user, @PathVariable Long blogId) { // 將接收的參數(shù)返回給前端 HashMap<String, Object> result = new HashMap<>(); result.put("user", user); result.put("blogId", blogId); return result; }}VUE請求代碼
request1() { this.axios.get("http://localhost:8080/hanzhe/p/11223344.html", this.config).then(res => { console.log("res", res); });}小程序請求代碼
request1() { wx.request({ // url:請求的目標(biāo)地址 url: 'http://localhost:8080/hanzhe/p/223344.html', // success:請求成功后執(zhí)行的方法 success: res => { console.log(res); } })}傳參-路徑參數(shù)這里需要注意區(qū)分【路徑占位傳參】和【路徑傳參】兩個(gè)概念,不要記混
什么是路徑傳參?發(fā)起一個(gè)請求http://localhost:8080/index?a=1&b=2,在路徑?后面的都屬于路徑傳參,路徑傳參就是將參數(shù)以明文方式拼接在請求地址后面
路徑傳參使用【正常接收參數(shù)】中的實(shí)例代碼即可接收到值
后臺(tái)接口的編寫
@RestControllerpublic class IndexController { @GetMapping("/index") public Map<String, Object> index(String user, String name) { // 將接收的參數(shù)返回給前端 HashMap<String, Object> result = new HashMap<>(); result.put("user", user); result.put("name", name); return result; }}VUE代碼
除了自己手動(dòng)拼接請求參數(shù)之外,axios在config中提供了params屬性,也可以實(shí)現(xiàn)該功能
// 正常拼接request1() { this.axios.get("http://localhost:8080/index?user=zhang&name=hanzhe").then(res => { console.log("res", res); });},// 使用config中的params屬性進(jìn)行路徑傳參request2() { let config = { params: { user: "zhang", name: "hanzhe" } } this.axios.get("http://localhost:8080/index", config).then(res => { console.log("res", res); });}小程序代碼
// 正常拼接request1() { wx.request({ url: 'http://localhost:8080/index?user=zhang&name=hanzhe', success: res => { console.log(res); } })},// 將請求類型設(shè)置為GET,wx識(shí)別后會(huì)將data轉(zhuǎn)換為路徑傳參request2() { wx.request({ url: 'http://localhost:8080/index', method: "GET", data: { user: "zhang", name: "hanzhe" }, success: res => { console.log(res); } })}傳參-表單類型參數(shù)表單類型參數(shù),就是通過form表單提交的參數(shù),通常用在例如HTML、JSP頁面的form標(biāo)簽上,但如果是前后端分離的話就不能使用form表單提交了,這里可以手動(dòng)創(chuàng)建表單對象進(jìn)行傳值
需要注意,GET請求一般只用于路徑傳參,其他類型傳參需要使用POST或其他類型的請求
表單類型參數(shù)也是【正常接收參數(shù)】中的實(shí)例代碼接收值
后臺(tái)接口的編寫
@RestControllerpublic class IndexController { @PostMapping("/index") public Map<String, Object> index(String username, String password) { // 將接收的參數(shù)返回給前端 HashMap<String, Object> result = new HashMap<>(); result.put("username", username); result.put("password", password); return result; }}VUE代碼
request1() { // 構(gòu)建表單對象,向表單中追加參數(shù) let data = new FormData(); data.append("username", "123"); data.append("password", "456"); // 發(fā)起請求 this.axios.post("http://localhost:8080/index", data).then(res => { console.log("res", res); });},小程序代碼
小程序刪除了FormData對象,不能發(fā)起表單類型參數(shù)的請求,如果非要寫的話可以試著使用wx.uploadFile實(shí)現(xiàn),這里就不嘗試了
傳參-請求體參數(shù)請求體傳參,是在發(fā)起請求時(shí)將參數(shù)放在請求體中
表單類型參數(shù)需要使用上面【請求體接收參數(shù)】中的實(shí)例代碼接收值
后臺(tái)接口的編寫
@RestControllerpublic class IndexController { @PostMapping("/index") public Map<String, Object> index(@RequestBody UserEntity entity) { // 將接收的參數(shù)返回給前端 HashMap<String, Object> result = new HashMap<>(); result.put("username", entity.getUsername()); result.put("password", entity.getPassword()); return result; }}VUE代碼
axios如果發(fā)起的為POST類型請求,默認(rèn)會(huì)將參數(shù)放在請求體中,這里直接寫即可
request1() { // 創(chuàng)建date對象存儲(chǔ)參數(shù) let data = { username: "哈哈哈哈", password: "嘿嘿嘿嘿" } // 發(fā)起請求 this.axios.post("http://localhost:8080/index", data).then(res => { console.log("res", res); });},小程序代碼
小程序代碼也是一樣的,當(dāng)發(fā)起的時(shí)POST類型的請求時(shí),默認(rèn)會(huì)把參數(shù)放在請求體中
request1() { // 構(gòu)建表單對象,向表單中追加參數(shù) let data = { username: "哈哈哈哈哈哈", password: "aabbccdd" } // 發(fā)起請求 wx.request({ url: 'http://localhost:8080/index', method: "POST", data: data, success: res => { console.log(res.data); } })},小技巧:如何區(qū)分傳參類型在實(shí)際開發(fā)中大概率不用寫前端代碼,只負(fù)責(zé)編寫后臺(tái)接口,但怎樣才能知道前端請求是什么類型參數(shù)?
關(guān)于這點(diǎn)可以通過瀏覽器開發(fā)者工具的【網(wǎng)絡(luò)】面板可以看出來,網(wǎng)絡(luò)面板打開時(shí)會(huì)錄制網(wǎng)頁發(fā)起的所有請求
路徑占位傳參就不解釋了,沒啥好說的,這里介紹一下路徑傳參、表單傳參和請求體傳參
路徑傳參
編寫好路徑傳參的請求代碼后切換到網(wǎng)絡(luò)面板,點(diǎn)擊發(fā)起請求:
請求體傳參
編寫好請求體傳參的請求代碼后切換到網(wǎng)絡(luò)面板,點(diǎn)擊發(fā)起請求:
表單類型傳參
編寫好表單類型傳參的請求代碼后切換到網(wǎng)絡(luò)面板,點(diǎn)擊發(fā)起請求:
封裝統(tǒng)一響應(yīng)工具類掌握了前后端交互的流程就可以正常開發(fā)網(wǎng)站了,這里推薦后端返回一套規(guī)定好的模板數(shù)據(jù),否則某些情況可能會(huì)比較難處理,例如這個(gè)查詢用戶列表的接口:
@RestControllerpublic class IndexController { @RequestMapping("/index") public List<HashMap<String, String>> index() { // 查詢用戶列表 List<HashMap<String, String>> userList = this.selectList(); // 將用戶列表數(shù)據(jù)返回給前端 return userList; } // 模擬dao層的查詢代碼,返回一個(gè)集合列表,集合中每個(gè)元素對應(yīng)一條用戶信息 public List<HashMap<String, String>> selectList() { ArrayList<HashMap<String, String>> list = new ArrayList<>(); for (int i = 1; i <= 5; i++) { HashMap<String, String> map = new HashMap<>(); map.put("id", UUID.randomUUID().toString()); map.put("username", "游客" + i); map.put("gender", i % 2 == 1 ? "男" : "女"); list.add(map); } return list; }}該接口乍一看沒毛病,拿到用戶列表數(shù)據(jù)后返回給前端用于渲染,合情合理,可是如果后端業(yè)務(wù)邏輯有BUG可能會(huì)導(dǎo)致前端接收到的結(jié)果為空,這種情況下前端就需要判斷,如果接收到的值為空,就提示請求出錯(cuò),問題貌似已經(jīng)解決,但是如果表中本來就沒有任何數(shù)據(jù)的話有應(yīng)該怎么處理
上述的就是最常見的一種比較頭疼的情況,所以針對這種情況最好指定一套標(biāo)準(zhǔn)的返回模板進(jìn)行處理
制定響應(yīng)工具類
根據(jù)剛剛的舉例來看,返回結(jié)果中應(yīng)該有一個(gè)標(biāo)識(shí)來判斷該請求是否執(zhí)行成功,如果執(zhí)行失敗的話還應(yīng)該返回失敗原因,響應(yīng)給前端的數(shù)據(jù)會(huì)被轉(zhuǎn)換為JSON數(shù)據(jù),使用Map集合來返回最合適不過了
import java.util.HashMap;import java.util.Map;public class Result extends HashMap<String, Object> { /** * 私有化構(gòu)造方法,不讓外界直接創(chuàng)建對象 * @param status true為請求成功,false為請求失敗 * @param msg 返回給前端的消息 */ private Result(boolean status, String msg) { // 規(guī)定無論請求成功還是失敗,這兩個(gè)參數(shù)都必須攜帶 super.put("status", status); super.put("msg", msg); } /** * 靜態(tài)方法,如果請求成功就調(diào)用ok */ public static Result ok() { return new Result(true, "請求成功"); } /** * 靜態(tài)方法,如果請求失敗就調(diào)用fail,需要提供失敗信息 */ public static Result fail(String msg) { return new Result(false, msg); } /** * 規(guī)定所有返回前端的數(shù)據(jù)都放在data中 * @param name 對象名 * @param obj 返回的對象 */ public Result put(String name, Object obj) { // 如果集合中不包含data,就創(chuàng)建個(gè)Map集合添加進(jìn)去 if (!this.containsKey("data")) { super.put("data", new HashMap<String, Object>()); } // 獲取data對應(yīng)的map集合,往里面添加數(shù)據(jù) Map<String, Object> data = (Map<String, Object>) this.get("data"); data.put(name, obj); return this; }}擴(kuò)展:ApiPost接口調(diào)試工具在后臺(tái)接口編寫完成后,一般情況下我們都需要進(jìn)行測試,GET請求還好,瀏覽器直接就訪問呢了,如果是POST請求還要去寫前端代碼就很煩,這里介紹一款接口調(diào)試工具ApiPost
你可能沒聽過ApiPost,但是你大概率聽說過Postman,他們的用法幾乎一致,且ApiPost是國人開發(fā)的免費(fèi)的接口調(diào)試工具,界面中文很友好
這里也可以看出來,form表單傳參其實(shí)也算在了請求體里面,只不過使用的是multipart/form-data類型的參數(shù)而已,而之前提到的請求體傳參對應(yīng)的就是application/json
原文地址:https://xintu.cnblogs.com/hanzhe/p/16037322.html
掃描二維碼推送至手機(jī)訪問。
版權(quán)聲明:本文由信途科技轉(zhuǎn)載于網(wǎng)絡(luò),如有侵權(quán)聯(lián)系站長刪除。
轉(zhuǎn)載請注明出處http://macbookprostickers.com/xintu/64099.html