使用 JavaScript 發出網路請求

在現代網站和應用程式中,另一個非常常見的任務是發出網路請求,從伺服器檢索單個數據項,以更新網頁的各個部分,而無需載入整個新頁面。這個看似微小的細節對網站的效能和行為產生了巨大的影響,因此在本文中,我們將解釋這個概念並介紹使其成為可能的技術:特別是 Fetch API

預備知識 瞭解 HTMLCSS 基礎,熟悉前面課程中介紹的 JavaScript 基礎。
學習成果
  • 非同步網路請求,這是 Web 上最常見的非同步 JavaScript 用例。
  • 從網路獲取的常見資源型別:JSON、媒體資產、來自 RESTful API 的資料。
  • 如何使用 fetch() 實現非同步網路請求。

這裡有什麼問題?

一個網頁由一個 HTML 頁面和(通常)其他各種檔案組成,例如樣式表、指令碼和影像。Web 上頁面載入的基本模型是您的瀏覽器向伺服器發出一個或多個 HTTP 請求,以獲取顯示頁面所需的檔案,伺服器響應所請求的檔案。如果您訪問另一個頁面,瀏覽器會請求新的檔案,伺服器會響應這些檔案。

Traditional page loading

這種模型對許多網站都非常有效。但考慮一個數據驅動的網站。例如,像 溫哥華公共圖書館 這樣的圖書館網站。除此之外,您可以將這樣的網站視為資料庫的使用者介面。它可能允許您搜尋特定型別的書籍,或者根據您以前借閱過的書籍向您顯示可能喜歡的書籍推薦。當您執行此操作時,它需要使用要顯示的新書籍集來更新頁面。但請注意,頁面內容的大部分(包括頁面標題、側邊欄和頁尾等專案)保持不變。

傳統模型的麻煩在於,即使我們只需要更新頁面的一部分,我們也必須獲取並載入整個頁面。這效率低下,並可能導致糟糕的使用者體驗。

因此,許多網站不使用傳統模型,而是使用 JavaScript API 從伺服器請求資料並更新頁面內容,而無需頁面載入。因此,當用戶搜尋新產品時,瀏覽器只請求更新頁面所需的資料——例如,要顯示的新書籍集。

Using fetch to update pages

這裡主要的 API 是 Fetch API。它使頁面中執行的 JavaScript 能夠向伺服器發出 HTTP 請求以檢索特定資源。當伺服器提供這些資源時,JavaScript 可以使用這些資料來更新頁面,通常是透過使用 DOM 操作 API。請求的資料通常是 JSON,這是一種傳輸結構化資料的好格式,但也可以是 HTML 或純文字。

這是亞馬遜、YouTube、eBay 等資料驅動網站的常見模式。使用此模型,

  • 頁面更新速度更快,您無需等待頁面重新整理,這意味著網站感覺更快、響應更及時。
  • 每次更新下載的資料量更少,這意味著頻寬浪費更少。這在寬頻連線的桌上型電腦上可能不是一個大問題,但在移動裝置和沒有普及高速網際網路服務的國家/地區,這是一個主要問題。

注意:在早期,這種通用技術被稱為 非同步 JavaScript 和 XML (AJAX),因為它傾向於請求 XML 資料。如今通常不是這樣(您更可能請求 JSON),但結果仍然相同,“AJAX”一詞仍然經常用於描述該技術。

為了進一步加快速度,一些網站在首次請求時還會將資產和資料儲存在使用者的計算機上,這意味著在後續訪問中,它們會使用本地版本,而不是每次首次載入頁面時都下載新的副本。內容僅在更新時才從伺服器重新載入。

Fetch API

在本節中,我們將介紹幾個 Fetch API 的示例。

以下示例具有一定的複雜性,並展示瞭如何在一些實際場景中使用 Fetch API。如果您以前從未使用過 fetch,您可能希望從 Scrimba 的 First fetch MDN 學習合作伙伴 互動式教程開始,它提供了一個非常簡單的入門演練。

獲取文字內容

對於此示例,我們將從幾個不同的文字檔案中請求資料,並使用它們來填充內容區域。

這一系列檔案將充當我們虛假的資料庫;在實際應用程式中,我們更可能使用像 PHP、Python 或 Node 這樣的伺服器端語言從資料庫請求資料。但是,在這裡,我們希望保持簡單並專注於客戶端部分。

要開始此示例,請在您的計算機上的新目錄中本地複製 fetch-start.html 和四個文字檔案 — verse1.txtverse2.txtverse3.txtverse4.txt。在此示例中,當在下拉選單中選擇時,我們將獲取詩歌(您可能很熟悉)的不同詩節。

<script> 元素內部,新增以下程式碼。這儲存了對 <select><pre> 元素的引用,並向 <select> 元素添加了一個監聽器,以便當使用者選擇一個新值時,新值作為引數傳遞給名為 updateDisplay() 的函式。

js
const verseChoose = document.querySelector("select");
const poemDisplay = document.querySelector("pre");

verseChoose.addEventListener("change", () => {
  const verse = verseChoose.value;
  updateDisplay(verse);
});

讓我們定義 updateDisplay() 函式。首先,將以下內容放在您之前的程式碼塊下面——這是函式的空殼。

js
function updateDisplay(verse) {

}

我們將透過構造一個指向我們要載入的文字檔案的相對 URL 來開始我們的函式,因為我們稍後會需要它。任何時候 <select> 元素的值都與選定 <option> 內部的文字相同(除非您在值屬性中指定了不同的值)——例如“Verse 1”。對應的詩節文字檔案是“verse1.txt”,並且與 HTML 檔案在同一目錄中,因此只需檔名即可。

然而,Web 伺服器往往區分大小寫,並且檔名中沒有空格。要將“Verse 1”轉換為“verse1.txt”,我們需要將“V”轉換為小寫,刪除空格,並在末尾新增“.txt”。這可以透過 replace()toLowerCase()模板字面量 來完成。在 updateDisplay() 函式中新增以下行

js
verse = verse.replace(" ", "").toLowerCase();
const url = `${verse}.txt`;

最後,我們準備好使用 Fetch API

js
// Call `fetch()`, passing in the URL.
fetch(url)
  // fetch() returns a promise. When we have received a response from the server,
  // the promise's `then()` handler is called with the response.
  .then((response) => {
    // Our handler throws an error if the request did not succeed.
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    // Otherwise (if the response succeeded), our handler fetches the response
    // as text by calling response.text(), and immediately returns the promise
    // returned by `response.text()`.
    return response.text();
  })
  // When response.text() has succeeded, the `then()` handler is called with
  // the text, and we copy it into the `poemDisplay` box.
  .then((text) => {
    poemDisplay.textContent = text;
  })
  // Catch any errors that might happen, and display a message
  // in the `poemDisplay` box.
  .catch((error) => {
    poemDisplay.textContent = `Could not fetch verse: ${error}`;
  });

這裡有很多值得深入探討的地方。

首先,Fetch API 的入口點是一個名為 fetch() 的全域性函式,它將 URL 作為引數(它接受另一個可選引數用於自定義設定,但我們這裡不使用)。

接下來,fetch() 是一個非同步 API,它返回一個 Promise。如果您不知道那是什麼,請閱讀有關 非同步 JavaScript 的模組,特別是有關 promise 的課程,然後回到這裡。您會發現該文章也談到了 fetch() API!

因此,由於 fetch() 返回一個 promise,我們將一個函式傳遞給返回 promise 的 then() 方法。當 HTTP 請求從伺服器收到響應時,將呼叫此方法。在處理程式中,我們檢查請求是否成功,如果失敗則丟擲錯誤。否則,我們呼叫 response.text(),以文字形式獲取響應正文。

結果發現 response.text() 也是非同步的,所以我們返回它返回的 promise,並將一個函式傳遞到這個新 promise 的 then() 方法中。當響應文字準備好時,將呼叫此函式,並在其中使用文字更新我們的 <pre> 塊。

最後,我們在末尾鏈上一個 catch() 處理程式,以捕獲我們呼叫或其處理程式中的任何非同步函式丟擲的任何錯誤。

這個例子目前存在的一個問題是它在首次載入時不會顯示任何詩歌。為了解決這個問題,在程式碼底部(緊鄰結束的 </script> 標籤上方)新增以下兩行,以預設載入第一節,並確保 <select> 元素始終顯示正確的值

js
updateDisplay("Verse 1");
verseChoose.value = "Verse 1";

從伺服器提供您的示例

如果您只是從本地檔案執行示例,現代瀏覽器將不會執行 HTTP 請求。這是由於安全限制(有關 Web 安全的更多資訊,請閱讀 網站安全)。

為了解決這個問題,我們需要透過執行本地 Web 伺服器來測試示例。要了解如何執行此操作,請參閱 如何設定本地測試伺服器?

罐頭店

在此示例中,我們建立了一個名為 The Can Store 的示例網站——它是一個只銷售罐頭食品的虛構超市。您可以在 GitHub 上檢視此示例,並檢視原始碼

A fake e-commerce site showing search options in the left hand column, and product search results in the right-hand column.

預設情況下,該網站顯示所有產品,但您可以使用左側欄中的表單控制元件按類別、搜尋詞或兩者兼有來篩選它們。

有很多複雜的程式碼處理按類別和搜尋詞篩選產品,操作字串以便資料在 UI 中正確顯示等。我們不會在文章中討論所有這些,但您可以在程式碼中找到詳細的註釋(參見 can-script.js)。

但是,我們將解釋 Fetch 程式碼。

使用 Fetch 的第一個程式碼塊可以在 JavaScript 的開頭找到

js
fetch("products.json")
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.json();
  })
  .then((json) => initialize(json))
  .catch((err) => console.error(`Fetch problem: ${err.message}`));

fetch() 函式返回一個 promise。如果它成功完成,第一個 .then() 塊中的函式將包含從網路返回的 response

在這個函式中,我們

  • 檢查伺服器是否沒有返回錯誤(例如 404 Not Found)。如果返回了,我們丟擲錯誤。
  • 對響應呼叫 json()。這將以 JSON 物件 的形式檢索資料。我們返回 response.json() 返回的 promise。

接下來,我們將一個函式傳遞到該返回 promise 的 then() 方法中。該函式將獲得一個包含 JSON 形式的響應資料的物件,我們將其傳遞給 initialize() 函式。正是 initialize() 啟動了在使用者介面中顯示所有產品的過程。

為了處理錯誤,我們在鏈的末尾連結了一個 .catch() 塊。如果 promise 因某種原因失敗,它就會執行。在其中,我們包含一個作為引數傳遞的函式,一個 err 物件。此 err 物件可用於報告發生的錯誤的性質,在此示例中我們使用簡單的 console.error() 來完成。

然而,一個完整的網站會透過在使用者的螢幕上顯示訊息並提供補救情況的選項來更優雅地處理此錯誤,但我們不需要比簡單的 console.error() 更多的東西。

您可以自己測試失敗案例

  1. 製作示例檔案的本地副本。
  2. 透過 Web 伺服器執行程式碼(如上文 從伺服器提供您的示例 中所述)。
  3. 修改要獲取的檔案的路徑,例如將其更改為“produc.json”(確保拼寫錯誤)。
  4. 現在在瀏覽器中載入索引檔案(透過 localhost:8000)並檢視瀏覽器開發人員控制檯。您將看到一條類似於“Fetch problem: HTTP error: 404”的訊息。

第二個 Fetch 塊可以在 fetchBlob() 函式中找到

js
fetch(url)
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.blob();
  })
  .then((blob) => showProduct(blob, product))
  .catch((err) => console.error(`Fetch problem: ${err.message}`));

這與前一個工作方式大致相同,不同之處在於我們使用 blob() 而不是 json()。在這種情況下,我們希望以影像檔案形式返回響應,我們為此使用的資料格式是 Blob(該術語是“Binary Large Object”的縮寫,基本上可以用來表示大型檔案狀物件,例如影像或影片檔案)。

一旦我們成功接收到 blob,我們就會將其傳遞給我們的 showProduct() 函式,該函式會顯示它。

XMLHttpRequest API

有時,尤其是在舊程式碼中,您會看到另一個名為 XMLHttpRequest(通常縮寫為“XHR”)的 API 用於發出 HTTP 請求。這早於 Fetch,並且是第一個廣泛用於實現 AJAX 的 API。我們建議您儘可能使用 Fetch:它是一個更簡單的 API,並且比 XMLHttpRequest 具有更多功能。我們不會介紹使用 XMLHttpRequest 的示例,但我們將向您展示我們第一個罐頭店請求的 XMLHttpRequest 版本會是什麼樣子

js
const request = new XMLHttpRequest();

try {
  request.open("GET", "products.json");

  request.responseType = "json";

  request.addEventListener("load", () => initialize(request.response));
  request.addEventListener("error", () => console.error("XHR error"));

  request.send();
} catch (error) {
  console.error(`XHR error ${request.status}`);
}

這分為五個階段

  1. 建立一個新的 XMLHttpRequest 物件。
  2. 呼叫其 open() 方法進行初始化。
  3. 為其 load 事件新增一個事件監聽器,該事件在響應成功完成時觸發。在監聽器中,我們使用資料呼叫 initialize()
  4. 為其 error 事件新增一個事件監聽器,該事件在請求遇到錯誤時觸發
  5. 傳送請求。

我們還必須將整個事情包裝在 try...catch 塊中,以處理 open()send() 丟擲的任何錯誤。

希望您認為 Fetch API 比這有所改進。特別是,請注意我們必須在兩個不同的地方處理錯誤。

總結

本文展示瞭如何開始使用 Fetch 從伺服器獲取資料。

另見

然而,本文討論了許多不同的主題,這只是觸及了皮毛。有關這些主題的更多詳細資訊,請嘗試以下文章