Fetch API

這個程式碼研究室是 Google Developers 訓練團隊開發的「開發漸進式網頁應用程式」訓練課程的一部分。建議您依序完成程式碼研究室,充分體驗本課程的價值。

如要瞭解課程的完整詳細資料,請參閱開發漸進式網頁應用程式總覽

簡介

本實驗室將逐步說明如何使用 Fetch API (擷取資源的簡單介面,比 XMLHttpRequest API 更進步)。

課程內容

  • 如何使用 Fetch API 要求資源
  • 如何使用 Fetch 傳送 GET、HEAD 和 POST 要求
  • 如何讀取及設定自訂標頭
  • CORS 的用途和限制

注意事項

  • 基本 JavaScript 和 HTML
  • 熟悉 ES2015 Promise 的概念和基本語法

軟硬體需求

  • 可存取終端機/殼層的電腦
  • 網際網路連線
  • 支援 Fetch 的瀏覽器
  • 文字編輯器
  • Nodenpm

注意:雖然並非所有瀏覽器都支援 Fetch API,但有 polyfill

從 GitHub 下載或複製 pwa-training-labs 存放區,並視需要安裝 Node.js 的 LTS 版本

開啟電腦的指令列。前往 fetch-api-lab/app/ 目錄,然後啟動本機開發伺服器:

cd fetch-api-lab/app
npm install
node server.js

你隨時可以按下 Ctrl-c 終止伺服器。

開啟瀏覽器並前往 localhost:8081/。您應該會看到一個頁面,其中包含提出要求的按鈕 (但這些按鈕尚無法運作)。

注意:取消註冊所有 Service Worker,並清除 localhost 的所有 Service Worker 快取,以免干擾實驗室。在 Chrome 開發人員工具中,按一下「應用程式」分頁的「清除儲存空間」部分中的「清除網站資料」,即可達成這個目標。

使用偏好的文字編輯器開啟 fetch-api-lab/app/ 資料夾。您將在 app/ 資料夾中建構實驗室。

這個資料夾包含:

  • echo-servers/ 包含用於執行測試伺服器的檔案
  • examples/ 包含我們在實驗擷取時使用的範例資源
  • js/main.js 是應用程式的主要 JavaScript,您會在其中編寫所有程式碼
  • index.html 是範例網站/應用程式的主要 HTML 網頁
  • package-lock.jsonpackage.json 是開發伺服器和回應伺服器依附元件的設定檔,
  • server.js 是節點開發伺服器

Fetch API 的介面相對簡單,本節說明如何使用擷取功能編寫基本 HTTP 要求。

擷取 JSON 檔案

js/main.js 中,應用程式的「Fetch JSON」按鈕會附加至 fetchJSON 函式。

更新 fetchJSON 函式,要求 examples/animals.json 檔案並記錄回應:

function fetchJSON() {
  fetch('examples/animals.json')
    .then(logResult)
    .catch(logError);
}

儲存指令碼並重新整理頁面。按一下「Fetch JSON」(擷取 JSON)。控制台應會記錄擷取回應。

說明

fetch 方法會接受要擷取資源的路徑做為參數,在本例中為 examples/animals.jsonfetch 會傳回 Promise,該 Promise 會解析為 Response 物件。如果 Promise 解決,回應會傳遞至 logResult 函式。如果 Promise 遭到拒絕,catch 會接管並將錯誤傳遞至 logError 函式。

回應物件代表對要求的相關回應。其中包含回應內容,以及實用的屬性和方法。

測試無效的回覆

檢查控制台中的記錄回應。請注意 statusurlok 屬性的值。

fetchJSON 中的 examples/animals.json 資源替換為 examples/non-existent.json。更新後的 fetchJSON 函式應如下所示:

function fetchJSON() {
  fetch('examples/non-existent.json')
    .then(logResult)
    .catch(logError);
}

儲存指令碼並重新整理頁面。再次點選「擷取 JSON」,嘗試擷取這個不存在的資源。

觀察擷取作業是否順利完成,且未觸發 catch 區塊。現在請找出新回應的 statusURLok 屬性。

這兩個檔案的值應該不同 (您瞭解原因嗎?)。如果收到任何控制台錯誤,這些值是否與錯誤的內容相符?

說明

為什麼失敗的回應沒有啟動 catch 區塊?這是擷取和 Promise 的重要注意事項:不良的回應 (例如 404) 仍會解析!只有在要求無法完成時,擷取 Promise 才會遭到拒絕,因此您必須一律檢查回應的有效性。我們將在下一節驗證回應。

瞭解詳情

檢查回覆的有效性

我們需要更新程式碼,檢查回應是否有效。

main.js 中,新增函式來驗證回覆:

function validateResponse(response) {
  if (!response.ok) {
    throw Error(response.statusText);
  }
  return response;
}

然後將 fetchJSON 替換為下列程式碼:

function fetchJSON() {
  fetch('examples/non-existent.json')
    .then(validateResponse)
    .then(logResult)
    .catch(logError);
}

儲存指令碼並重新整理頁面。按一下「Fetch JSON」(擷取 JSON)。檢查控制台。現在,examples/non-existent.json 的回應應會觸發 catch 區塊。

fetchJSON 函式中的 examples/non-existent.json 替換為原始的 examples/animals.json。更新後的函式應如下所示:

function fetchJSON() {
  fetch('examples/animals.json')
    .then(validateResponse)
    .then(logResult)
    .catch(logError);
}

儲存指令碼並重新整理頁面。按一下「Fetch JSON」(擷取 JSON)。您應該會看到回應已成功記錄,與先前相同。

說明

現在我們已新增 validateResponse 檢查,不良回應 (例如 404) 會擲回錯誤,並由 catch 接管。這樣我們就能處理失敗的回應,並防止非預期的回應在擷取鏈中向下傳播。

閱讀回覆

擷取的回應會以 ReadableStreams (streams 規格) 表示,且必須讀取才能存取回應主體。回應物件有相關的方法

main.js 中,新增具有下列程式碼的 readResponseAsJSON 函式:

function readResponseAsJSON(response) {
  return response.json();
}

然後將 fetchJSON 函式替換為下列程式碼:

function fetchJSON() {
  fetch('examples/animals.json') // 1
  .then(validateResponse) // 2
  .then(readResponseAsJSON) // 3
  .then(logResult) // 4
  .catch(logError);
}

儲存指令碼並重新整理頁面。按一下「Fetch JSON」(擷取 JSON)。檢查控制台,確認系統記錄的是 examples/animals.json 中的 JSON,而非 Response 物件。

說明

我們來回顧一下近期趨勢。

步驟 1:系統會對資源 (examples/animals.json) 呼叫 Fetch。Fetch 會傳回 promise,該 promise 會解析為 Response 物件。promise 解決後,回應物件會傳遞至 validateResponse

步驟 2:validateResponse 會檢查回應是否有效 (是否為 200)。如果不是,系統會擲回錯誤,略過其餘的 then 區塊,並觸發 catch 區塊。這點尤其重要。如果沒有這項檢查,錯誤的回覆就會傳遞到鏈結中,並可能導致後續程式碼中斷,因為這些程式碼可能需要接收有效的回覆。如果回應有效,就會傳遞至 readResponseAsJSON

步驟 3:readResponseAsJSON 會使用 Response.json() 方法讀取回應主體。這個方法會傳回解析為 JSON 的 Promise。這個 Promise 解析後,JSON 資料會傳遞至 logResult。(如果 response.json() 的 Promise 遭到拒絕,系統會觸發 catch 區塊)。

步驟 4:最後,logResult 會記錄原始要求傳送至 examples/animals.json 的 JSON 資料。

瞭解詳情

Fetch 不限於 JSON,在本範例中,我們會擷取圖片並附加至網頁。

main.js 中,使用下列程式碼編寫 showImage 函式:

function showImage(responseAsBlob) {
  const container = document.getElementById('img-container');
  const imgElem = document.createElement('img');
  container.appendChild(imgElem);
  const imgUrl = URL.createObjectURL(responseAsBlob);
  imgElem.src = imgUrl;
}

然後新增 readResponseAsBlob 函式,將回應讀取為 Blob

function readResponseAsBlob(response) {
  return response.blob();
}

使用下列程式碼更新 fetchImage 函式:

function fetchImage() {
  fetch('examples/fetching.jpg')
    .then(validateResponse)
    .then(readResponseAsBlob)
    .then(showImage)
    .catch(logError);
}

儲存指令碼並重新整理頁面。按一下「擷取圖片」。頁面應該會顯示一隻可愛的狗狗撿回木棍 (這是個「fetch」雙關笑話!)。

說明

在本範例中,系統正在擷取圖片 examples/fetching.jpg。如同上一個練習,回應會透過 validateResponse 驗證。接著,系統會將回應讀取為 Blob (而非上一節中的 JSON)。系統會建立圖片元素並附加至網頁,且圖片的 src 屬性會設為代表 Blob 的資料網址。

注意:網址物件的 createObjectURL() 方法可用來產生代表 Blob 的資料網址。請務必注意這一點。您無法直接將圖片來源設為 Blob。Blob 必須轉換為資料網址。

瞭解詳情

這個部分是選填挑戰。

更新 fetchText 函式,

  1. 擷取 /examples/words.txt
  2. 使用 validateResponse 驗證回覆
  3. 以文字形式讀取回應 (提示:請參閱 Response.text())
  4. 並在網頁上顯示文字

您可以使用這個 showText 函式做為顯示最終文字的輔助工具:

function showText(responseAsText) {
  const message = document.getElementById('message');
  message.textContent = responseAsText;
}

儲存指令碼並重新整理頁面。按一下「擷取文字」。如果已正確導入 fetchText,網頁上應該會顯示新增的文字。

注意: 您可能會想擷取 HTML 並使用 innerHTML 屬性附加,但請務必小心。這可能會導致網站遭受跨網站指令碼攻擊

瞭解詳情

根據預設,擷取作業會使用 GET 方法,擷取特定資源。但擷取作業也可以使用其他 HTTP 方法。

發出 HEAD 要求

headRequest 函式替換為下列程式碼:

function headRequest() {
  fetch('examples/words.txt', {
    method: 'HEAD'
  })
  .then(validateResponse)
  .then(readResponseAsText)
  .then(logResult)
  .catch(logError);
}

儲存指令碼並重新整理頁面。按一下「HEAD 要求」。請注意,記錄的文字內容為空白。

說明

fetch 方法可以接收第二個選用參數 init。這個參數可設定擷取要求,例如要求方法、快取模式、憑證

在本範例中,我們使用 init 參數將擷取要求方法設為 HEAD。HEAD 要求與 GET 要求類似,但回應主體為空白。如果您只需要檔案的中繼資料,不需要傳輸所有檔案資料,就可以使用這類要求。

選用:找出資源大小

讓我們查看 examples/words.txt 的擷取回應標頭,判斷檔案大小。

更新 headRequest 函式,記錄回應 headerscontent-length 屬性 (提示:請參閱標頭說明文件和 get 方法)。

更新程式碼後,請儲存檔案並重新整理頁面。按一下「HEAD 要求」。控制台應記錄 examples/words.txt 的大小 (以位元組為單位)。

說明

在這個範例中,我們使用 HEAD 方法要求資源的大小 (以位元組為單位,以 content-length 標頭表示),但不會實際載入資源本身。實務上,這可用於判斷是否應要求完整資源 (或甚至如何要求)。

選用:使用其他方法找出 examples/words.txt 的大小,並確認該大小與回應標頭中的值相符 (您可以查詢特定作業系統的做法,使用指令列可獲得加分!)。

瞭解詳情

Fetch 也可以透過 POST 要求傳送資料。

設定 Echo 伺服器

在本範例中,您需要執行 Echo 伺服器。在 fetch-api-lab/app/ 目錄中執行下列指令 (如果指令列遭到 localhost:8081 伺服器封鎖,請開啟新的指令列視窗或分頁):

node echo-servers/cors-server.js

這項指令會在 localhost:5000/ 啟動簡易伺服器,將傳送至該伺服器的要求回傳。

你隨時可以按下 ctrl+c 終止這個伺服器。

提出 POST 要求

postRequest 函式替換為下列程式碼 (如果您未完成第 4 節,請務必定義 showText 函式):

function postRequest() {
  fetch('http://localhost:5000/', {
    method: 'POST',
    body: 'name=david&message=hello'
  })
    .then(validateResponse)
    .then(readResponseAsText)
    .then(showText)
    .catch(logError);
}

儲存指令碼並重新整理頁面。按一下「POST request」(POST 要求)。觀察頁面上回傳的已傳送要求。其中應包含名稱和訊息 (請注意,我們尚未從表單取得資料)。

說明

如要使用 Fetch 提出 POST 要求,請使用 init 參數指定方法 (與我們在上一節中設定 HEAD 方法的方式類似)。我們也會在這裡設定要求主體,在本例中是簡單的字串。主體是我們要傳送的資料。

注意:在正式版中,請務必加密所有機密的使用者資料。

當資料以 POST 要求傳送至 localhost:5000/ 時,要求會以回應的形式回傳。然後使用 validateResponse 驗證回覆、以文字形式讀取,並顯示在頁面上。

在實務上,這個伺服器會代表第三方 API。

選用:使用 FormData 介面

您可以使用 FormData 介面輕鬆從表單擷取資料。

postRequest 函式中,從 msg-form 表單元素例項化新的 FormData 物件:

const formData = new FormData(document.getElementById('msg-form'));

然後將 body 參數的值替換為 formData 變數。

儲存指令碼並重新整理頁面。填寫頁面上的表單 (「Name」和「Message」欄位),然後按一下「POST」要求。查看網頁上顯示的表單內容。

說明

FormData 建構函式可以接收 HTML form,並建立 FormData 物件。這個物件會填入表單的鍵和值。

瞭解詳情

啟動非 CORS 回應伺服器

停止先前的 Echo 伺服器 (從指令列按下 ctrl+c),然後從 fetch-lab-api/app/ 目錄執行下列指令,啟動新的 Echo 伺服器:

node echo-servers/no-cors-server.js

這項指令會設定另一個簡單的 Echo 伺服器,這次是在 localhost:5001/。不過,這個伺服器並未設定為接受跨來源要求

從新伺服器擷取

現在新伺服器已在 localhost:5001/ 執行,我們可以向該伺服器傳送擷取要求。

更新 postRequest 函式,從 localhost:5001/ 擷取資料,而非 localhost:5000/。更新程式碼後,請儲存檔案、重新整理網頁,然後按一下「POST Request」

您應該會在控制台中收到錯誤訊息,指出缺少 CORS Access-Control-Allow-Origin 標頭,因此跨來源要求遭到封鎖。

使用下列程式碼更新 postRequest 函式中的 fetch,這段程式碼會使用 no-cors 模式 (如錯誤記錄檔所示),並移除對 validateResponsereadResponseAsText 的呼叫 (請參閱下方的說明):

function postRequest() {
  const formData = new FormData(document.getElementById('msg-form'));
  fetch('http://localhost:5001/', {
    method: 'POST',
    body: formData,
    mode: 'no-cors'
  })
    .then(logResult)
    .catch(logError);
}

儲存指令碼並重新整理頁面。然後填寫訊息表單,並按一下「POST Request」

觀察記錄在控制台中的回應物件。

說明

Fetch (和 XMLHttpRequest) 遵循同源政策。也就是說,瀏覽器會限制指令碼中的跨來源 HTTP 要求。當一個網域 (例如 http://foo.com/) 向另一個網域 (例如 http://bar.com/) 要求資源時,就會發生跨來源要求。

注意:跨來源要求限制經常造成混淆。許多資源 (例如圖片、樣式表和指令碼) 都是跨網域 (即跨來源) 擷取。不過,這些是相同來源政策的例外狀況。指令碼仍會限制跨原始來源要求。

由於應用程式伺服器的連接埠號碼與兩個回應伺服器不同,因此對任一回應伺服器的要求都會視為跨來源要求。不過,在 localhost:5000/ 上執行的第一個 Echo 伺服器已設定為支援 CORS (您可以開啟 echo-servers/cors-server.js 並檢查設定)。在 localhost:5001/ 上執行的全新 Echo 伺服器並非如此 (這就是我們收到錯誤的原因)。

使用 mode: no-cors 可擷取不透明的回應。這樣我們就能取得回應,但無法使用 JavaScript 存取回應 (這也是我們無法使用 validateResponsereadResponseAsTextshowResponse 的原因)。回應仍可供其他 API 使用,或由 Service Worker 進行快取。

修改要求標頭

Fetch 也支援修改要求標頭。停止 localhost:5001 (無 CORS) 迴聲伺服器,然後從第 6 節重新啟動 localhost:5000 (CORS) 迴聲伺服器:

node echo-servers/cors-server.js

還原從 localhost:5000/ 擷取的 postRequest 函式前一個版本:

function postRequest() {
  const formData = new FormData(document.getElementById('msg-form'));
  fetch('http://localhost:5000/', {
    method: 'POST',
    body: formData
  })
    .then(validateResponse)
    .then(readResponseAsText)
    .then(showText)
    .catch(logError);
}

現在,請使用 Header 介面在名為 messageHeaderspostRequest 函式中建立 Headers 物件,並將 Content-Type 標頭設為 application/json

然後將 init 物件的 headers 屬性設為 messageHeaders 變數。

body 屬性更新為字串化的 JSON 物件,例如:

JSON.stringify({ lab: 'fetch', status: 'fun' })

更新程式碼後,請儲存檔案並重新整理頁面。然後按一下「POST Request」

請注意,回應的請求現在具有 application/jsonContent-Type (而非先前的 multipart/form-data)。

現在,請在 messageHeaders 物件中加入自訂 Content-Length 標頭,並為要求指定任意大小。

更新程式碼後,請儲存檔案、重新整理網頁,然後按一下「POST Request」。請注意,這個標頭不會在回應要求中修改。

說明

標頭介面可建立及修改 Headers 物件。fetch 可以修改部分標頭,例如 Content-Type。其他屬性 (例如 Content-Length) 則受到保護,無法修改 (基於安全考量)。

設定自訂要求標頭

Fetch 支援設定自訂標頭。

postRequest 函式的 messageHeaders 物件中移除 Content-Length 標頭。新增自訂標頭 X-Custom,並提供任意值 (例如「X-CUSTOM': 'hello world'」)。

儲存指令碼、重新整理頁面,然後按一下「POST Request」

您應該會看到回應的要求包含您新增的 X-Custom 屬性。

現在,請將 Y-Custom 標頭新增至 Headers 物件。儲存指令碼、重新整理頁面,然後按一下「POST 要求」

您應該會在控制台中看到類似以下的錯誤訊息:

Fetch API cannot load http://localhost:5000/. Request header field y-custom is not allowed by Access-Control-Allow-Headers in preflight response.

說明

與跨來源要求一樣,自訂標頭必須由要求資源的伺服器支援。在本例中,我們的 Echo 伺服器已設定為接受 X-Custom 標頭,但不接受 Y-Custom 標頭 (您可以開啟 echo-servers/cors-server.js 並尋找 Access-Control-Allow-Headers,親自確認)。只要設定自訂標頭,瀏覽器就會執行預檢。也就是說,瀏覽器會先傳送 OPTIONS 要求給伺服器,判斷伺服器允許哪些 HTTP 方法和標頭。如果伺服器已設定為接受原始要求的方法和標頭,系統就會傳送要求,否則會擲回錯誤。

瞭解詳情

解決方案程式碼

如要取得有效程式碼副本,請前往 solution 資料夾。

您現在已瞭解如何使用 Fetch API!

資源

如要查看 PWA 訓練課程中的所有程式碼研究室,請參閱課程的歡迎程式碼研究室