從伺服器獲取資料

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

先決條件 JavaScript 基礎知識(參見 入門構建塊JavaScript 物件),以及 客戶端 API 基礎知識
目標 學習如何從伺服器獲取資料並使用它來更新網頁內容。

這裡的問題是什麼?

網頁由 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 的幾個示例。

獲取文字內容

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

這系列檔案將充當我們的假資料庫;在實際應用中,我們更有可能使用 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> 內部的文字相同(除非您在 value 屬性中指定了不同的值)——例如“詩歌節 1”。相應的詩歌節文字檔案為“verse1.txt”,並且與 HTML 檔案位於同一目錄中,因此只需使用檔名即可。

但是,Web 伺服器往往區分大小寫,並且檔名中沒有空格。要將“詩歌節 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> 標記上方)新增以下兩行,以預設載入詩歌節 1,並確保 <select> 元素始終顯示正確的值

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

從伺服器提供您的示例

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

要解決此問題,我們需要透過本地 Web 伺服器執行示例來對其進行測試。要了解如何執行此操作,請閱讀 我們有關設定本地測試伺服器的指南

罐頭商店

在此示例中,我們建立了一個名為“罐頭商店”的示例網站——這是一家僅銷售罐裝商品的虛構超市。您可以在 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 未找到)。如果返回了錯誤,我們將丟擲該錯誤。
  • 在響應上呼叫 json()。這將以 JSON 物件 的形式檢索資料。我們返回 response.json() 返回的 Promise。

接下來,我們將一個函式傳遞給返回的 Promise 的 then() 方法。此函式將接收一個包含響應資料(以 JSON 格式)的物件,我們將此物件傳遞給 initialize() 函式。此函式啟動在使用者介面中顯示所有產品的過程。

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

但是,一個完整的網站會更優雅地處理此錯誤,例如在使用者的螢幕上顯示一條訊息,並可能提供解決問題的方法,但我們只需要一個簡單的 console.error() 即可。

您可以自己測試失敗的情況

  1. 建立示例檔案的本地副本。
  2. 透過 Web 伺服器執行程式碼(如上所述,在 從伺服器提供示例 中)。
  3. 修改正在獲取的檔案的路徑,例如將其更改為 'produc.json'(確保拼寫錯誤)。
  4. 現在在瀏覽器中載入 index 檔案(透過 localhost:8000),並在瀏覽器開發者控制檯中檢視。您將看到類似“Fetch 問題:HTTP 錯誤: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}`));

它的工作方式與前一個非常相似,除了不使用 json(),而是使用 blob()。在這種情況下,我們希望將響應作為影像檔案返回,並且我們為此使用的資料格式是 Blob(該術語是“二進位制大物件”的縮寫,基本上可以用來表示大型檔案類物件,例如影像或影片檔案)。

成功接收 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 從伺服器獲取資料。

另請參閱

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