這個程式碼研究室是 Google Developers 訓練團隊開發的「開發漸進式網頁應用程式」訓練課程的一部分。建議您依序完成程式碼研究室,充分體驗本課程的價值。
如要瞭解課程的完整詳細資料,請參閱開發漸進式網頁應用程式總覽。
簡介
本實驗室將逐步說明如何使用 Fetch API (擷取資源的簡單介面,比 XMLHttpRequest API 更進步)。
課程內容
- 如何使用 Fetch API 要求資源
- 如何使用 Fetch 傳送 GET、HEAD 和 POST 要求
- 如何讀取及設定自訂標頭
- CORS 的用途和限制
注意事項
- 基本 JavaScript 和 HTML
- 熟悉 ES2015 Promise 的概念和基本語法
軟硬體需求
注意:雖然並非所有瀏覽器都支援 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.json
和package.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.json
。fetch
會傳回 Promise,該 Promise 會解析為 Response 物件。如果 Promise 解決,回應會傳遞至 logResult
函式。如果 Promise 遭到拒絕,catch
會接管並將錯誤傳遞至 logError
函式。
回應物件代表對要求的相關回應。其中包含回應內容,以及實用的屬性和方法。
測試無效的回覆
檢查控制台中的記錄回應。請注意 status
、url
和 ok
屬性的值。
將 fetchJSON
中的 examples/animals.json
資源替換為 examples/non-existent.json
。更新後的 fetchJSON
函式應如下所示:
function fetchJSON() {
fetch('examples/non-existent.json')
.then(logResult)
.catch(logError);
}
儲存指令碼並重新整理頁面。再次點選「擷取 JSON」,嘗試擷取這個不存在的資源。
觀察擷取作業是否順利完成,且未觸發 catch
區塊。現在請找出新回應的 status
、URL
和 ok
屬性。
這兩個檔案的值應該不同 (您瞭解原因嗎?)。如果收到任何控制台錯誤,這些值是否與錯誤的內容相符?
說明
為什麼失敗的回應沒有啟動 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
函式,
- 擷取
/examples/words.txt
- 使用
validateResponse
驗證回覆 - 以文字形式讀取回應 (提示:請參閱 Response.text())
- 並在網頁上顯示文字
您可以使用這個 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
函式,記錄回應 headers
的 content-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 模式 (如錯誤記錄檔所示),並移除對 validateResponse
和 readResponseAsText
的呼叫 (請參閱下方的說明):
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 存取回應 (這也是我們無法使用 validateResponse
、readResponseAsText
或 showResponse
的原因)。回應仍可供其他 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 介面在名為 messageHeaders
的 postRequest
函式中建立 Headers 物件,並將 Content-Type
標頭設為 application/json
。
然後將 init
物件的 headers
屬性設為 messageHeaders
變數。
將 body
屬性更新為字串化的 JSON 物件,例如:
JSON.stringify({ lab: 'fetch', status: 'fun' })
更新程式碼後,請儲存檔案並重新整理頁面。然後按一下「POST Request」。
請注意,回應的請求現在具有 application/json
的 Content-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 訓練課程中的所有程式碼研究室,請參閱課程的歡迎程式碼研究室。