Fetch API與async / await

by vivid 20. 二月 2019 05:48

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N190220402
出刊日期: 2019/2/20

在本站《Fetch API》文章中,介紹了Fetch API 提供類似XMLHttpRequest(XHR)的功能,讓我們可以送出HTTP請求,也說明如何透過撰寫JavaScript程式碼,利用Fetch API來送出HTTP請求,取得遠方伺服器上的資料。Fetch API使用ECMAScript 2015標準的「Promise」非同步程式設計功能來實作,比起XMLHttpRequest(XHR)更簡單,更容易使用。而async / await 技術可以讓你以同步程式碼的風格,來撰寫非同步程式碼。

在這篇文章中,將回顧一下如何使用Fetch API來送出HTTP請求,最後整合Fetch API與async / await 技術,來改良抓取資料的程式碼,改以非同步的方式來送出請求。

除了IE瀏覽器之外,大部分的瀏覽器都支援Fetch API,可參閱:「https://caniuse.com/#search=fetch」網站來查詢瀏覽器支援程度。

clip_image002

圖 1:Fetch API瀏覽器支援程度。

要送出請求到伺服器,可以直接在網頁中叫用「fetch」方法,這個方法隸屬於瀏覽器的「window」物件,此方法包含兩個參數。第一個參數用來指定遠端資源所在的「url」,第二個參數是一個選擇性的參數,它是一個「options」物件,可以用來設定請求的相關資訊,例如設定HTTP表頭,或者指定要使用Http「get」或「post」方式送請求到遠端伺服器。

我們先來看一個範例,以了解Fetch API如何利用Promise物件從遠端伺服器取得一個Response物件,並根據回傳的資料格式來擷取資料。假設網站伺服器上有一個「customers.json」檔案,其中加入以下JSON格式的顧客資料:

[
  {
    "id": 1,
    "firstName": "Michael",
    "lastName": "Jones"
  },
  {
    "id": 2,
    "firstName": "",
    "lastName": "Taylor"
  },
  {
    "id": 3,
    "firstName": "Amy",
    "lastName": "Wu"
  },
  {
    "id": 4,
    "firstName": "Caroline",
    "lastName": "Kingsley"
  },
  {
    "id": 5,
    "firstName": "Anup",
    "lastName": "Bradley"
  },
  {
    "id": 6,
    "firstName": "James",
    "lastName": "Kennedy"
  },
  {
    "id": 7,
    "firstName": "Jennifer",
    "lastName": "Sato"
  },
  {
    "id": 8,
    "firstName": "Jonathan",
    "lastName": "Gupta"
  }
]

在HTML檔案之中,透過以下JavaScript程式碼,利用Fetch API送出HTTP請求,並顯示「customers.json」檔案中顧客的清單:

 

<!DOCTYPE html>
<html>
<head>
  <meta charset = "utf-8" />
  <title> </title>
</head>
<body>
  <div id = "list"> </div>

  <table id = "customerTable" >
    <tr>
      <th> Full name </th>
      <th> First Name </th>
      <th> Last Name </th>
      <th> Id </th>
    </tr>
  </table>
  <script>
    let createNode = function ( element ) {
      return document.createElement( element );
    };

    let append = function ( parent, el ) {
      parent.appendChild( el );
    };

    document.addEventListener('DOMContentLoaded', function ( event ) {
      const url = "customers.json",
        table = document.getElementById( "customerTable" );
      var downloadCustomers = function () {
        fetch(url)
          .then( function ( response ) {
            if ( response.ok ) {
              response.json().then(function ( data ) {
                data.map( function ( customer ) {
                  const tr = createNode( 'tr' ),
                    firstNameCol = createNode( 'td' ),
                    lastNameCol = createNode( 'td' ),
                    fullNameCol = createNode( 'td' ),
                    idCol = createNode( 'id' );

                  fullNameCol.innerHTML = ` (${customer.id}) ${customer.firstName} ${customer.lastName} `;
                  firstNameCol.innerHTML = ` ${customer.firstName} `;
                  lastNameCol.innerHTML = ` ${customer.lastName} `;
                  idCol.innerHTML = ` ${customer.id} `;

                  append( tr, fullNameCol );
                  append( tr, firstNameCol );
                  append( tr, lastNameCol );
                  append( tr, idCol );
                  append( table, tr );

                });
              })
            } else {
              alert( "Customers list not available." );
            }
          })
      }
      downloadCustomers();
    });

  </script>
</body>
</html>

 

這個範例程式的執行結果參考如下:

clip_image004

圖 2:使用Fetch API取得遠端伺服器檔案資料。

範例中叫用「fetch」方法時,只有傳入一個參數來指定「url」,因此預設將會送出HTTP 「Get」請求到伺服端取得資料。前文提及Fetch API使用ECMAScript 2015標準的「Promise」非同步程式設計功能來實作,當你叫用了「fetch」方法,就會回傳一個Promise物件,代表HTTP請求的回應(HTTP Response),我們可以使用Promise物件的「then」方法來取得結果。

回傳的結果會放在型別為「ReadableStream」的「Body」屬性之中,因為「customers.json」檔案中的資料格式為JSON格式,因此範例中利用「json」方法,從「ReadableStream」取得資料。「json」方法也會回傳一個Promise物件,我們便可以在後續的「then」方法中處理取得的資料。

Async與await

「async」與「await」關鍵字是 ECMAScript 2017 版本制定的標準語法,可以搭配Promise物件來設計非同步執行的程式碼,撰寫風格類似同步程式的程式語法。我們先來了解一下非同步函式(async function)語法,只要函式前方加上「async」關鍵字,就代表它是一個非同步函式,例如以下範例程式碼:

async function myFuncAsync() {
      return 100;
    }
    myFuncAsync().then(function ( result ) {
      alert( result ) // 100
    });

「myFuncAsync」函式前方加註「async」關鍵字,這表示此方法將會回傳一個Promise物件。因此我們便可以直接在「then」方法中,來取得「myFuncAsync」函式執行的結果。我們也可以直接在非同步函式(async function)中直接叫用「Promise.resolve」方法,回傳一個Promise物件,例如以下範例程式碼,執行結果同上述範例:

async function myFuncAsync() {
  return Promise.resolve( 100 );
}
myFuncAsync().then(function ( result ) {
  alert( result ) // 100
});

 

再來我們來說明一下「await」關鍵字的用法,它只能夠出現在非同步函式之中,若在一般的方法使用到此關鍵字,會產生語法錯誤(SyntaxError),參考以下範例程式碼:

async function myFuncAsync() {
   let result = await Promise.resolve( 100 );
   alert( result ); // 100
}
myFuncAsync();

 

「await」關鍵字會讓JavaScript等到Promise的狀態變成「Settle」後,再將結果回傳。這並不會浪費CPU的資源,在等待執行結果的同時,JavaScript可以執行其它程式碼。使用「await」關鍵字可以精簡叫用Promise的「then」語法,讓程式更易閱讀。

若Promise執行過程發生錯誤,將會產生例外,我們可以利用「try/catch」語法攔截處理之,參考以下範例程式碼,直接叫用「Promise.reject」產生一個例外,例外發生之後,會直接跳到「catch」區塊執行錯誤處理程式碼:

async function myFuncAsync() {
  try {
    let result = await Promise.reject( new Error( " this is error " ));
  } catch ( e ) {
    alert( e ); //Error : this is error
  }
  alert( result );
}
myFuncAsync();

若沒有使用「try/catch」語法攔截例外錯誤,則Promise的狀態會變成「rejected」,我們可以利用Promise的「catch」方法來處理它,改寫上例如下將得到相同執行結果:

async function myFuncAsync() {
        let result = await Promise.reject( new Error( "this is error" ));
      alert( result );
    }
    myFuncAsync().catch(function ( e ) {
      alert( e ); // Error : this is error
    });

 

結合Fetch API與async/await

Fetch API與async/await可以完全整合在一起,參考以下範例程式碼:

<!DOCTYPE html>
<html>
<head>
  <meta charset = "utf-8" />
  <title></title>
</head>
<body>
  <div id = "list"> </div>

  <table id = "customerTable" border = "1">
    <tr>
      <th> Full name </th>
      <th> First Name </th>
      <th> Last Name </th>
      <th> Id </th>
    </tr>
  </table>

  <script>
    let createNode = function ( element ) {
      return document.createElement( element );
    };

    let append = function ( parent, el ) {
      parent.appendChild( el );
    };

    document.addEventListener( 'DOMContentLoaded', function ( event ) {
      const url = "customers.json",
        table = document.getElementById("customerTable");

      const downloadCustomers = async function () {
        let response = await fetch( url );

        if ( response.ok ) {
          let data = await response.json();
          console.log(d ata );
          data.map( function ( customer ) {
            const tr = createNode( 'tr' ),
              firstNameCol = createNode( 'td' ),
              lastNameCol = createNode( 'td' ),
              fullNameCol = createNode( 'td' ),
              idCol = createNode( 'td' );

            fullNameCol.innerHTML = ` (${customer.id}) ${customer.firstName} ${customer.lastName} `;
            firstNameCol.innerHTML = ` ${customer.firstName} `;
            lastNameCol.innerHTML = ` ${customer.lastName} `;
            idCol.innerHTML = ` ${customer.id} `;

            append( tr, fullNameCol );
            append( tr, firstNameCol );
            append( tr, lastNameCol );
            append( tr, idCol );
            append( table, tr );
          })

        }
        else {
          alert( "Customers list not available." );
        }
      }
      downloadCustomers();
    });


  </script>
</body>
</html>

使用箭頭函式

「fetch」與「response.json」都會回傳一個Promise物件,因此我們可以直接使用「await」關鍵字從Promise物件取得非同步執行結果。你還可以使用箭頭函式簡化更多的程式碼,參考以下範例程式:

<!DOCTYPE html>
<html>
<head>
  <meta charset = "utf-8" />
  <title> </title>
</head>
<body>
  <div id = "list"> </div>

  <table id = "customerTable" border = "1">
    <tr>
      <th> Full name </th>
      <th> First Name </th>
      <th> Last Name </th>
      <th> Id </th>
    </tr>
  </table>

  <script>
    let createNode = ( element ) => document.createElement( element );
    let append = ( parent, el ) => parent.appendChild( el );

    document.addEventListener( 'DOMContentLoaded', function ( event ) {
      const url = "customers.json",
        table = document.getElementById("customerTable");

      const downloadCustomers = async () => {
        let response = await fetch( url );

        if ( response.ok ) {
          let data = await response.json();
          console.log( data );
          data.map( function ( customer ) {
            const tr = createNode( 'tr' ),
              firstNameCol = createNode( 'td' ),
              lastNameCol = createNode( 'td' ),
              fullNameCol = createNode( 'td' ),
              idCol = createNode( 'td' );

            fullNameCol.innerHTML = ` (${customer.id}) ${customer.firstName} ${customer.lastName} `;
            firstNameCol.innerHTML = ` ${customer.firstName} `;
            lastNameCol.innerHTML = ` ${customer.lastName} `;
            idCol.innerHTML = ` ${customer.id} `;

            append( tr, fullNameCol );
            append( tr, firstNameCol );
            append( tr, lastNameCol) ;
            append( tr, idCol );
            append( table, tr );
          })

        }
        else {
          alert( "Customers list not available." );
        }
      }
      downloadCustomers();
    });


  </script>
</body>
</html>

 

以下則是加上錯誤處理的範例程式,範例中故意叫用「response.text()」方法,來取得執行結果,如此data.map這行將會發生例外錯誤,透過try/catch來攔截之。

<!DOCTYPE html>
<html>
<head>
  <meta charset = "utf-8" />
  <title></title>
</head>
<body>
  <div id = "list"> </div>

  <table id = "customerTable" border = "1">
    <tr>
      <th> Full name </th>
      <th> First Name </th>
      <th> Last Name </th>
      <th> Id </th>
    </tr>
  </table>
  <script>
    let createNode = ( element ) => document.createElement( element );
    let append = ( parent, el ) => parent.appendChild( el );

    document.addEventListener( 'DOMContentLoaded', function ( event ) {

      const url = "customers.json",
        table = document.getElementById( "customerTable" );

      const downloadCustomers = async () => {
        try {
          let response = await fetch( url );

          if ( response.ok ) {
            let data = await response.text(); //make error
            //let data = await response.json();
            console.log( data );
            data.map( function ( customer ) {
              const tr = createNode( 'tr' ),
                firstNameCol = createNode( 'td' ),
                lastNameCol = createNode( 'td' ),
                fullNameCol = createNode( 'td' ),
                idCol = createNode( 'td' );

              fullNameCol.innerHTML = ` (${customer.id}) ${customer.firstName} ${customer.lastName} `;
              firstNameCol.innerHTML = ` ${customer.firstName} `;
              lastNameCol.innerHTML = ` ${customer.lastName} `;
              idCol.innerHTML = ` ${customer.id} `;

              append( tr, fullNameCol );
              append( tr, firstNameCol );
              append( tr, lastNameCol );
              append( tr, idCol );
              append( table, tr );
            })

          } else {
            alert( "Customers list not available." );
          }
        } catch (e) {
          alert(" Error : " + e );
        }

      }
      downloadCustomers();
    });


  </script>
</body>
</html>

 

這個範例程式的執行結果參考如下:

clip_image006

圖 3:錯誤處理執行結果。

Tags:

.NET Magazine國際中文電子雜誌 | JavaScript | 許薰尹Vivid Hsu

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List