Fetch方法封裝、業務實踐

Webfunny前端監控發表於2020-07-03

  說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方法的封裝說到底就為了滿足更多的業務場景,而且現實的業務場景中,我在上文中基本都已經提到了。也許你不能完全理解程式碼中所寫的東西,但是你只要能理解為什麼要做這些事情,你就知道該怎麼去封裝這些請求方法了。

 

相關文章