說Fetch之前啊,我們不得不說一說Ajax了,以前使用最多的自然是jQuery封裝的Ajax方法了,強大而且好用。
有人說了,jQuery的Ajax都已經封裝得那麼好了,你還整Fetch幹什麼,這不是多此一舉嘛。實際上,在大家寫業務需求的時候能夠感覺到,越是複雜的業務邏輯,Ajax用起來就會顯得有點費勁,因為它的非同步回撥機制,很容易讓我們陷入巢狀迴圈中,業務程式碼就會變得非常臃腫而難以理解。而Fetch請求則可以寫成同步的模式,一步一步執行,閱讀起來就非常舒服。
有人又說了,Ajax也能寫成同步的啊,用Promise封裝一個就好了。你說得對,可那不是有點多餘嗎,有現成的你不用,還得搭上一個jQuery,不划算。總之,我們使用Fetch來進行請求,輕量而且方便。
題外話:你把介面寫完了,是不是得了解一下介面請求是不是成功啦,請求成功率有多高呢;介面耗時怎麼樣啊,超時的介面又有多少;那麼如何監控介面請求的效能問題,可以看下我寫的前端監控系統:www.webfunny.cn 或者github搜尋:webfunny_monitor 歡迎瞭解。
題外話說完,下邊開始說乾貨了。
Fetch本身其實跟Ajax一樣都是對XMLHttpRequest的封裝,而且封裝的很不錯了,為什麼還要封裝呢,我們且看下fetch請求的寫法。
// fetch原本方法的寫法 fetch(url, { method: 'GET'; }).then(res => { res.json(); }).then(data => { console.log(data); }).catch(error => { console.log(error.msg); })
請看,上邊的程式碼算是最簡單的fetch請求方法了,寫出來是這個樣子的,雖然簡單,但是呼叫鏈也忒長了點兒啊,我們在看下相對較複雜一點的fetch請求寫法。
// fetch原本方法的寫法 fetch(url, { method: "POST", headers: { "Accept": "*/*", "Content-Type": "application/json;charset=utf-8", "access-token": "token" } }).then(res => { res.json() }).then(data => { console.log(data) }).catch(error => { console.log(error.msg) }) }
上邊帶一坨headers, 下邊帶一坨catch,十分噁心。像這個簡單的呼叫方法,還沒有處理超時、報錯、404等,都夠我們們喝一壺了,所以業務實踐,Fetch方法必須要封裝。
一、區分請求型別
一般請求的方式有多重型別如:PUT,POST,GET,DELETE;今天我們就以最常見的post、get為例進行封裝。不管是用什麼方法請求,請求所需要的引數和邏輯處理都是大致相同的,所以我們要做的就是把這些共用的部分提取出來。
我們先來解釋一下它的引數,url:請求路徑;param:請求引數,get、post分別用不同的方式傳入;httpCustomerOpertion(自定義引數),isHandleResult:是否需要處理錯誤結果,比如toast錯誤資訊,上報錯誤資訊等等;isShowLoading:是否顯示loading,每個介面請求都需要時間,是否顯示loading效果;customerHead:是否使用自定義的請求頭;timeout: 自定義介面超時的時間
以上引數雖然繁瑣,但都是很有必要的,程式碼如下:
/** * get 請求 * @param url * @param params * @param isHandleError * @param httpCustomerOpertion 使用者傳遞過來的引數, 用於以後的擴充套件使用者自定義的行為 * { * isHandleResult: boolen //是否需要處理錯誤結果 true 需要/false 不需要 * isShowLoading: boolen //是否需要顯示loading動畫 * customHead: object // 自定義的請求頭 * timeout: int //自定義介面超時的時間 * } * @returns {Promise} */ get(url, params = {}, httpCustomerOpertion = { isHandleResult: true, isShowLoading: true }) { if (!httpCustomerOpertion.hasOwnProperty("isHandleResult")) { httpCustomerOpertion.isHandleResult = true } if (!httpCustomerOpertion.hasOwnProperty("isShowLoading")) { httpCustomerOpertion.isShowLoading = true } const method = "GET" const fetchUrl = url + CommonTool.qs(params) // 將引數轉化到url上 const fetchParams = Object.assign({}, { method }, this.getHeaders()) return HttpUtil.handleFetchData(fetchUrl, fetchParams, httpCustomerOpertion) } /** * post 請求 * @param url * @param params * @param isHandleError * @param httpCustomerOpertion 使用者傳遞過來的引數, 用於以後的擴充套件使用者自定義的行為 * @returns {Promise} */ post(url, params = {}, httpCustomerOpertion = { isHandleResult: true, isShowLoading: true }) { if (!httpCustomerOpertion.hasOwnProperty("isHandleResult")) { httpCustomerOpertion.isHandleResult = true } if (!httpCustomerOpertion.hasOwnProperty("isShowLoading")) { httpCustomerOpertion.isShowLoading = true } const method = "POST" const body = JSON.stringify(params) // 將引數轉化成JSON字串 const fetchParams = Object.assign({}, { method, body }, this.getHeaders()) return HttpUtil.handleFetchData(url, fetchParams, httpCustomerOpertion) }
二、fetch請求業務邏輯封裝
請求方式區分開了,接下來我們對fetch的核心內容進行封裝。
1. 判斷自定義請求資訊,loading,header。
2. 對fetch請求再進行一次封裝。
3. 放棄遲到的響應,輪詢時,有可能發生這種情況,後邊發出的介面請求,提前獲得了返回結果,就需要放棄前一次的請求結果。
4. 統一處理返回結果,後臺業務返回結果,無論結果如何,說明介面正常。業務邏輯再區分正常和異常,比如token超時,業務異常等等。
5. 介面狀態判斷,第4點說了,返回結果,無論結果如何,介面都是正常的;如果介面狀態是非200型別的,說明介面本身出錯了,如404,500等,需要單獨捕獲和處理。
6. 介面超時處理,由於fetch本身並不支援超時判斷,所以我們需要利用promise.race方法來判斷超時問題。
核心程式碼如下:
/**
* 傳送fetch請求 * @param fetchUrl * @param fetchParams * @returns {Promise} */ static handleFetchData(fetchUrl, fetchParams, httpCustomerOpertion) {
// 1. 處理的第一步 const { isShowLoading } = httpCustomerOpertion if (isShowLoading) { HttpUtil.showLoading() } httpCustomerOpertion.isFetched = false httpCustomerOpertion.isAbort = false // 處理自定義的請求頭 if (httpCustomerOpertion.hasOwnProperty("customHead")) { const { customHead } = httpCustomerOpertion fetchParams.headers = Object.assign({}, fetchParams.headers, customHead) }
// 2. 對fetch請求再進行一次Promise的封裝 const fetchPromise = new Promise((resolve, reject) => { fetch(fetchUrl, fetchParams).then( response => {
// 3. 放棄遲到的響應 if (httpCustomerOpertion.isAbort) { // 3. 請求超時後,放棄遲到的響應 return } if (isShowLoading) { HttpUtil.hideLoading() } httpCustomerOpertion.isFetched = true response.json().then(jsonBody => { if (response.ok) {
// 4. 統一處理返回結果 if (jsonBody.status === 5) { // token失效,重新登入 CommonTool.turnToLogin() } else if (jsonBody.status) { // 業務邏輯報錯, 不屬於介面報錯的範疇 reject(HttpUtil.handleFailedResult(jsonBody, httpCustomerOpertion)) } else { resolve(HttpUtil.handleResult(jsonBody, httpCustomerOpertion)) } } else {
// 5. 介面狀態判斷 // http status header <200 || >299 let msg = "當前服務繁忙,請稍後再試" if (response.status === 404) { msg = "您訪問的內容走丟了…" }
Toast.info(msg, 2) reject(HttpUtil.handleFailedResult({ fetchStatus: "error", netStatus: response.status, error: msg }, httpCustomerOpertion)) } }).catch(e => { const errMsg = e.name + " " + e.message reject(HttpUtil.handleFailedResult({ fetchStatus: "error", error: errMsg, netStatus: response.status }, httpCustomerOpertion)) }) } ).catch(e => { const errMsg = e.name + " " + e.message // console.error('ERR:', fetchUrl, errMsg) if (httpCustomerOpertion.isAbort) { // 請求超時後,放棄遲到的響應 return } if (isShowLoading) { HttpUtil.hideLoading() } httpCustomerOpertion.isFetched = true httpCustomerOpertion.isHandleResult && Toast.info("網路開小差了,稍後再試吧", 2) reject(HttpUtil.handleFailedResult({ fetchStatus: "error", error: errMsg }, httpCustomerOpertion)) }) }) return Promise.race([fetchPromise, HttpUtil.fetchTimeout(httpCustomerOpertion)]) }
三、通用邏輯單獨封裝
程式碼裡註釋也非常詳盡了,我就不一一贅述了
/** * 統一處理後臺返回的結果, 包括業務邏輯報錯的結果 * @param result * ps: 通過 this.isHandleError 來判斷是否需要有fetch方法來統一處理錯誤資訊 */ static handleResult(result, httpCustomerOpertion) { if (result.status && httpCustomerOpertion.isHandleResult === true) { const errMsg = result.msg || result.message || "伺服器開小差了,稍後再試吧" const errStr = `${errMsg}(${result.status})` HttpUtil.hideLoading() Toast.info(errStr, 2) } return result } /** * 統一處fetch的異常, 不包括業務邏輯報錯 * @param result * ps: 通過 this.isHandleError 來判斷是否需要有fetch方法來統一處理錯誤資訊 */ static handleFailedResult(result, httpCustomerOpertion) { if (result.status && httpCustomerOpertion.isHandleResult === true) { const errMsg = result.msg || result.message || "伺服器開小差了,稍後再試吧" const errStr = `${errMsg}(${result.status})` HttpUtil.hideLoading() Toast.info(errStr, 2) } const errorMsg = "Uncaught PromiseError: " + (result.netStatus || "") + " " + (result.error || result.msg || result.message || "") return errorMsg } /** * 控制Fetch請求是否超時 * @returns {Promise} */ static fetchTimeout(httpCustomerOpertion) { const { isShowLoading } = httpCustomerOpertion return new Promise((resolve, reject) => { setTimeout(() => { if (!httpCustomerOpertion.isFetched) { // 還未收到響應,則開始超時邏輯,並標記fetch需要放棄 httpCustomerOpertion.isAbort = true // console.error('ERR: 請求超時') if (isShowLoading) { HttpUtil.hideLoading() } Toast.info("網路開小差了,稍後再試吧", 2) reject({ fetchStatus: "timeout" }) } }, httpCustomerOpertion.timeout || timeout) }) }
到此,fetch請求的封裝就算完成了,如此封裝的fetch請求方法可以相容很多業務場景了。
四、與後端介面的約定
當然,介面請求從來都不僅僅是前端一個人的事情,還需要跟後臺的小夥伴進行約定。 比如:返回結果包含status, status = 0,說明業務邏輯正常,我們只管取結果即可; status!=0,說明業務邏輯異常,後端將異常資訊放到msg欄位裡,前端將其Toast出來,展現給使用者。
五、封裝後的呼叫方式
export const fetchAction = (param, handleResult, handleFailResult) => { return HttpUtil.post(HttpApi.url, param).then( response => { handleResult(response.data) }).catch((e) => { handleFailResult() console.error(e) }) }
// 呼叫的結果就是這樣的
await fetchAction1
await fetchAction2
await fetchAction3
結語
Fetch方法的封裝說到底就為了滿足更多的業務場景,而且現實的業務場景中,我在上文中基本都已經提到了。也許你不能完全理解程式碼中所寫的東西,但是你只要能理解為什麼要做這些事情,你就知道該怎麼去封裝這些請求方法了。