前言
當以前後端分離的方式進行項目的開發時,我們可以簡單地把前端開發看做頁面展示的開發,把後端開發看做數據處理的開發。前端頁面的展示,會根據需求去後端請求相應的數據。
後端是以URL的方式暴露接口來提供服務的,也就是說前端需要根據需求對應的URL組裝http請求,去調用後端接口獲取數據並將展示在頁面上。前端項目的實際開發中,經常使用axios + promise
來整合http請求。這篇博客就是對axios + promise整合http請求
的相關知識點進行總結。
目錄
Ajax相關知識點
AJAX
= Asynchronous JavaScript and XML(異步的 JavaScript 和 XML)。 AJAX 不是新的編程語言,而是一種使用現有標準的新方法。在不重新加載整個頁面的情況下, AJAX 與服務器交換數據並更新部分網頁的內容。
—參考自W3school的ajax教學
ajax的相關代碼如下:
// 創建XMLHttpRequest對象
var request;
if (window.XMLHttpRequest) {
// IE7+和其他
request = new XMLHttpRequest();
} else {
// IE6或IE5
request = new ActiveXObject("Microsoft.XMLHTTP");
}
// 當http請求的狀態發生變化時,會調用onreadystatechange()方法
request.onreadystatechange = function(){
// 當readyStatue = 4且status = 200時,表示http請求成功完成
if (request.readyState === 4 && request.status === 200) {
// 獲取http請求返回的字符串類型的響應文本
var response = request.responseText;
// 通過json解析後,將響應文本的值填充到頁面對應的元素中
var jsonObj = JSON.parse(response);
document.getElementById("date").innerHTML = jsonObj.date;
document.getElementById("time").innerHTML = jsonObj.time;
}
}
// 規定http請求的的類型、路徑、是否異步
request.open('method_type','url',async);
// 如果是post請求提交表單數據,需要設置請求頭
request.setRequestHeader("Content-type","application/x-www-form-urlencoded");
// 發送http請求
request.send();
一個最常見的適合使用ajax的場景就是:註冊用戶時的表單校驗。用戶註冊demo對應的原生html代碼如下:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=UTF-8" />
</head>
<body>
<div id="test" class="test">
<form id="form" method="post" action="register_url">
<p id="register-email">
郵 箱:<input type="text" name="email"/>
</p>
<p id="register-mobile">
手 機:<input type="text" name="mobile"/>
</p>
<p id="register-username">
用戶名稱:<input type="text" name="username"/>
</p>
<p id="register-password">
密 碼:<input type="password" name="password"/>
</p>
<p id="register-password-confirm">
確認密碼:<input type="password" name="passwordConfirm"/>
</div>
<p id="button-area">
<button id="register-btn" type="submit">註冊</button>
</p>
</form>
</div>
</body>
</html>
當點擊“註冊”按鈕時,會發送一個POST
類型、url爲register_url
的http請求。這個請求對應的後端接口,需要先對用戶填寫的信息進行校驗。如果校驗通過,後端會新增一條用戶記錄然後告知前端註冊成功;如果校驗失敗,後端則會報錯的詳細信息返回給前端。
Web的運作原理:一次HTTP請求對應一個頁面。也就是說,我們用一般的表單提交進行用戶註冊,當錄入的用戶信息校驗未通過或遇到網絡問題時,其實會跳轉到其他的頁面(如404頁面等等)。但要使用註冊功能只能在註冊頁面,這個時候註冊頁面就會重新加載一次(也就是我們可能會遇到的瀏覽器閃了一下)。重新加載頁面,我們之前錄入的信息就會消失。如果多次校驗失敗,我們就要多次填寫重複信息,用戶體驗非常差。
保證發送的http請求出現異常時不會刷新網頁,這個時候就要考慮使用ajax。這裏就只需要改裝表單的提交方式,改爲給“註冊”按鈕添加一個點擊事件,在這個事件中去用ajax調用後端的註冊接口即可。
Promise相關知識點
Promise
對象用於表示一個異步操作的最終狀態(完成或失敗),以及該異步操作的結果值。
—參考自Promise中文官網
Promise
對象只有三種狀態:pending(進行中)
,fulfilled(已完成)
,rejected(已拒絕)
。Promise
對象創建後的初始狀態就是pending
,它的構造函數中有兩個函數參數:resolve
和reject
。構造函數內部調用resolve()
會將當前Promise
對象的狀態從pending
變爲fulfilled
;構造函數內部調用reject()
會將當前Promise
對象的狀態從pending
變爲rejected
。
由於ES6實現了Promise,而現在的主流瀏覽器基本支持ES6的絕大部分規範,因此我們可以直接在瀏覽器的控制檯中鍵入js代碼進行測試。
在瀏覽器的控制檯中鍵入如下代碼:
function runAsync(count) {
console.log('第' + count + '次調用runAsync()');
let promise = new Promise(function(resolve, reject){
// 模擬異步請求
setTimeout(function(){
let num = Math.random();
if (num > 0.5) {
console.log('第' + count + '次的結果大於0.5,調用resolve()將promise狀態設置爲fulfilled');
resolve();
} else {
console.log('第' + count + '次的結果不大於0.5,調用reject()將promise狀態設置爲rejected');
reject();
}
}, 1000);
});
return promise;
}
runAsync(0);
runAsync(1);
runAsync(2);
這段代碼的意思很簡單—聲明瞭一個返回promise對象的函數runAsync()
。該promise的構造函數中有一個延時定時器,任務是:1秒之後生成一個0-1之間的隨機數,當隨機數大於0.5時把promise的狀態設置爲fulfilled
,否則設置爲rejected
。
測試效果如下:
Promise的中文意思就是:承諾。promise是一個代理對象,它代理的是其構造函數內部的一個事件,通過判斷promise對象的狀態,我們能夠獲取promise所代理的那個事件將來的執行結果。基於promise對象的狀態也就是被代理事件的執行結果是成功(promise的狀態爲fulfilled
)還是失敗(promise的狀態爲rejected
),來做一些基於結果的額外操作。獲取事件最終結果主要使用它的then()
方法和catch()
方法。
Promise的then()
和catch()
當promise的狀態是fulfilled
時,會執行then()
中的第一個函數參數;當promise的狀態是rejected
時,會執行then()
中的第二個函數參數或catch()
的第一個函數參數。
測試的代碼如下:
function runAsync() {
let promise = new Promise(function(resolve, reject){
setTimeout(function(){
let num = Math.random();
if (num > 0.5) {
resolve('<fulfilled>' + num);
} else {
reject('<rejected>' + num);
}
}, 1000)
})
return promise;
}
runAsync().then(res => {
console.log('事件成功:' + res);
}, err => {
console.log('事件失敗:' + err);
})
runAsync().catch(err => {
console.log('事件失敗:' + err);
})
測試效果如下:
catch()
對事件失敗的處理與then()
中第二個函數參數是一樣的。這裏可以自行測試。
在1秒之後獲取一個0-1的隨機數,這個隨機數與0.5比大小的結果是不可期的。但我們可以通過Promise的狀態預先定義出:當這個事件未來的執行結果爲成功時(resolve()
將狀態設置爲fulfilled
),控制檯打印“事件成功”;當事件未來的執行結果爲失敗時(reject()
將狀態設置爲rejected
),控制檯打印“事件失敗”。這對於js編程是很有意義的,由於js是單線程執行的,碰到耗時長的網絡請求時如果讓線程等待到拿到最終的執行結果再進行其他的操作,那用戶的體驗是非常糟糕的。這時就可以考慮使用promise來封裝網絡請求。
then()
和catch()
的返回值也是promise對象,因此可以通過鏈式操作來進行同步操作。
測試代碼如下:
function runAsync() {
let promise = new Promise(function(resolve, reject){
setTimeout(function(){
let num = Math.random();
if (num > 0.5) {
resolve('<fulfilled>' + num);
} else {
reject('<rejected>' + num);
}
}, 1000)
})
return promise;
}
runAsync().then(res => {
console.log('事件成功:' + res);
return '<1>';
}).then(res => {
console.log('接收前一個promise中的返回值:' + res);
return '<2>';
}).then(res => {
console.log('接收前一個promise中的返回值:' + res);
let a = undefined;
console.log(a.index);
}).catch(err => {
console.log('事件失敗:' + err);
});
測試效果如下:
從這段代碼我們可以看到,then()
的鏈式操作中第一個參數就是上一個函數的返回值;catch()
不僅能夠在promise對象處於rejected
狀態進行操作,也能夠捕獲鏈式操作前面出現的異常,保證js不卡在這裏。
Promise的all()
和race()
上面我們通過鏈式操作能夠進行同步執行,現在我們可以學習all()
和race()
進行異步執行。all()
中promise參數集合對應的所有事件同時執行,只有所有事件的結果都是成功纔算成功(fulfilled
),否則就是失敗(rejected
)。race()
是所有事件進行競賽,最先執行完成的事件結果就是其結果。
測試代碼如下:
function runAsync1() {
let promise = new Promise(function(resolve, reject){
setTimeout(function(){
let num = Math.random();
if (num > 0.5) {
resolve('runAsync1()-fulfilled: ' + num);
} else {
reject('runAsync1()-rejected: ' + num);
}
}, 3000)
})
return promise;
}
function runAsync2() {
let promise = new Promise(function(resolve, reject){
setTimeout(function(){
let num = Math.random();
if (num > 0.5) {
resolve('runAsync2()-fulfilled: ' + num);
} else {
reject('runAsync2()-rejected: ' + num);
}
}, 1000)
})
return promise;
}
function runAsync3() {
let promise = new Promise(function(resolve, reject){
setTimeout(function(){
let num = Math.random();
if (num > 0.5) {
resolve('runAsync3()-fulfilled: ' + num);
} else {
reject('runAsync3()-rejected: ' + num);
}
}, 2000)
})
return promise;
}
// all()的測試
Promise.all([runAsync1(), runAsync2(), runAsync3()])
.then(res => {
console.log(res);
}).catch(err => {
console.log(err);
})
// race()的測試
Promise.race([runAsync1(), runAsync2(), runAsync3()])
.then(res => {
console.log(res);
}).catch(err => {
console.log(err);
})
all()
的測試效果圖如下:
race()
的測試效果圖如下:
從效果圖可以看出,all()
只有當所有的promise參數的最終狀態是fulfilled
時,自身的狀態纔會是fulfilled
,也就是去執行其後then()
中第一個函數參數,並且會把所有的返回值按數組順序返回,否則自身狀態就是rejected
,並且會返回最先返回rejected
的那個promise參數對應事件的返回值。
race()
就是所有的promise參數對應的事件進行競速,以最快執行完成的事件對應的promise參數的最終狀態爲準。這裏runAsync2()
的延時最短(1s),因此是以它的最終狀態爲準。
Axios相關知識點
Axios
是一個基於promise
的HTTP庫,可以用在瀏覽器和node.js中。
—參考自Axios中文官網
簡單來說,axios
就是對promise
進行了一次封裝。通過使用axios
,我們能夠在項目中很便捷輕鬆地發送HTTP請求。
假設有後端源碼的僞代碼如下:
// 配置項目端口8090
server.port: 8090
// 實體類Student
class Student {
Long id;
String name;
Integer age;
String phoneNum;
//...更多屬性
}
// Student對應接口層
@RequestMapping("/springboot_jpa")
public class StudentController{
@GetMapping("/student/findAll")
public List<Student> findAll(){
return studentService.findAll();
}
@RequestMapping("/student/findByNameLike")
public List<Student> findByNameLike(@RequestParam("surname") String surname){
return studentService.findByNameLike( surname );
}
}
成功啓動後端項目並在地址欄中發送HTTP請求(http://localhost:8090/springboot_jpa/student/findAll
),獲取當前所有的學生信息效果圖如下:
利用vue-cli
的腳手架工具快速搭建前端項目,並測試axios
的相關命令如下:
# 利用vue腳手架搭建前端項目axios-test
vue init webpack axios-test
cd axios-test
# 安裝需要的axios依賴包
npm install axios
# 啓動當前項目
npm run dev
成功啓動前端項目後,當前項目的默認目錄以及啓動效果圖如圖所示:
改動vue項目的主頁面src/App.vue
,測試使用axios
發送HTTP請求。
改動後App.vue
的代碼如下:
<template>
<div id="app">
<img src="./assets/logo.png">
<router-view/>
<div v-for="item in studentList" :key="item.id">
{{item.id}} - {{item.name}}
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'App',
data () {
return {
studentList: []
}
},
created () {
axios.get('localhost:8090/springboot_jpa/student/findAll').then(res => {
this.studentList = res.data
console.log(res)
}).catch(err => {
console.log(err)
})
}
}
</script>
上面這段代碼的意思非常簡單,就是定義了一個div
,裏面的信息就是初始化鉤子created
中使用axios
去請求後端接口獲取到學生信息列表。控制檯會報錯如下:
其實這是跨域的問題,禁止跨域是瀏覽器的安全限制機制。解決的辦法有很多種,這裏採用的做法是,修改config/index.js
文件的proxyTable
屬性如下:
proxyTable: {
'/api': {
target: 'http://localhost:8090',
changeOrigin: true,
pathRewrite: {
'^/api': '/'
}
}
}
然後還需要把所有的HTTP請求的域名以及端口號都替換爲/api
。
這裏的意思是:在開發環境中,檢測到所有以/api
開發的HTTP請求,都會把其URL重寫。(如:這裏的就是會把檢測的HTTP請求的URL—/api/springboot_jpa/student/findAll
重寫爲http://localhost:8090/springboot_jpa/student/findAll
,再去請求真正的後端接口)。
修改好config/index.js
和src/App.vue
後,重啓項目。最終的效果圖如下:
從上面的效果圖來看,使用axios
發送HTTP請求非常簡單。同樣地,其他類型的HTTP請求也可以自行測試,這裏就不再贅述,只做總結。
- 我們可以使用
axios
提供的get()/post()/put()/delete()
方法發送相應類型的http請求; - 方法參數的拼接取決於其位置,以
?
拼接在URL後的參數我們可以自己拼接也可以使用params
進行組裝,封裝在請求體(後端需要加註解@RequestBody
)中的參數,可以使用對象拼接也可以使用data
進行組裝。
基本所有的項目都需要做權限認證。使用axios
可以非常簡單的實現這一功能。
利用axios.interceptors
攔截器能夠攔截所有的HTTP請求。
- 將用戶登錄成功後返回的
token
保存到本地倉庫localStorage
中。 - 然後給出登錄\登出以外的所有接口在請求頭上都加上
token
。 - 在登出時,去清除本地倉庫
localStorage
中的token
集合。
實現這一功能的整合的axiosUtil
工具包代碼如下:
import axios from 'axios'
import router from 'vue-router'
// 創建實例並設置超時時間
const http = axios.create({
timeout: 10000
})
// 攔截所有HTTP請求
// request攔截器
http.interceptors.request.use(config => {
if (config.url === '/api/login') {
return config
}
// 除了登錄以外的所有接口,都在請求頭加上進行權限認證用的token
if (localStorage.token) {
config.headers.common['Authorization'] = localStorage.token
}
return config
}, error => {
return Promise.reject(error)
})
// response攔截器
http.interceptors.request.use(res => {
// 登出接口清除token
if (res.url === '/api/logout') {
localStorage.removeItem('token')
}
return res
}, error => {
if (error.response) {
switch (error.response.status) {
// 如果是由於權限問題報錯,重定向到登錄頁面,登錄成功後會回到原始頁面(不一定是主頁)
case 401:
router.replace({
path: '/login',
query: { redirect: router.currentRoute.fullPath }
})
}
}
return Promise.reject(error)
})
export default http
需要注意的是response攔截器是拿不到響應返回的數據的。也就是說,調用後端登錄接口返回的token
無法在response攔截器中獲取。這樣就只能在登錄頁面導入axiosUtil
工具包,成功調用登錄接口後再將token保存到localStorage
中。