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