此系列主要討論 JavaScript 發送請求的方式,
若不熟稔於 Promise 與 Async/Await,建議閱讀 上集。
就算不會 JavaScript,閱讀本系列後,您將擁有基本實作能力,
不清楚如何執行 JavaScript 的話,可參閱 W3SChools,或利用 CodePen 進行測試。
目錄
Fetch API
Fetch API [fɛtʃ] 獲取、取得之意,是 WHATWG 近年來推動的新標準,
其擁有良好的設計、Promise 的實作、API 相容性 (e.g., Service Worker、Cache) …,
儘管 規範 尚未穩定,且許多功能仍處實驗中,仍令許多開發者趨之若鶩 !
基本使用
同樣的,以 Fetch 對 https://gank.io/api/random/data/福利/20,
送出 GET 請求,並將回應提供的 影像 URI,添加至頁面為例:
Polyfill
不同於 jQuery 需額外載入,
Fetch API 是標準的 Web API,因此能夠 直接使用。
但別忘記啦 ! 新的 API 也說明 瀏覽器相容性 可能不普及,
MDN 提供了表格 (以撰文日期 2017/05/30 為準) :
可謂慘不忍睹,像 iOS safari 就要 10.1 以上才能使用,QQ 😢
不過沒關係,可以使用 自動補完函式庫 (polyfill) 解決,
polyfill-service 是個方便簡單的選擇 :
(在 HTML 的 head 元素內加入 <script src=…)
<head>
<title>Fetch API Demo</title>
<meta charset="utf-8">
<script src="//cdn.polyfill.io/v2/polyfill.min.js?features=fetch"></script>
</head>
fetch()
首先,呼叫全域的 fetch 方法,參數放置的是 目標 URI :
(當然,還有還有其他實例方式 — — 可選參數、多載函式…,詳見 MDN)
fetch('https://gank.io/api/random/data/福利/20')
fetch() 函式會回傳一個 Promise,並在解析/完成 (resolve) 後,回傳 Response 物件,
因此,能直接以 .then(onFulfilled, onRejected)
串接解析 完成 或 拒絕 的回調函式,
且能使用 Response 物件 提供的 json()
方法,將回應解析為 JSON 物件 !
fetch('https://gank.io/api/random/data/福利/20')
.then(function (res) {
return res.json();
})
.then(function (data) {
jsonHandler(data);
});
(此例沿用第一集的 jsonHandler 函式)
完成 ! (一切都是為了學習),
可前往 這裡 觀看成果 :
Async/Await
說到 Promise ,怎麼能忘記Async/Await !
再進一步改寫,跟回調函式說掰掰 ~
async function fetchGank() {
let res = await fetch('https://gank.io/api/random/data/福利/20');
// 等待 parse json
let data = await res.json();
jsonHandler(data);
}
啊啊啊啊 ~~ 變好清楚啊
需注意的是,json() 方法回傳的也是 Promise,
在其解析完成後,會回傳 JSON 物件。
因此 務必 加上 await 等待解析的結果,
否則 data 會是 undefined
!
二進制回應
Fetch 處理二進制回應,也是毫無壓力,
直接使用 res.blob(),回應就解析完成 :
async function blobDemo() {
try {
let res = await fetch('Nobita.jpg');
if (res.ok) {
// 等待 parse blob
let blob = await res.blob();
let img = document.createElement("img");
let link = document.createElement("a");
// 建立 blob URI (Object URI)
var blobUri = window.URL.createObjectURL(blob);
img.src = blobUri;
link.href = blobUri;
link.innerHTML = "Download"; // 下載提示文字
link.download = "Nobita.jpg"; // 檔案名稱
// 新增 img 與 a 元素至頁面
document.body.appendChild(img);
document.body.appendChild(link);
} else {
let text = await res.text();
console.log(text);
}
} catch (e) {
console.log(e);
}
}
Result:
不熟稔 Blob 的話,請先看 第一集 喔 !
關注點分離
(Separation of Concerns)
不同於 XMLHttpRequest 或 jQuery Ajax,所有職責都在單一類別。
Fetch API 擁有良好的 關注點分離 (Separation of Concerns, SOC),
除了已經見過的 全域 fetch 方法,以及回應的 Response 物件,
尚有 Body、 Headers 以及 Request 等介面。
其實,我們也已使用過 Body 介面了 !
還記得上例中,在回應的 Response 物件上,
使用 res.json() 來讀取 訊息主體 (Body) 並解析為 JSON 物件嗎 ?
那就是 Body 介面所提供的屬性與方法喔 !
Body
串流 (Stream)
訊息主體 (Body) 被 Request 與 Response 所實作,內容是 Byte Stream 的串流形式,
因此,一旦透過 json()
、text()
、formData()
或 arrayBuffer()
讀取後,
Body 的 bodyUsed
唯讀屬性,會設置為 true。
這代表:
Request、Response 物件,只能被讀取一次 !
以下範例,欲分別以 json 與 text 的方式讀取回應:
async function willError() {
let res = await fetch('https://gank.io/api/random/data/福利/20');
let data = await res.json();
let text = await res.text();
console.log(data);
console.log(text);
}
看似合理,卻會拋出 Body has already been consumed. 錯誤 ! 使用上需多加注意。
副本 (Clone)
復用 請求 與 回應 Body 是很常見的需求,
解決 只讀一次 最簡單的方式,就是做一個 副本 啦 !
因此 Request 與 Response 介面,都提供了 clone 方法。
接著,改寫一下上方範例 :
async function WillSuccess() {
let res = await fetch('https://gank.io/api/random/data/福利/20');
// 製作 Response 物件副本
let resCopy = res.clone();
let data = await res.json();
let text = await resCopy.text();
console.log(data);
console.log(text);
}
成功 !
Headers
Headers 表頭,是 Fetch API 中最簡單的介面了 !
首先使用 Headers 建構元實例物件 :
var myHeaders = new Headers(init);
init 參數是可選的,可以是 ByteString 形式的物件 :
var myHeaders = new Headers({
'Content-Type': 'image/jpeg',
'User-Agent': 'Hellow, World!'
});
Result:
或是另一個 Headers :
var myHeaders1 = new Headers({
'Content-Type': 'image/jpeg',
'User-Agent': 'Hellow, World!'
});
var myHeaders2 = new Headers(myHeaders1);
也可以在實例完成之後,用 append()
方法添加:
var headers = new Headers();
headers.append('How-Are-You', 'I\'m fine, Thank you.');
欲讀取 回應 (Response) 的 表頭,則是 :
var myHeaders = response.headers;
Forbidden_header_name
想取得某個表頭欄位的值,可以透過 get()
,且不區分大小寫,
若沒有該欄位,則回傳 空值 (null) :
var type = response.headers.get('Content-Type');
然而,與 XMLHttpRequest 相同,
基於安全考量,這些表頭欄位: Forbidden_header_name 是禁止添加的喔,
會由 使用者代理 (e.g., 瀏覽器) 幫你搞定 !
且 Headers 其實有一個隱藏的 Guard 屬性,
限制了可以操作的表頭欄位,完整的 Headers 用法 詳見 MDN。
內容類型
別忘記啦 ! HTML5 表單 預設編碼方式 為 application/x-www-form-urlencoded,
POST 表單 在送出資料前,需設置 Content-Type 表頭,
否則 Server 可能不會鳥你 !
最重要的是 — 開發時,務必記得處理 百分比編碼 (Percent-Encoding),
你或許會想使用 犀牛本 作者 David 寫的簡易函式 — encodeFormData
:
function encodeFormData(data) {
if (!data) return ""; // Always return a string
var pairs = []; // To hold name=value pairs
for (var name in data) { // For each name
if (!data.hasOwnProperty(name)) continue; // Skip inherited
if (typeof data[name] === "function") continue; // Skip methods
var value = data[name].toString(); // Value as string
name = encodeURIComponent(name.replace(" ", "+")); // Encode name
value = encodeURIComponent(value.replace(" ", "+")); // Encode value
pairs.push(name + "=" + value); // Remember name=value pair
}
return pairs.join('&'); // Return joined pairs separated with &
}
Input:
var data = {name: "勝", city: "Taipei"};
Output:
name=%E4%B8%AD%E5%8B%9D&city=Taipei
Request
在介紹 Request 之前,先看看完整的 fetch() 語法 :
fetch(input[, init]);
再來看看 Request() 的語法 :
Request(input, input[, init]);
沒錯,除了一個回傳值是 Promise<Response>,
另一個是 Request,他們的實例方式一模一樣 !
input
input 參數,除了 基本使用 介紹過的 目標 URI 以外,
也能是一個 Request 物件 !
還記得剛才用 clone 方法,解決 Body 只讀一次 的問題嗎 ?
也能如法炮製,直接使用建構元製作副本喔 !
var myRequest = new Request('https://example.com');
// 製作 Request 物件副本
var myRequest2 = new Request(myRequest);
init
可選的 init 參數,則是包含所有請求的進階設定,
例如,欲使用的 請求方法 — method、
請求 訊息主體 — body (GET、HEAD、DELETE…等請求方法 不應設置),
甚至 第一集 所提到的 同源政策 (Same-origin policy) 模式 — mode …,完整用法詳見 MDN。
最後,統整本節的所有介面,
一個 POST 請求就嚕出來囉 😃 :
async function post() {
try {
// 設置表頭欄位 (以一個客製化表頭為例)
var headers = new Headers({'x-just-test': 'This is a test.'});
headers.append('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
// 準備 資料酬載 (Payload)
var data = {name: "勝", city: "Taipei"};
// 以 application/x-www-form-urlencoded 的內容類型送出
// 酬載 (Payload) 需使用百分比編碼
var encodedData = encodeFormData(data);
// 請求參數 init:
//
// 請求方法 POST
// 設置表頭
// 將編碼後的酬載 置於訊息主體 (Message Body) 中
var myInit = {
method: 'POST',
headers: headers,
body: encodedData
};
// 實例請求 -- Request,設置 目標 URI 與 請求參數 init
var myRequest = new Request('demo_test_post.asp', myInit);
// 非同步執行 fetch
let res = await fetch(myRequest);
// 判斷回應狀態碼
if (res.ok) {
alert(await res.text());
} else {
let text = await res.text();
console.log(text);
}
} catch (e) {
console.log(e);
}
}
Fetch vs. jQuery
錯誤狀態
不同於 jQuery,無論回應狀態碼是 4xx 的客戶端錯誤 或 5xx 的伺服端錯誤 ,
Fetch 都視為 解析成功 (Resolved Promise),
僅在網路錯誤 或 請求被拒 才會拒絕 Promise,
因此實務開發時,回應狀態碼的判斷 必不能省 :
if (res.ok) {
...成功...
}
其等價於 :
if (200 <= res.status && res.status <= 299) {
...成功...
}
Cookie
MDN: 默認情況下,fetch在伺服端不會發送或接收任何 cookies,
如果站點依賴於維護一個用戶會話,則導致未經認證的請求(要發送 cookies,必須發送憑證表頭) 。
總結
Fetch 是用於獲取資源的低階 API,
涵蓋比 XMLHttpRequest 還全面的範疇,
儘管目前仍缺少對於請求進度的處理 (e.g., 中斷)。
另外,仍有許多的 HTTP Client Library,例如我愛用的 Request,
或是對 XHR 、 Fetch 的進一步封裝 (e.g., Vue-Resource) …。
(建議閱讀 上集 結尾的 XHR 封裝)
也因為 Promise、Async/Await 的盛行,使用上皆大同小異,
依照需求、相容性 選個最舒服來用即可 😀,
但千萬別忘記 同源政策 (Same-origin policy),
這客戶端 JavaScript 的最大的限制 !
最後,我要感謝 Gank IO 免費提供這麼好的 API,
不僅豐富了我的人生,也促使我撰寫了三集 JavaScript 發送 HTTP 請求,
希望大家多多支持它們…還有我 😂。
範例原始檔: Github。
給 S & N 的話 :
希望你們看完這三集後,
再去看之前很紅的 『在 2016 年學 JavaScript 是一種什麼樣的體驗?』,
應該已能看懂 2/3 😆 ,JS 並沒有想像中的可怕,加油 !
在《Fetch API — JavaScript 發送 HTTP 請求 (III)》中有 2 則留言
文中提到「使用 res.json() 來讀取 訊息主體 (Body) 並解析為 JSON 物件嗎 ?
那就是 Body 介面所提供的屬性與方法喔 !」
但是透過 console 觀察,看起來 .json() 方法 應該是繼承自 Response 的原型物件,而非 body 屬性 對應的 ReadableStream 物件,想請問這部分我是否理解有誤?