溫故而知新,本文為一時興起寫出,如有錯誤還請指正
本文後臺基於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