網絡請求封裝
網絡請求模塊難度較大,如果學習起來感覺吃力,可以直接學習
[請求封裝-使用 npm 包發送請求]
以后的模塊
01. 為什么要封裝 wx.request
小程序大多數 API 都是異步 API,如 wx.request(),wx.login() 等。這類 API 接口通常都接收一個 Object
對象類型的參數,參數中可以按需指定以下字段來接收接口調用結果:
參數名 | 類型 | 必填 | 說明 |
---|---|---|---|
success | function | 否 | 調用成功的回調函數 |
fail | function | 否 | 調用失敗的回調函數 |
complete | function | 否 | 調用結束的回調函數(調用成功、失敗都會執行) |
wx.request({// 接口調用成功的回調函數success() {wx.request({success() {wx.request({success() {wx.request({success() {wx.request({success() {wx.request({success() {wx.request({success() {wx.request({success() {}})}})}})}})}})}})}})},// 接口調用失敗的回調函數fail() {},// 接口調用結束的回調函數(調用成功、失敗都會執行)complete() {}
})
如果采用這種回調函數的方法接收返回的值,可能會出現多層 success
套用的情況,容易出現回調地獄問題,
為了解決這個問題,小程序基礎庫從 2.10.2 版本起,異步 API 支持 callback & promise 兩種調用方式。
當接口參數 Object 對象中不包含 success/fail/complete 時,將默認返回 promise,否則仍按回調方式執行,無返回值。
但是部分接口如 downloadFile
, request
, uploadFile
等本身就有返回值,因此不支持 promise 調用方式,它們的 promisify 需要開發者自行封裝。
Axios
是我們日常開發中常用的一個基于 promise 的網絡請求庫
我們可以參考 Axios
的 [使用方式] 來封裝自己的網絡請求模塊,咱們看一下使用的方式:
網絡請求模塊封裝
import WxRequest from 'mina-request'// 自定義配置新建一個實例
const instance = new WxRequest(({baseURL: 'https://some-domain.com/api/',timeout: 1000,headers: {'X-Custom-Header': 'foobar'}
})// 通過 instance.request(config) 方式發起網絡請求
instance.requst({method: 'post',url: '/user/12345',data: {firstName: 'Fred',lastName: 'Flintstone'}
})// 通過 instance.get 方式發起網絡請求
instance.get(url, data, config)// 通過 instance.delete 方式發起網絡請求
instance.delete(url, data, config)// 通過 instance.post 方式發起網絡請求
instance.post(url, data, config)// 通過 instance.put 方式發起網絡請求
instance.put(url, data, config)// ----------------------------------------------// 添加請求攔截器
instance.interceptors.request = (config) => {// 在發送請求之前做些什么return config
}// 添加響應攔截器
instance.interceptors.response = (response) => {// response.isSuccess = true,代碼執行了 wx.request 的 success 回調函數// response.isSuccess = false,代碼執行了 wx.request 的 fail 回調函數// response.statusCode // http 響應狀態碼// response.config // 網絡請求請求參數// response.data 服務器響應的真正數據// 對響應數據做點什么return response
}
封裝后網絡請求模塊包含以下功能
- 包含 request 實例方法發送請求
- 包含 get、delete、put、post 等實例方法可以快捷的發送網絡請求
- 包含 請求攔截器、響應攔截器
- 包含 uploadFile 將本地資源上傳到服務器 API
- 包含 all 并發請求方法
- 同時優化了并發請求時 loading 顯示效果
02. 請求封裝-request 方法
思路分析:
在封裝網絡請求模塊的時候,采用 Class
類來進行封裝,采用類的方式封裝代碼更具可復用性,也方便地添加新的方法和屬性,提高代碼的擴展性
我們先創建一個 class 類,同時定義 constructor 構造函數
// 創建 WxRequest 類
class WxRequest {constructor() {}
}
我們在 WxRequest
類內部封裝一個 request
實例方法
request
實例方法中需要使用 Promise
封裝 wx.request,也就是使用 Promise
處理 wx.request
的返回結果
request
實例方法接收一個 options
對象作為形參,options
參數和調用 wx.request
時傳遞的請求配置項一致
- 接口調用成功時,通過
resolve
返回響應數據 - 接口調用失敗時,通過
reject
返回錯誤原因
class WxRequest {// 定義 constructor 構造函數,用于創建和初始化類的屬性和方法constructor() {}/*** @description 發起請求的方法* @param { Object} options 請求配置選項,同 wx.request 請求配置選項* @returns Promise*/request(options) {// 使用 Promise 封裝異步請求return new Promise((resolve, reject) => {// 使用 wx.request 發起請求wx.request({...options,// 接口調用成功的回調函數success: (res) => {resolve(res)},// 接口調用失敗的回調函數fail: (err) => {reject(err)}})})}
}
然后對 WxRequest
進行實例化,然后測試 request
實例方法是否封裝成功!
注意:我們先將類 和 實例化的對象放到同一個文件中,這樣方便進行調試,后面我們在拆分成兩個文件
class WxRequest {// coding....
}// ----------------- 實例化 ----------------------// 對 WxRequest 進行實例化
const instance = new WxRequest()// 將 WxRequest 的實例通過模塊化的方式暴露出去
export default instance
在其他模塊中引入封裝的文件后,我們期待通過 request()
方式發起請求,以 promise 的方式返回參數
// 導入創建的實例
import instance from '../../utils/wx-request'Page({// 點擊按鈕觸發 handler 方法async handler() {// 通過實例調用 request 方法發送請求const res = await instance.request({url: 'https://gmall-prod.atguigu.cn/mall-api/index/findBanner',method: 'GET'})console.log(res)}
})
落地代碼:
?? /utils/request.js
// 創建 WxRequest 類,采用類的方式進行封裝會讓方法更具有復用性,也可以方便進行添加新的屬性和方法class WxRequest {// 定義 constructor 構造函數,用于創建和初始化類的屬性和方法constructor() {}/*** @description 發起請求的方法* @param { Object} options 請求配置選項,同 wx.request 請求配置選項* @returns Promise*/request(options) {// 使用 Promise 封裝異步請求return new Promise((resolve, reject) => {// 使用 wx.request 發起請求wx.request({...options,// 接口調用成功的回調函數success: (res) => {resolve(res)},// 接口調用失敗的回調函數fail: (err) => {reject(err)}})})}
}// ----------------- 實例化 ----------------------// 對 WxRequest 進行實例化
const instance = new WxRequest()// 將 WxRequest 的實例通過模塊化的方式暴露出去
export default instance
?? /pages/test/test.js
import instance from '../../utils/request'Page({// 點擊按鈕觸發 handler 方法async handler() {// 第一種調用方式:通過 then 和 catch 接收返回的值// instance// .request({// url: 'https://gmall-prod.atguigu.cn/mall-api/index/findBanner',// method: 'GET'// })// .then((res) => {// console.log(res)// })// .catch((err) => {// console.log(err)// })// 第二種調用方式:通過 await 和 async 接收返回的值const res = await instance.request({url: 'https://gmall-prod.atguigu.cn/mall-api/index/findBanner',method: 'GET'})console.log(res)}})
03. 請求封裝-設置請求參數
思路分析:
在發起網絡請求時,需要配置一些請求參數,
其中有一些參數我們可以設置為默認參數,例如:請求方法、超時時長 等等,因此我們在封裝時我們要定義一些默認的參數。
// 默認參數對象
defaults = {baseURL: '', // 請求基準地址url: '', // 開發者服務器接口地址data: null, // 請求參數method: 'GET',// 默認請求方法// 請求頭header: {'Content-type': 'application/json' // 設置數據的交互格式},timeout: 60000 // 小程序默認超時時間是 60000,一分鐘// 其他參數...
}
但是不同的項目,請求參數的設置是不同的,我們還需要允許在進行實例化的時候,傳入參數,對默認的參數進行修改。例如:
// 對 WxRequest 進行實例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api', // 請求基準地址timeout: 10000 // 微信小程序 timeout 默認值為 60000
})
在通過實例,調用 request
實例方法時也會傳入相關的請求參數
const res = await instance.request({url: '/index/findBanner',method: 'GET'
})
從而得出結論:請求參數的設置有三種方式:
- 默認參數:在
WxRequest
類中添加defaults
實例屬性來設置默認值 - 實例化時參數:在對
WxRequest
類進行實例化時傳入相關的參數,需要在constructor
構造函數形參進行接收 - 調用實例方法時傳入請求參數
默認參數和自定義參數的合并操作,通常會在constructor
中進行。
因此我們就在 constructor
中將開發者傳入的相關參數和defaults
默認值進行合并,需要傳入的配置項覆蓋默認配置項
class WxRequest {+ // 默認參數對象
+ defaults = {
+ baseURL: '', // 請求基準地址
+ url: '', // 開發者服務器接口地址
+ data: null, // 請求參數
+ method: 'GET',// 默認請求方法
+ // 請求頭
+ header: {
+ 'Content-type': 'application/json' // 設置數據的交互格式
+ },
+ timeout: 60000 // 小程序默認超時時間是 60000,一分鐘
+ }/*** @description 定義 constructor 構造函數,用于創建和初始化類的屬性和方法* @param {*} params 用戶傳入的請求配置項*/
+ constructor(params = {}) {
+ // 在實例化時傳入的參數能夠被 constructor 進行接收
+ console.log(params)+ // 使用 Object.assign 合并默認參數以及傳遞的請求參數
+ this.defaults = Object.assign({}, this.defaults, params)
+ }// coding....
}// ----------------- 實例化 ----------------------// 對 WxRequest 進行實例化
+ const instance = new WxRequest({
+ baseURL: 'https://gmall-prod.atguigu.cn/mall-api',
+ timeout: 15000
+ })// 將 WxRequest 的實例通過模塊化的方式暴露出去
export default instance
在調用 request
實例時也會傳入相關的參數,是發起請求真正的參數,
我們需要將調用 reqeust
實例方法時傳入的參數,繼續覆蓋合并以后的參數,請求才能夠發送成功
注意:讓使用傳入的參數覆蓋默認的參數,同時拼接完整的請求地址。
// 創建 request 請求方法
request(options) {
+ // 拼接完整的請求地址
+ options.url = this.defaults.baseURL + options.url
+ // 合并請求參數
+ options = { ...this.defaults, ...options }return new Promise((resolve, reject) => {// coding...})}
落地代碼:
?? utils/request.js
// 創建 Request 類,用于封裝 wx.request() 方法
class WxRequest {+ // 默認參數對象
+ defaults = {
+ baseURL: '', // 請求基準地址
+ url: '', // 開發者服務器接口地址
+ data: null, // 請求參數
+ method: 'GET',// 默認請求方法
+ // 請求頭
+ header: {
+ 'Content-type': 'application/json' // 設置數據的交互格式
+ },
+ timeout: 60000 // 小程序默認超時時間是 60000,一分鐘
+ }+ /**
+ * @description 定義 constructor 構造函數,用于創建和初始化類的屬性和方法
+ * @param {*} params 用戶傳入的請求配置項
+ */
+ constructor(params = {}) {
+ // 在實例化時傳入的參數能夠被 constructor 進行接收
+ console.log(params)+ // 使用 Object.assign 合并默認參數以及傳遞的請求參數
+ this.defaults = Object.assign({}, this.defaults, params)
+ }/*** @description 發起請求的方法* @param { Object} options 請求配置選項,同 wx.request 請求配置選項* @returns Promise*/request(options) {
+ // 拼接完整的請求地址
+ options.url = this.defaults.baseURL + options.url
+ // 合并請求參數
+ options = { ...this.defaults, ...options }// 方法返回一個 Promise 對象return new Promise((resolve, reject) => {// coding...})}
}// ----------------- 實例化 ----------------------// 對 WxRequest 進行實例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api',timeout: 15000
})// 將 WxRequest 的實例通過模塊化的方式暴露出去
export default instance
04. 請求封裝-封裝請求快捷方法
思路分析:
目前已經完成了 request()
請求方法的封裝,同時處理了請求參數。
每次發送請求時都使用 request()
方法即可,但是項目中的接口地址有很多,不是很簡潔
const res = await instance.request({url: '/index/findBanner',method: 'GET'
})
所以我們在 request()
基礎上封裝一些快捷方法,簡化 request()
的調用。
需要封裝 4 個快捷方法,分別是 get
、delete
、post
、put
,他們的調用方式如下:
instance.get('請求地址', '請求參數', '請求配置')
instance.delete('請求地址', '請求參數', '請求配置')
instance.post('請求地址', '請求參數', '請求配置')
instance.put('請求地址', '請求參數', '請求配置')
這 4 個請求方法,都是通過實例化的方式進行調用,所以需要 Request
類中暴露出來 get
、delete
、post
、put
方法。每個方法接收三個參數,分別是:接口地址、請求參數以及其他參數。
這 4 個快捷方法,本質上其實還是調用 request
方法,我們只要在方法內部組織好參數,調用 request
發送請求即可
class WxRequest {// coding...+ // 封裝 GET 實例方法
+ get(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'GET' }, config))
+ }+ // 封裝 POST 實例方法
+ post(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'POST' }, config))
+ }+ // 封裝 PUT 實例方法
+ put(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'PUT' }, config))
+ }+ // 封裝 DELETE 實例方法
+ delete(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'DELETE' }, config))
+ }
}// ----------------- 實例化 ----------------------// 對 WxRequest 進行實例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api',timeout: 15000
})// 將 WxRequest 的實例通過模塊化的方式暴露出去
export default instance
落地代碼:
?? utils/request.js
class WxRequest {// coding...+ // 封裝 GET 實例方法
+ get(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'GET' }, config))
+ }+ // 封裝 POST 實例方法
+ post(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'POST' }, config))
+ }+ // 封裝 PUT 實例方法
+ put(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'PUT' }, config))
+ }+ // 封裝 DELETE 實例方法
+ delete(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'DELETE' }, config))
+ }
}// ----------------- 實例化 ----------------------// 對 WxRequest 進行實例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api',timeout: 15000
})// 將 WxRequest 的實例通過模塊化的方式暴露出去
export default instance
?? /pages/test/test.js
// 導入創建的實例
import instance from '../../utils/wx-request'Page({async handler() {// 第一種調用方式:通過 then 和 catch 接收返回的值// instance// .request({// url: 'https://gmall-prod.atguigu.cn/mall-api/index/findBanner',// method: 'GET'// })// .then((res) => {// console.log(res)// })// .catch((err) => {// console.log(err)// })// 第二種調用方式// 通過實例調用 request 方法發送請求// const res = await instance.request({// url: '/index/findBanner',// method: 'GET'// })// console.log(res)// 第三種調用方式:通過調用快捷方式接收返回的值const res = await instance.get('/index/findBanner')console.log(res)}
})
05. 請求封裝-wx.request 注意事項
知識點:
在使用 wx.request 發送網絡請求時。
只要成功接收到服務器返回,無論statusCode
是多少,都會進入 success
回調
開發者根據業務邏輯對返回值進行判斷。
什么時候會有 fail
回調函數 ?
一般只有網絡出現異常、請求超時等時候,才會走 fail
回調
落地代碼:
測試代碼
request() {wx.request({url: 'https://gmall-prod.atguigu.cn/mall-api/index/findCategory',method: 'GET',// timeout: 100, 測試網絡超時,需要調整網絡success: (res) => {console.log('只要成功接收到服務器返回,不管狀態是多少,都會進入 success 回調')console.log(res)},fail: (err) => {console.log(err)}})
}
06. 請求封裝-定義請求/響應攔截器
思路分析:
為了方便統一處理請求參數以及服務器響應結果,為 WxRequest
添加攔截器功能,攔截器包括 請求攔截器 和 響應攔截器
請求攔截器本質上是在請求之前調用的函數,用來對請求參數進行新增和修改
響應攔截器本質上是在響應之后調用的函數,用來對響應數據做點什么
注意:不管成功響應還是失敗響應,都會執行響應攔截器
攔截器的使用方式:
// 請求攔截器
instance.interceptors.request = (config) => {// 在發送請求之前做些什么return config
}// 響應攔截器
instance.interceptors.response = (response) => {// 對響應數據做點什么return response
}
通過使用方式,我們可以得出結論:
可以在 WxRequest
類內部定義 interceptors
實例屬性,屬性中需要包含 request
以及 response
方法
需要注意:在發送請求時,還需要區分是否通過實例調用了攔截器:
- 沒有通過實例調用攔截器,需要定義默認攔截器,在默認攔截器中,需要將請求參數進行返回
- 通過實例調用攔截器,那么實例調用的攔截器會覆蓋默認的攔截器方法,然后將新增或修改的請求參數進行返回
實現攔截器的思路:
- 在
WxRequest
類內部定義interceptors
實例屬性,屬性中需要包含request
以及response
方法 - 是否通過實例調用了攔截器
- 是:定義默認攔截器
- 否:實例調用的攔截器覆蓋默認攔截器
- 在發送請求之前,調用請求攔截器
- 在服務器響應以后,調用響應攔截器
- 不管成功、失敗響應,都需要調用響應攔截器
在 WxRequest
類內部定義 interceptors
實例屬性,屬性中需要包含 request
以及 response
方法。
沒有使用攔截器,定義默認攔截器,需要將默認的請求參數進行返回。
如果使用了攔截器,那么使用者的攔截器會覆蓋默認的攔截器方法
class WxRequest {// coding...+ // 定義攔截器對象,包含請求攔截器和響應攔截器方法,方便在請求或響應之前進行處理。
+ interceptors = {
+ // 請求攔截器
+ request: (config) => config,
+ // 響應攔截器
+ response: (response) => response
+ }// 用于創建和初始化類的屬性以及方法// 在實例化時傳入的參數,會被 constructor 形參進行接收constructor(options = {}) {// coding...}
}
// ----------------- 以下是實例化的代碼 --------------------
// 目前寫到同一個文件中,是為了方便進行測試,以后會提取成多個文件// 對 WxRequest 進行實例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api',timeout: 15000
})+ // 配置請求攔截器
+ instance.interceptors.request = (config) => {
+ // 在發送請求之前做些什么
+ return config
+ }+ // 響應攔截器
+ instance.interceptors.response = (response) => {
+ // 對響應數據做點什么
+ return response
+ }// 將 WxRequest 實例進行暴露出去,方便在其他文件中進行使用
export default instance
在發送請求之前,調用請求攔截器,在服務器響應以后,調用響應攔截器
不管成功、失敗,都需要調用響應攔截器
class WxRequest {// coding...// request 實例方法接收一個對象類型的參數// 屬性值和 wx.request 方法調用時傳遞的參數保持一致request(options) {// 注意:需要先合并完整的請求地址 (baseURL + url)// https://gmall-prod.atguigu.cn/mall-api/index/findBanneroptions.url = this.defaults.baseURL + options.url// 合并請求參數options = { ...this.defaults, ...options }+ // 在發送請求之前調用請求攔截器
+ options = this.interceptors.request(options)// 需要使用 Promise 封裝 wx.request,處理異步請求return new Promise((resolve, reject) => {wx.request({...options,// 當接口調用成功時會觸發 success 回調函數success: (res) => {
+ // 不管接口成功還是失敗,都需要調用響應攔截器
+ // 第一個參數:需要合并的目標對象
+ // 第二個參數:服務器響應的數據
+ // 第三個參數:請求配置以及自定義的屬性
+ const mergetRes = Object.assign({}, res, { config: options })
+ resolve(this.interceptors.response(mergetRes))},// 當接口調用失敗時會觸發 fail 回調函數fail: (err) => {
+ // 不管接口成功還是失敗,都需要調用響應攔截器
+ const mergetErr = Object.assign({}, err, { config: options })
+ reject(this.interceptors.response(mergetErr))}})})}// coding...
}
落地代碼:
?? utils/request.js
// 創建 WxRequest 類
// 通過類的方式來進行封裝,會讓代碼更加具有復用性
// 也可以方便添加新的屬性和方法class WxRequest {// 定義實例屬性,用來設置默認請求參數defaults = {baseURL: '', // 請求基準地址url: '', // 接口的請求路徑data: null, // 請求參數method: 'GET', // 默認的請求方法// 請求頭header: {'Content-type': 'application/json' // 設置數據的交互格式},timeout: 60000 // 默認的超時時長,小程序默認的超時時長是 1 分鐘}+ // 定義攔截器對象,包含請求攔截器和響應攔截器方法,方便在請求或響應之前進行處理。
+ interceptors = {
+ // 請求攔截器
+ request: (config) => config,
+ // 響應攔截器
+ response: (response) => response
+ }// 用于創建和初始化類的屬性以及方法// 在實例化時傳入的參數,會被 constructor 形參進行接收constructor(params = {}) {// 通過 Object.assign 方法合并請求參數// 注意:需要傳入的參數,覆蓋默認的參數,因此傳入的參數需要放到最后this.defaults = Object.assign({}, this.defaults, params)}// request 實例方法接收一個對象類型的參數// 屬性值和 wx.request 方法調用時傳遞的參數保持一致request(options) {// 注意:需要先合并完整的請求地址 (baseURL + url)// https://gmall-prod.atguigu.cn/mall-api/index/findBanneroptions.url = this.defaults.baseURL + options.url// 合并請求參數options = { ...this.defaults, ...options }+ // 在發送請求之前調用請求攔截器
+ options = this.interceptors.request(options)// 需要使用 Promise 封裝 wx.request,處理異步請求return new Promise((resolve, reject) => {wx.request({...options,// 當接口調用成功時會觸發 success 回調函數success: (res) => {
+ // 不管接口成功還是失敗,都需要調用響應攔截器
+ // 第一個參數:需要合并的目標對象
+ // 第二個參數:服務器響應的數據
+ // 第三個參數:請求配置以及自定義的屬性
+ const mergeRes = Object.assign({}, res, { config: options })
+ resolve(this.interceptors.response(mergeRes))},// 當接口調用失敗時會觸發 fail 回調函數fail: (err) => {
+ // 不管接口成功還是失敗,都需要調用響應攔截器
+ const mergeErr = Object.assign({}, err, { iconfig: options })
+ // 不管接口成功還是失敗,都需要調用響應攔截器
+ err = this.interceptors.response(mergeErr)
+ reject(err)}})})}// 封裝 GET 實例方法get(url, data = {}, config = {}) {// 需要調用 request 請求方法發送請求,只需要組織好參數,傳遞給 request 請求方法即可// 當調用 get 方法時,需要將 request 方法的返回值 return 出去return this.request(Object.assign({ url, data, method: 'GET' }, config))}// 封裝 DELETE 實例方法delete(url, data = {}, config = {}) {return this.request(Object.assign({ url, data, method: 'DELETE' }, config))}// 封裝 POST 實例方法post(url, data = {}, config = {}) {return this.request(Object.assign({ url, data, method: 'POST' }, config))}// 封裝 PUT 實例方法put(url, data = {}, config = {}) {return this.request(Object.assign({ url, data, method: 'PUT' }, config))}
}// ----------------- 以下是實例化的代碼 --------------------
// 目前寫到同一個文件中,是為了方便進行測試,以后會提取成多個文件// 對 WxRequest 進行實例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api',timeout: 15000
})+ // 配置請求攔截器
+ instance.interceptors.request = (config) => {
+ // 在發送請求之前做些什么
+ return config
+ }+ // 響應攔截器
+ instance.interceptors.response = (response) => {
+
+ // 對響應數據做點什么
+ return response.data
+ }// 將 WxRequest 實例進行暴露出去,方便在其他文件中進行使用
export default instance
07. 請求封裝-完善請求/響應攔截器
思路分析:
在響應攔截器,我們需要判斷是請求成功,還是請求失敗,然后進行不同的業務邏輯處理。
例如:請求成功以后將數據簡化返回,網絡出現異常則給用戶進行網絡異常提示。
目前不管請求成功 (success),還是請求失敗(fail),都會執行響應攔截器
那么怎么判斷是請求成功,還是請求失敗呢 ?
封裝需求:
- 如果請求成功,將響應成功的數據傳遞給響應攔截器,同時在傳遞的數據中新增
isSuccess: true
字段,表示請求成功 - 如果請求失敗,將響應失敗的數據傳遞給響應攔截器,同時在傳遞的數據中新增
isSuccess: false
字段,表示請求失敗
在實例調用的響應攔截中,根據傳遞的數據進行以下的處理:
- 如果
isSuccess: true
表示服務器響應了結果,我們可以將服務器響應的數據簡化以后進行返回 - 如果
isSuccess: false
表示是網絡超時或其他網絡問題,提示網絡異常
,同時將返回即可
落地代碼:
?? utils/request.js
class WxRequest {// coding....request(options) {// coding....// 使用 Promise 封裝異步請求return new Promise((resolve, reject) => {// 使用 wx.request 發起請求wx.request({...options,// 接口調用成功的回調函數success: (res) => {// 響應成功以后觸發響應攔截器if (this.interceptors.response) {
+ // 調用響應攔截器方法,獲取到響應攔截器內部返回數據
+ // success: true 表示服務器成功響應了結果,我們需要對業務狀態碼進行判斷
+ res = this.interceptors.response({ response: res, isSuccess: true })}// 將數據通過 resolve 進行返回即可resolve(res)},// 接口調用失敗的回調函數fail: (err) => {// 響應失敗以后也要執行響應攔截器if (this.interceptors.response) {
+ // isSuccess: false 表示是網絡超時或其他問題
+ err = this.interceptors.response({ response: err, isSuccess: true })}// 當請求失敗以后,通過 reject 返回錯誤原因reject(err)}})})}// coding......
}// -----------------------------------------------------// 對 WxRequest 進行實例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api'
})// 設置請求攔截器
instance.setRequestInterceptor((config) => {console.log('執行請求攔截器')return config
})// 設置響應攔截器
+ instance.setResponseInterceptor((response) => {
+ const { response: res, isSuccess } = response+ // isSuccess: false 表示是網絡超時或其他問題,提示 網絡異常,同時將返回即可
+ if (!isSuccess) {
+ wx.toast('網絡異常,請稍后重試~')
+ // 如果請求錯誤,將錯誤的結果返回出去
+ return res
+ }+ // 簡化數據
+ return response.data
})// 將 WxRequest 的實例通過模塊化的方式暴露出去
export default instance
08. 請求封裝-使用請求/響應攔截器
思路分析:
使用請求攔截器:
在發送請求時,購物車列表、收貨地址、更新頭像等接口,都需要進行權限驗證,因此我們需要在請求攔截器中判斷本地是否存在訪問令牌 token
,如果存在就需要在請求頭中添加 token
字段。
使用響應攔截器:
在使用 wx.request 發送網絡請求時。只要成功接收到服務器返回,無論statusCode
是多少,都會進入 success
回調。
因此開發者根據業務邏輯對返回值進行判斷。
后端返回的業務狀態碼如下:
- 業務狀態碼 === 200, 說明接口請求成功,服務器成功返回了數據
- 業務狀態碼 === 208, 說明沒有 token 或者 token 過期失效,需要登錄或者重新登錄
- 業務狀態碼 === 其他,說明請求或者響應出現了異常
其他測試接口:/cart/getCartList
落地代碼:
?? utils/request.js
// 創建 WxRequest 類,采用類的方式進行封裝會讓方法更具有復用性,也可以方便進行添加新的屬性和方法class WxRequest {// coding...
}// -----------------------------------------------------// 對 WxRequest 進行實例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api',timeout: 5000
})// 設置請求攔截器
instance.setRequestInterceptor((config) => {
+ // 從本地獲取 token
+ if (wx.getStorageSync('token')) {
+ // 如果存在 token ,則添加請求頭
+ config.header['token'] = wx.getStorageSync('token')
+ }
+
+ // 返回請求參數
+ return config
})// 設置響應攔截器
instance.setResponseInterceptor(async (response) => {
+ const { response: res, isSuccess } = response+ // isSuccess: false 表示是網絡超時或其他問題,提示 網絡異常,同時將返回即可
+ if (!isSuccess) {
+ wx.toast('網絡異常,請稍后重試~')
+ // 如果請求錯誤,將錯誤的結果返回出去
+ return res
+ }+ switch (res.data.code) {
+ case 200:
+ return res.data+ case 208:
+ // 判斷用戶是否點擊了確定
+ const modalStatus = await wx.modal({
+ title: '提示',
+ content: '登錄授權過期,請重新授權'
+ })+ // 如果點擊了確定,先清空本地的 token,然后跳轉到登錄頁面
+ if (modalStatus) {
+ wx.clearStorageSync()
+ wx.navigateTo({
+ url: '/pages/login/login'
+ })
+ }
+ return+ default:
+ wx.showToast({
+ title: '接口調用失敗~~~~',
+ icon: 'none'
+ })+ // 將錯誤繼續向下傳遞
+ return Promise.reject(response)
+ }
})// 將 WxRequest 的實例通過模塊化的方式暴露出去
export default instance
09. 請求封裝-添加并發請求
思路分析:
前端并發請求是指在前端頁面同時向后端發起多個請求的情況。當一個頁面需要請求多個接口獲取數據時,為了提高頁面的加載速度和用戶體驗,可以同時發起多個請求,這些請求之間就是并發的關系。
我們通過兩種方式演示發起多個請求:
- 使用
async
和await
方式 - 使用
Promise.all()
方式
首先使用async
和 await
方式發送請求,使用 async
和 await
能夠控制異步任務以同步的流程執行,代碼如下,這時候就會產生一個問題,當第一個請求執行完以后,才能執行第二個請求,這樣就會造成請求的阻塞,影響渲染的速度,如下圖
這時候我們需要使用 Promise.all()
方式同時發起多個異步請求,并在所有請求完成后再進行數據處理和渲染。使用Promise.all()
能夠將多個請求同時發出,不會造成請求的阻塞。
通過兩種方式演示,我們能夠知道封裝并發請求的必要性。在 WxRequest 實例中封裝 all
方法,使用展開運算符將傳入的參數轉成數組,方法的內部,使用 Promise.all()
接收傳遞的多個異步請求,將處理的結果返回即可。
class WxRequest {// coding...+ // 封裝處理并發請求的 all 方法
+ all(...promise) {
+ return Promise.all(promise)
+ }// coding...
}// coding...
落地代碼:
?? utils/request.js
class WxRequest {// coding...+ // 封裝處理并發請求的 all 方法
+ all(...promise) {
+ return Promise.all(promise)
+ }// coding...
}// coding...
?? /pages/test/test.js
import instance from '../../utils/http'Page({async getData() {// 使用 Promise.all 同時處理多個異步請求const [res1, res2] = await instance.all([instance.get('/mall-api/index/findBanner'),instance.get('/mall-api/index/findCategory1')])console.log(res1)console.log(res2)}
})
10. 請求封裝-添加 loading
思路分析:
在封裝時添加 loading
效果,從而提高用戶使用體驗
-
在請求發送之前,需要通過
wx.showLoading
展示loading
效果 -
當服務器響應數據以后,需要調用
wx.hideLoading
隱藏loading
效果
要不要加 loading 添加到 WxRequest 內部 ?
在類內部進行添加,方便多個項目直接使用類提供的 loading 效果,也方便統一優化 wx.showLoading 使用體驗。
但是不方便自己來進行 loading 個性化定制。
如果想自己來控制 loading 效果,帶來更豐富的交互體驗,就不需要將 loading 封裝到類內部,但是需要開發者自己來優化 wx.showLoading 使用體驗,每個項目都要寫一份。
大伙可以按照自己的業務需求進行封裝,
在項目中我們會選擇第一種方式。折中
不過也會通過屬性控制是否展示 loading,從而方便類使用者自己控制 loading 顯示
落地代碼:
?? utils/request.js
class WxRequest {// coding...constructor(options = {}) {// coding...}// 創建 request 請求方法request(options) {// 拼接完整的請求地址options.url = this.defaults.baseURL + options.url// 合并請求參數options = { ...this.defaults, ...options }+ // 發送請求之前添加 loding
+ wx.showLoading()// 如果存在請求攔截器,我們則調用請求攔截器if (this.interceptors.request) {// 請求之前,觸發請求攔截器options = this.interceptors.request(options)}// 方法返回一個 Promise 對象return new Promise((resolve, reject) => {wx.request({...options,success: (res) => {// coding...},fail: (err) => {// coding...},
+ complete: () => {
+ // 接口調用完成后隱藏 loding
+ wx.hideLoading()
+ }})})}// coding...
}
11. 請求封裝-完善 loading
思路分析:
目前在發送請求時,請求發送之前會展示 loading
,響應以后會隱藏 loading
。
但是 loading 的展示和隱藏會存在以下問題:
- 每次請求都會執行
wx.showLoading()
,但是頁面中只會顯示一個,后面的loading
會將前面的覆蓋 - 同時發起多次請求,只要有一個請求成功響應就會調用
wx.hideLoading
,導致其他請求還沒完成,也不會loading
- 請求過快 或 一個請求在另一個請求后立即觸發,這時候會出現
loading
閃爍問題
我們通過 隊列 的方式解決這三個問題:首先在類中新增一個實例屬性 queue
,初始值是一個空數組
- 發起請求之前,判斷
queue
如果是空數組則顯示loading
,然后立即向queue
新增請求標識 - 在
complete
中每次請求成功結束,從queue
中移除一個請求標識,queue
為空時隱藏loading
- 為了解決網絡請求過快產生
loading
閃爍問題,可以使用定時器來做判斷即可
落地代碼:
?? utils/request.js
class WxRequest {// coding...constructor(options = {}) {// 使用 Object.assign 合并默認參數以及傳遞的請求參數this.defaults = Object.assign({}, this.defaults, options)// 定義攔截器對象,包含請求攔截器和響應攔截器方法,方便在請求或響應之前進行處理。this.interceptors = {// 請求攔截器request: null,// 響應攔截器response: null}+ // 初始化 queue 數組,用于存儲請求隊列
+ this.queue = []}// 創建 request 請求方法request(options) {
+ // 如果有新的請求,則清空上一次的定時器
+ this.timerId && clearTimeout(this.timerId)// 拼接完整的請求地址options.url = this.defaults.baseURL + options.url// 合并請求參數options = { ...this.defaults, ...options }// 如果存在請求攔截器,我們則調用請求攔截器if (this.interceptors.request) {// 請求之前,觸發請求攔截器options = this.interceptors.request(options)}+ // 發送請求之前添加 loding
+ this.queue.length === 0 && wx.showLoading()
+ // 然后想隊列中添加 request 標識,代表需要發送一次新請求
+ this.queue.push('request')// 方法返回一個 Promise 對象return new Promise((resolve, reject) => {wx.request({...options,success: (res) => {// coding...},fail: (err) => {// coding...},complete: () => {// 接口調用完成后隱藏 loding// wx.hideLoading()+ // 每次請求結束后,從隊列中刪除一個請求標識
+ this.queue.pop()
+
+ // 如果隊列已經清空,在往隊列中添加一個標識
+ this.queue.length === 0 && this.queue.push('request')+ // 等所有的任務執行完以后,經過 100 毫秒
+ // 將最后一個 request 清除,然后隱藏 loading
+ this.timerId = setTimeout(() => {
+ this.queue.pop()
+ this.queue.length === 0 && wx.hideLoading()
+ }, 100)}})})}// 封裝快捷請求方法// coding...// 封裝攔截器// coding...
}// coding...export default instance
12. 請求封裝-控制 loading 顯示
思路分析:
在我們封裝的網絡請求文件中,通過 wx.showLoading
默認顯示了 loading
效果
但是在實際開發中,有的接口可能不需要顯示 loading
效果,或者開發者希望自己來控制 loading
的樣式與交互,那么就需要關閉默認 loading
效果。
這時候我們就需要一個開關來控制 loading
顯示。
- 類內部設置默認請求參數
isLoading
屬性,默認值是true
,在類內部根據isLoading
屬性做判斷即可 - 某個接口不需要顯示
loading
效果,可以在發送請求的時候,可以新增請求配置isLoading
設置為false
- 整個項目都不需要顯示
loading
效果,可以在實例化的時候,傳入isLoading
配置為false
實現步驟:
-
在 WxRequest 類的默認請求配置項中,設置 isLoading 默認值為 true,顯示 loading
class WxRequest {// 初始化默認的請求屬性defaults = {url: '', // 開發者服務器接口地址data: null, // 請求參數header: {}, // 設置請求的 headertimeout: 60000, // 超時時間method: 'GET', // 請求方式 + isLoading: true // 是否顯示 loading 提示框}// code... }
-
在進行實例化的時候,可以配置 isLoading 配置為 false,隱藏 loading
// 對 WxRequest 進行實例化 const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api', + isLoading: false // 隱藏 loading })
-
在發送網絡請求時候,傳入請求配置 isLoading 配置為 false,隱藏 loading
async func() { + // 請求配置 isLoading 配置為 false,隱藏 loading + await instance.get('/index/findCategory1', null, { isLoading: true }) }
-
wx-request 內部代碼實現
// 創建 WxRequest 類,采用類的方式進行封裝會讓方法更具有復用性,也可以方便進行添加新的屬性和方法class WxRequest {// 初始化默認的請求屬性defaults = {url: '', // 開發者服務器接口地址data: null, // 請求參數header: {}, // 設置請求的 headertimeout: 60000, // 超時時間method: 'GET', // 請求方式 + isLoading: true // 是否顯示 loading 提示框}constructor(params = {}) {// coding...}request(options) {// coding...+ // 發送請求之前添加 loding + if (options.isLoading) { + this.queue.length === 0 && wx.showLoading() + // 然后想隊列中添加 request 標識,代表需要發送一次新請求 + this.queue.push('request') + }// 請求之前,觸發請求攔截器// 如果存在請求攔截器,則觸發請求攔截器if (this.interceptors.request) {options = this.interceptors.request(options)}// 使用 Promise 封裝異步請求return new Promise((resolve, reject) => {// 使用 wx.request 發起請求wx.request({...options,// 接口調用成功的回調函數success: (res) => {// coding...},// 接口調用失敗的回調函數fail: (err) => {// coding...},complete: () => {// 接口調用完成后隱藏 loding// wx.hideLoading()+ if (!options.isLoading) return// 每次請求結束后,從隊列中刪除一個請求標識this.queue.pop()// 如果隊列已經清空,在往隊列中添加一個標識this.queue.length === 0 && this.queue.push('request')// 等所有的任務執行完以后,經過 100 毫秒// 將最后一個 request 清除,然后隱藏 loadingthis.timerId = setTimeout(() => {this.queue.pop()this.queue.length === 0 && wx.hideLoading()}, 100)}})})}// coding... }
13. 請求封裝-封裝 uploadFile
思路分析:
wx.uploadFile
也是我們在開發中常用的一個 API
,用來將本地資源上傳到服務器。
例如:在獲取到微信頭像以后,將微信頭像上傳到公司服務器。
wx.uploadFile({url: '', // 必填項,開發者服務器地址filePath: '', // 必填項,要上傳文件資源的路徑 (本地路徑)name: '' // 必填項,文件對應的 key,開發者在服務端可以通過這個 key 獲取文件的二進制內容
})
在了解了 API 以后,我們直接對 wx.uploadFile
進行封裝即可。
首先在 WxRequest
類內部創建 upload
實例方法,實例方法接收四個屬性:
/**
* @description 文件上傳接口封裝
* @param { string } url 文件上傳地址
* @param { string } filePath 要上傳文件資源的路徑
* @param { string } name 文件對應的 key
* @param { string } config 其他配置項
* @returns
*/
upload(url, filePath, name, config = {}) {return this.request(Object.assign({ url, filePath, name, method: 'UPLOAD' }, config))
}
這時候我們需要在 request
實例方法中,對 method
進行判斷,如果是 UPLOAD
,則調用 wx.uploadFile
上傳API
// request 實例方法接收一個對象類型的參數
// 屬性值和 wx.request 方法調用時傳遞的參數保持一致
request(options) {// coding...// 需要使用 Promise 封裝 wx.request,處理異步請求return new Promise((resolve, reject) => {
+ if (options.method === 'UPLOAD') {
+ wx.uploadFile({
+ ...options,
+
+ success: (res) => {
+ // 將服務器響應的數據通過 JSON.parse 轉換為 JS 對象
+ res.data = JSON.parse(res.data)
+
+ const mergeRes = Object.assign({}, res, {
+ config: options,
+ isSuccess: true
+ })
+
+ resolve(this.interceptors.response(mergeRes))
+ },
+
+ fail: (err) => {
+ const mergeErr = Object.assign({}, err, {
+ config: options,
+ isSuccess: true
+ })
+
+ reject(this.interceptors.response(mergeErr))
+ },
+
+ complete: () => {
+ this.queue.pop()
+
+ this.queue.length === 0 && wx.hideLoading()
+ }
+ })} else {wx.request({// coding...})}})
}
落地代碼:
?? utils/request.js
// request 實例方法接收一個對象類型的參數
// 屬性值和 wx.request 方法調用時傳遞的參數保持一致
request(options) {// coding...// 需要使用 Promise 封裝 wx.request,處理異步請求return new Promise((resolve, reject) => {
+ if (options.method === 'UPLOAD') {
+ wx.uploadFile({
+ ...options,
+
+ success: (res) => {
+ // 將服務器響應的數據通過 JSON.parse 轉換為 JS 對象
+ res.data = JSON.parse(res.data)
+
+ const mergeRes = Object.assign({}, res, {
+ config: options,
+ isSuccess: true
+ })
+
+ resolve(this.interceptors.response(mergeRes))
+ },
+
+ fail: (err) => {
+ const mergeErr = Object.assign({}, err, {
+ config: options,
+ isSuccess: true
+ })
+
+ reject(this.interceptors.response(mergeErr))
+ },
+
+ complete: () => {
+ this.queue.pop()
+
+ this.queue.length === 0 && wx.hideLoading()
+ }
+ })} else {wx.request({// coding...})}})
}
test/test.js
Page({/*** 頁面的初始數據*/data: {avatarUrl: '../../assets/Jerry.png'},// 獲取微信頭像async chooseavatar(event) {// 目前獲取的微信頭像是臨時路徑// 臨時路徑是有失效時間的,在實際開發中,需要將臨時路徑上傳到公司的服務器const { avatarUrl } = event.detail// 調用 upload 方法發送請求,將臨時路徑上傳到公司的服務器const res = await instance.upload('/fileUpload',event.detail.avatarUrl,'file')// 將返回的數據賦值給 data 中的數據this.setData({avatarUrl: res.data})},// coding...
}
14. 請求封裝-使用 npm 包發送請求
思路分析:
封裝的網絡請求模塊發布到了 npm
,如果你在學習網絡請求模塊封裝時感覺比較吃力,可以先使用 npm 包實現功能。
npm install mina-request
📌 構建 npm:
? 安裝包后,需要在微信開發者工具中進行 npm 構建,點擊
工具
??構建 npm
其余步驟參考文檔進行開發即可:
mina-request 地址
落地代碼:
import WxRequest from "./request";
import { env } from "./env ";
// 是否顯示重新登錄
let isRelogin = { show: false };
// ----------------- 實例化 ----------------------
// 對 WxRequest 進行實例化
const instance = new WxRequest({baseURL: env.baseURL,timeout: 15000,
});// 配置請求攔截器
instance.interceptors.request = (config) => {// 在發送請求之前做些什么console.log(config, "在發送請求之前做些什么");// 從本地獲取 tokenif (wx.getStorageSync("token")) {// 如果存在 token ,則添加請求頭config.header["token"] = wx.getStorageSync("token");}// 返回請求參數return config;
};// 響應攔截器
instance.interceptors.response = (response) => {console.log(response, "響應攔截器");const { isSuccess, data } = response;// isSuccess: false 表示是網絡超時或其他問題,提示 網絡異常,同時將返回即可if (!isSuccess) {wx.showToast({title: '"網絡異常,請稍后重試~"',icon: "error",});// 如果請求錯誤,將錯誤的結果返回出去return response;}switch (data.code) {case 200:return data;case 208:// 控制多個接口觸發,彈框只出現一次if (!isRelogin.show) {isRelogin.show = true;wx.showModal({showCancel: false,title: "提示",content: "登錄授權過期,請重新授權",complete: (res) => {console.log(res);// 清空tokenwx.removeStorageSync("token");// 返回首頁wx.reLaunch({url: "/pages/login/login",});// 點擊確認后恢復狀態isRelogin.show = false;},});}// 將錯誤繼續向下傳遞return Promise.reject(response);default:wx.showToast({title: "接口調用失敗~~~~",icon: "none",});// 將錯誤繼續向下傳遞return Promise.reject(response);}
};// 將 WxRequest 的實例通過模塊化的方式暴露出去
export default instance;
15. 環境變量-小程序設置環境變量
知識點:
在實際開發中,不同的開發環境,調用的接口地址是不一樣的。
例如:開發環境需要調用開發版的接口地址,生產環境需要調用正式版的接口地址
這時候,我們就可以使用小程序提供了 wx.getAccountInfoSync()
接口,用來獲取當前賬號信息,在賬號信息中包含著 小程序 當前環境版本。
環境版本 | 合法值 |
---|---|
開發版 | develop |
體驗版 | trial |
正式版 | release |
落地代碼:
// 獲取當前帳號信息
const accountInfo = wx.getAccountInfoSync()// 獲取小程序項目的 appId
console.log(accountInfo.miniProgram.appId)
// 獲取小程序 當前環境版本
console.log(accountInfo.miniProgram.envVersion)
根據環境的不同,我們給 env 變量設置不同的請求基準路徑 baseURL
然后將 env
環境變量導出
// 獲取 小程序帳號信息
const { miniProgram } = wx.getAccountInfoSync();// 獲取小程序當前開發環境
// develop 開發版, trial 體驗版, release 正式版
const { envVersion } = miniProgram;let env = {baseURL: "https://gmall-prod.atguigu.cn/mall-api",
};switch (envVersion) {case "develop":env.baseURL = "https://gmall-prod.atguigu.cn/mall-api";break;case "trial":env.baseURL = "https://gmall-prod.atguigu.cn/mall-api";break;case "release":env.baseURL = "https://gmall-prod.atguigu.cn/mall-api";break;default:console.log("當前環境異常");env.baseURL = "https://gmall-prod.atguigu.cn/mall-api";
}export { env };
16. 接口調用方式說明
思路分析:
在開發中,我們會將所有的網絡請求方法放置在 api 目錄下統一管理,然后按照模塊功能來劃分成對應的文件,在文件中將接口封裝成一個個方法單獨導出,例如:
// 導入封裝的網絡請求工具 http.js
import http from '../utils/http'/*** @description 獲取輪播圖數據* @returns Promise*/
export const reqBannerData = () => http.get('/index/findBanner')
這樣做的有以下幾點好處:
- 易于維護:一個文件就是一個模塊,一個方法就是一個功能,清晰明了,查找方便
- 便于復用:哪里使用,哪里導入,可以在任何一個業務組件中導入需要的方法
- 團隊合作:分工合作
落地代碼:
// 導入封裝的網絡請求工具 http.js
import http from '../utils/http'/*** @description 獲取輪播圖數據* @returns Promise*/
export const reqSwiperData = () => http.get('/mall-api/index/findBanner')
// 導入接口 API
import { reqSwiperData } from '../../api/index'Page({// 頁面數據data: {swiperList: []},// 小程序頁面加載時執行onLoad () {// 調用獲取首頁數據的方法getHomeList()}// 獲取首頁數據async getHomeList() {// 獲取輪播圖數據const res = await reqSwiperData()console.log(res)}
})