這個API很“迷人”——(新的Fetch API)
原文:https://hacks.mozilla.org/2015/03/this-api-is-so-fetching
原標題是This API is So Fetching,Fetching也可以表示迷人的意思——譯者注
JavaScript 通過XMLHttpRequest(XHR)來執行異步請求,這個方式已經存在了很長一段時間。雖說它很有用,但它不是最佳API。它在設計上不符合職責分離原則,將輸入、輸出和用事件來跟蹤的狀態混雜在一個對象里。而且,基于事件的模型與最近JavaScript流行的Promise以及基于生成器的異步編程模型不太搭(事件模型在處理異步上有點過時了——譯者注)。
新的 Fetch API打算修正上面提到的那些缺陷。 它向JS中引入和HTTP協議中同樣的原語(即Fetch——譯者注)。具體而言,它引入一個實用的函數fetch()用來簡潔捕捉從網絡上檢索一個資源的意圖。
Fetch 規范的API明確了用戶代理獲取資源的語義。它結合ServiceWorkers,嘗試達到以下優化:
- 改善離線體驗
- 保持可擴展性
到寫這篇文章的時候,Fetch API被Firefox 39(Nightly版)以及Chrome 42(開發版)支持。在github上,有基于低版本瀏覽器的兼容實現
特性檢測
要檢查是否支持Fetch API,可以通過檢查 Headers, Request, Response 或者 fetch 在 window 或者 worker 作用域中是否存在。
簡單的fetching示例
在Fetch API中,最常用的就是fetch()函數。它接收一個URL參數,返回一個promise來處理response。response參數帶著一個Response對象。
fetch("/data.json").then(function(res) { // res instanceof Response == true. if (res.ok) { res.json().then(function(data) { console.log(data.entries); }); } else { console.log("Looks like the response wasn't perfect, got status", res.status); } }, function(e) { console.log("Fetch failed!", e); });
如果是提交一個POST請求,代碼如下:
fetch("http://www.example.org/submit.php", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "firstName=Nikhil&favColor=blue&password=easytoguess" }).then(function(res) { if (res.ok) { alert("Perfect! Your settings are saved."); } else if (res.status == 401) { alert("Oops! You are not authorized."); } }, function(e) { alert("Error submitting form!"); });
fetch()函數的參數和傳給Request()構造函數的參數保持完全一致,所以你可以直接傳任意復雜的request請求給fetch()。
Headers
Fetch引入了3個接口,它們分別是 Headers,Request 以及 Response 。他們直接對應了相應的HTTP概念,但是基于安全考慮,有些區別,例如支持CORS規則以及保證cookies不能被第三方獲取。
Headers接口是一個簡單的多映射的名-值表
var content = "Hello World"; var reqHeaders = new Headers(); reqHeaders.append("Content-Type", "text/plain" reqHeaders.append("Content-Length", content.length.toString()); reqHeaders.append("X-Custom-Header", "ProcessThisImmediately");
也可以傳一個多維數組或者json:
reqHeaders = new Headers({ "Content-Type": "text/plain", "Content-Length": content.length.toString(), "X-Custom-Header": "ProcessThisImmediately", });
Headers的內容可以被檢索:
console.log(reqHeaders.has("Content-Type")); // true console.log(reqHeaders.has("Set-Cookie")); // false reqHeaders.set("Content-Type", "text/html"); reqHeaders.append("X-Custom-Header", "AnotherValue"); console.log(reqHeaders.get("Content-Length")); // 11 console.log(reqHeaders.getAll("X-Custom-Header")); // ["ProcessThisImmediately", "AnotherValue"] reqHeaders.delete("X-Custom-Header"); console.log(reqHeaders.getAll("X-Custom-Header")); // []
一些操作不僅僅對ServiceWorkers有用,本身也提供了更方便的操作Headers的API(相對于XMLHttpRequest來說——譯者注)。
由于Headers可以在request請求中被發送或者在response請求中被接收,并且規定了哪些參數是可寫的,Headers對象有一個特殊的guard屬性。這個屬性沒有暴露給Web,但是它影響到哪些內容可以在Headers對象中被改變。
可能的值如下:
- "none": 默認的
- "request": 從Request中獲得的Headers只讀。
- "request-no-cors":從不同域的Request中獲得的Headers只讀。
- "response": 從Response獲得的Headers只讀。
- "immutable" 在ServiceWorkers中最常用的,所有的Headers都只讀。
哪一種 guard 作用于 Headers 導致什么行為,詳細定義在了這個規范中。例如,你不可以添加或者修改一個guard屬性是"request"的Request Headers的"Content-Length"屬性。同樣地,插入"Set-Cookie"屬性到一個Response headers是不允許的,因此ServiceWorkers是不能給合成的Response的headers添加一些cookies。
如果使用了一個不合法的HTTP Header屬性名,那么Headers的方法通常都拋出 TypeError 異常。如果不小心寫入了一個不可寫的屬性,也會拋出一個 TypeError 異常。除此以外的情況,失敗了并不拋出異常。例如:
var res = Response.error(); try { res.headers.set("Origin", "http://mybank.com"); } catch(e) { console.log("Cannot pretend to be a bank!"); }
Request
Request接口定義了通過HTTP請求資源的request格式。參數需要URL、method和headers,同時Request也接受一個特定的body,mode,credentials以及cache hints.
最簡單的 Request 當然是一個URL,可以通過URL來GET一個資源。
var req = new Request("/index.html"); console.log(req.method); // "GET" console.log(req.url); // "http://example.com/index.html"
你也可以將一個建好的Request對象傳給構造函數,這樣將復制出一個新的Request。
var copy = new Request(req); console.log(copy.method); // "GET" console.log(copy.url); // "http://example.com/index.html"
這種用法通常見于ServiceWorkers。
URL以外的其他屬性的初始值能夠通過第二個參數傳給Request構造函數。這個參數是一個json:
var uploadReq = new Request("/uploadImage", { method: "POST", headers: { "Content-Type": "image/png", }, body: "image data" });
mode屬性用來決定是否允許跨域請求,以及哪些response屬性可讀。可選的mode屬性值為same-origin,no-cors(默認)以及cors。
same-origin模式很簡單,如果一個請求是跨域的,那么返回一個簡單的error,這樣確保所有的請求遵守同源策略。
var arbitraryUrl = document.getElementById("url-input").value; fetch(arbitraryUrl, { mode: "same-origin" }).then(function(res) { console.log("Response succeeded?", res.ok); }, function(e) { console.log("Please enter a same-origin URL!"); });
no-cors模式允許來自CDN的腳本、其他域的圖片和其他一些跨域資源,但是首先有個前提條件,就是請求的method只能是"HEAD","GET"或者"POST"。此外,任何 ServiceWorkers 攔截了這些請求,它不能隨意添加或者改寫任何headers,除了這些。第三,JavaScript不能訪問Response中的任何屬性,這保證了 ServiceWorkers 不會導致任何跨域下的安全問題而隱私信息泄漏。
cors模式我們通常用作跨域請求來從第三方提供的API獲取數據。這個模式遵守CORS協議。只有有限的一些headers被暴露給Response對象,但是body是可讀的。例如,你可以獲得一個Flickr的最感興趣的照片的清單:
var u = new URLSearchParams(); u.append('method', 'flickr.interestingness.getList'); u.append('api_key', '<insert api key here>'); u.append('format', 'json'); u.append('nojsoncallback', '1'); var apiCall = fetch('https://api.flickr.com/services/rest?' + u); apiCall.then(function(response) { return response.json().then(function(json) { // photo is a list of photos. return json.photos.photo; }); }).then(function(photos) { photos.forEach(function(photo) { console.log(photo.title); }); });
你無法從Headers中讀取"Date"屬性,因為Flickr在Access-Control-Expose-Headers中設置了不允許讀取它。
response.headers.get("Date"); // null
credentials枚舉屬性決定了cookies是否能跨域得到。這個屬性與XHR的withCredentials標志相同,但是只有三個值,分別是"omit"(默認),"same-origin"以及"include"。
Request對象也可以提供 caching hints 給用戶代理。這個屬性還在安全復審階段。Firefox 提供了這個屬性,但是它目前還不起作用。
Request還有兩個只讀的屬性與ServiceWorks攔截有關。其中一個是referrer,表示Request的來源,可能為空。另外一個是context,是一個非常大的枚舉集合定義了獲得的資源的種類,它可能是image比如請求來自于img標簽,可能是worker如果是一個worker腳本,等等。如果使用fetch()函數,這個值是fetch。
Response
Response實例通常在fetch()的回調中獲得。但是它們也可以用JS構造,不過通常這招只用于ServiceWorkers。
Response中最常見的成員是status(一個整數默認值是200)和statusText(默認值是"OK"),對應HTTP請求的status和reason。還有一個"ok"屬性,當status為2xx的時候它是true。
headers 屬性是Response的Headers對象,它是只讀的(with guard "response"),url屬性是當前Response的來源URL。
Response 也有一個type屬性,它的值可能是"basic","cors","default","error"或者"opaque。
- "basic": 正常的,同域的請求,包含所有的headers除開"Set-Cookie"和"Set-Cookie2"。
- "cors": Response從一個合法的跨域請求獲得,一部分header和body可讀。
- "error": 網絡錯誤。Response的status是0,Headers是空的并且不可寫。當Response是從Response.error()中得到時,就是這種類型。
- "opaque": Response從"no-cors"請求了跨域資源。依靠Server端來做限制。
"error"類型會導致fetch()函數的Promise被reject并回調出一個TypeError。
還有一些屬性只在ServerWorker作用域下有效。以正確的方式 返回一個Response針對一個被ServiceWorkers攔截的Request,可以像下面這樣寫:
addEventListener('fetch', function(event) { event.respondWith(new Response("Response body", { headers: { "Content-Type" : "text/plain" } }); });
如你所見,Response有個接收兩個可選參數的構造器。第一個參數是返回的body,第二個參數是一個json,設置status、statusText以及headers。
靜態方法Response.error()簡單返回一個錯誤的請求。類似的,Response.redirect(url, status)返回一個跳轉URL的請求。
處理body
無論Request還是Response都可能帶著body。由于body可以是各種類型,比較復雜,所以前面我們故意先略過它,在這里單獨拿出來講解。
body可以是以下任何一種類型的實例:
- ArrayBuffer
- ArrayBufferView(Uint8Array and friends)
- Blob/File
- 字符串
- URLSearchParams
- FormData——目前不被Gecko和Blink支持,Firefox預計在版本39和Fetch的其他部分一起推出。
此外,Request和Response都為他們的body提供了以下方法,這些方法都返回一個Promise對象。
- arrayBuffer()
- blob()
- json()
- text()
- formData()
在使用非文本的數據方面,Fetch API和XHR相比提供了極大的便利。
可以通過傳body參數來設置Request的body:
var form = new FormData(document.getElementById('login-form')); fetch("/login", { method: "POST", body: form })
Response的第一個參數是body:
var res = new Response(new File(["chunk", "chunk"], "archive.zip", { type: "application/zip" }));
Request和Response(通過fetch()方法)都能夠自動識別自己的content type,Request還可以自動設置"Content-Type" header,如果開發者沒有設置它的話。
流和克隆
非常重要的一點要說明,那就是Request和Response的body只能被讀取一次!它們有一個屬性叫bodyUsed,讀取一次之后設置為true,就不能再讀取了。
var res = new Response("one time use"); console.log(res.bodyUsed); // false res.text().then(function(v) { console.log(res.bodyUsed); // true }); console.log(res.bodyUsed); // true res.text().catch(function(e) { console.log("Tried to read already consumed Response"); });
這樣設計的目的是為了之后兼容基于流的API,讓應用一次消費data,這樣就允許了JavaScript處理大文件例如視頻,并且可以支持實時壓縮和編輯。
有時候,我們希望多次訪問body,例如,你可能想用即將支持的Cache API去緩存Request和Response,以便于可以離線使用,Cache要求body能被再次讀取。
所以,我們該如何讓body能經得起多次讀取呢?API提供了一個clone()方法。調用這個方法可以得到一個克隆對象。不過要記得,clone()必須要在讀取之前調用,也就是先clone()再讀取。
addEventListener('fetch', function(evt) { var sheep = new Response("Dolly"); console.log(sheep.bodyUsed); // false var clone = sheep.clone(); console.log(clone.bodyUsed); // false clone.text(); console.log(sheep.bodyUsed); // false console.log(clone.bodyUsed); // true evt.respondWith(cache.add(sheep.clone()).then(function(e) { return sheep; }); });
未來的改進
為了支持流,Fetch最終將提供可以中斷執行讀取資源的能力,并且提供可以得到讀取進度的API。這些能力在XHR中有,但是想要實現成Promise-based的Fetch API有些麻煩。
你可以加入WHATWG的郵件組參與Fetch和ServiceWorker的討論,為改進API貢獻自己的力量。
為了創造更好的互聯網而努力!
感謝 Andrea Marchesini, Anne van Kesteren 和 Ben
Kelly 感謝他們對規范和實現所做的努力.
來自:http://www.w3ctech.com/topic/854