js 幾種網路請求方式梳理——擺脫回撥地獄

有道技術團隊發表於2022-03-03
摘要
本文介紹了基於 XMLHttpRequest、Promise、async/await 等三種非同步網路請求的寫法,其中async/await 寫法允許我們以類似於同步的方式編寫非同步程式,擺脫繁瑣的回撥函式。

一、背景

為了應對越來越多的測試需求,減少重複性的工作,有道智慧硬體測試組基於 electron 開發了一系列測試提效工具。

electron 的程式語言是js,因為大家都不是專業的前端,對js不太熟悉,在編寫程式時踩了不少坑。尤其是js中的事件和網路請求,這些涉及到非同步程式設計的地方很容易出錯。

隨著工具的快速開發迭代,程式碼中出現了越來越多的巢狀的回撥函式,工具崩潰的機率也越來越大。為了解決這些問題,我們用 async/await 對這些回撥函式進行了重構,使得程式碼量下降,程式碼的可讀性和可理解性都有了大幅度提高。

本文介紹了基於 XMLHttpRequest、Promise、async/await 等三種非同步網路請求的寫法,其中 async/await 寫法允許我們以類似於同步的方式編寫非同步程式,擺脫繁瑣的回撥函式。

二、前言

在js中如果只是發起單個網路請求還不算複雜,用fetch、axios或者直接用XMLHttpRequest就能滿足要求。

但若是多個請求按順序拉取資料那寫起來就很麻煩了?,因為js中的網路請求都是非同步的,想要順序執行最常見寫法就是在回撥函式中發起下一個請求,如下面這些程式碼:

const requestOptions = {
    method: 'GET',
    redirect: 'follow'
};

fetch('https://xxx.yyy.com/api/zzz/', requestOptions)
    .then(response => response.json())
    .then(data => {
        fetch('https://xxx.yyy.com/api/aaa/'+data.id, requestOptions)
            .then(response => response.json())
            .then(data => {
                console.log(data)
            })
            .catch(error => console.error('error', error));
    })
    .catch(error => console.error('error', error));

假設我需要經過兩步獲取一個資料,如從https://xxx.yyy.com/api/zzz/獲取一個資料物件data,通過data.id得到我要獲取資料的序號,之後再發一次請求得到想要的資料。

用回撥函式的方式就類似於上面這樣,太繁瑣了,而且容易出錯,並且一旦邏輯複雜就不好改啦。

接下來梳理一下js的幾種網路請求方式,擺脫回撥地獄,希望對遇到類似問題的小夥伴有所幫助。

(一)XMLHttpRequest

首先是XMLHttpRequest,初學前端時大名鼎鼎的Ajax主要指的就是它。通過XMLHttpRequest物件建立網路請求的套路如下:

// 假設訪問http://localhost:3000/user返回json物件{"name":"YouDao"}
const xhr = new XMLHttpRequest();
const url = 'http://localhost:3000/user'

xhr.onreadystatechange = function(){
  if (this.readyState == 4 && this.status == 200){
    const json=JSON.parse(xhr.responseText)
    const name=json.name
    console.log(name)
  }
}
xhr.open('GET',url)
xhr.send()

這段程式碼首先建立一個XMLHttpRequest物件xhr,然後給xhr.onreadystatechange新增readystatechange事件的回撥函式,之後xhr.open('GET',url)初始化請求,最後由xhr.send()傳送請求。

請求傳送後,程式會繼續執行不會阻塞,這也是非同步呼叫的好處。當瀏覽器收到響應時就會進入xhr.onreadystatechange的回撥函式中去。在整個請求過程中,xhr.onreadystatechange會觸發四次,每次readyState都會自增,從1一直到4,只有到了最後階段也就是readyState為4時才能得到最終的響應資料。到達第四階段後還要根據status判斷響應的狀態碼是否正常,通常響應碼為200說明請求沒有遇到問題。這段程式碼最終會在控制檯上會打出YouDao。

可以看出,通過XMLHttpRequest處理請求的話,首先要針對每個請求建立一個XMLHttpRequest物件,然後還要對每個物件繫結readystatechange事件的回撥函式,若是多個請求串起來,想想就很麻煩。

(二)Promise

Promise是在 ECMAScript 2015 引入的,如果一個事件依賴於另一個事件返回的結果,那麼使用回撥會使程式碼變得很複雜。Promise物件提供了檢查操作失敗或成功的一種模式。如果成功,則會返回另一個Promise。這使得回撥的書寫更加規範。

通過Promise處理的套路如下:

const promise = new Promise((resolve,reject)=>{
  let condition = true;
  if (condition) {
    resolve("ok")
  } else {
    reject("failed")
  }
}).then( msg => console.log(msg))
  .catch( err => console.error(err))
  .finally( _ =>console.log("finally"))

上面這段程式碼把整個處理過程串起來了,首先建立一個Promise物件,它的構造器接收一個函式,函式的第一個引數是沒出錯時要執行的函式resolve,第二個引數是出錯後要執行的函式reject。

resolve指執行成功後then裡面的回撥函式,reject指執行失敗後catch裡執行的回撥函式。最後的finally是不論成功失敗都會執行的,可以用來做一些收尾清理工作。

基於Promise的網路請求可以用axios庫或瀏覽器自帶的fetch實現。

axios庫建立請求的套路如下:

import axios from 'axios'
const url = 'http://xxx.yyy.com/'
axios.get(url)
  .then(data => console.log(data))
  .catch(err => console.error(err))

我比較喜歡用fetch,fetch是用來代替XMLHttpRequest的瀏覽器API,它不需要導庫,fetch建立請求的方式和axios類似,在開頭已經展示過了就不重複寫了。

雖然Promise把回撥函式的編寫方式簡化了一些,但還是沒有擺脫回撥地獄,多個請求串起來的話就會像我開頭寫的那樣,在then裡面建立新的Promise,最終變成Promise地獄。

(三)async/await

async/await是在 ECMAScript 2017 引入的,可以簡化Promise的寫法,使得程式碼中的非同步函式呼叫可以按順序執行,易於理解。

下面就用開頭的那個例子說明吧:

直接用fetch獲取資料:

const requestOptions = {
    method: 'GET',
    redirect: 'follow'
};

fetch('https://xxx.yyy.com/api/zzz/', requestOptions)
    .then(response => response.json())
    .then(data => {
        fetch('https://xxx.yyy.com/api/aaa/'+data.id, requestOptions)
            .then(response => response.json())
            .then(data => {
                console.log(data)
            })
            .catch(error => console.error('error', error));
    })
    .catch(error => console.error('error', error));

用async/await改寫後:

async function demo() {
​  const requestOptions = {
    method: 'GET',
    redirect: 'follow'
  };

  const response = await fetch('https://xxx.yyy.com/api/zzz/', requestOptions);
  const data = await response.json()
  const response1 = await fetch('https://xxx.yyy.com/api/aaa/'+data.id, requestOptions)
  const data1 = await response1.json()
  console.log(data1)
}

demo().catch(error => console.error('error',error))

改寫後的程式碼是不是就很清楚了,沒有那麼多的then跟在後面了,這樣如果有一連串的網路請求也不用怕了。

當async放在一個函式的宣告前時,這個函式就是一個非同步函式,呼叫該函式會返回一個Promise。
await用於等待一個Promise物件,它只能在非同步函式中使用,await表示式會暫停當前非同步函式的執行,等待 Promise 處理完成。

這樣如果想讓一連串的非同步函式呼叫順序執行,只要把被呼叫的這些函式放到一個用async修飾的函式中,呼叫前加上await就能讓這些函式乖乖地順序執行了。

結語

通過本文的梳理,相信你已經知道怎樣避免回撥地獄了。不過需要注意的是 Promise 是2015年加入語言規範的,而 async/await 是2017年才加入到語言規範的,如果你的專案比較老或者是必須要相容老版本的瀏覽器(如IE6?),那就需要用別的方式來解決回撥地獄了。
對於 electron 只要你用的是近幾年的版本都是支援的,electron 可以當成是 chromium 和 node.js 的結合體,特別適合用來寫跨平臺的工具類桌面應用程式。

相關文章