Web 全沾(渣)工程师一枚。博客文章以 Web 和 Linux 为主,偶尔也会发发日常的样子。

理解JavaScript中的Fetch API

Posted 24 March 2015

There's been some confusion around the new fetch API recently. Let's clear things up.

The first thing you'll notice about fetch is it's a massive improvement on XMLHttpRequest in terms of API design. Here's how to get some JSON using XHR:

最近关于新的Fetch API还有些让人疑惑的地方。让我们一起来理清思路。

首先你需要清楚的是fetch api是XMLHttpRequest在API设计上的巨大进步。

var xhr = new XMLHttpRequest();'GET', url);
xhr.responseType = 'json';

xhr.onload = function() {

xhr.onerror = function() {


Now mop up that vomit and take a look at how fetch does the same thing:


fetch(url).then(function(response) {
  return response.json();
}).then(function(data) {
}).catch(function() {

Mix in some ES6 arrow functions and it gets even more compact:


fetch(url).then(r => r.json())
  .then(data => console.log(data))
  .catch(e => console.log("Booo"))

And with ES7 async functions it can be structured like sync code, whilst still being async:


(async() => {
  try {
    var response = await fetch(url);
    var data = await response.json();
  } catch (e) {

Unfortunately, not everyone was throwing "SHUT UP AND TAKE MY MONEY" memes at it. One particular high-profile JavaScript community member was unconvinced…

They thought we shouldn't be adding high level features to the platform, especially as we're in dire need of lower level primitives when it comes to requests and responses.

To that I say, well, actually, that's soooo fetch, and we are going to make fetch happen. Let's clear up the misconceptions…




Don't be fooled by the nice API

A nice, compact API can be a sign of "high level", and therefore restrictive. But that isn't the case here. XHR is so bad, a lower-level and more featureful API can be also simpler and easier to use.

XHR is now defined in terms of fetch (see the calls to fetch in XHR's .send(), meaning fetch is lower level than XHR.


XHR现在在内部调用fetch,具体参考:fetch in XHR's .send(),这也意味着fetch更加底层。

Fetch isn't done yet

What exists in Chrome today doesn't cover the full spec, and the spec doesn't cover all of the planned features. In some cases this is because they haven't been designed yet, in others it's because they're dependent on other in-progress specs. Basically, we're following this pattern:


Developing iteratively so the user starts with a skateboard, scooter, bike, motorbike then car, as opposed to giving them nothing until you give them the whole car.

Bit of a rant: it bothers me that as developers, we preach iterative development and release, but when we're the customers of that approach the reaction is all too often "HOW DARE YOU PRESENT ME WITH SUCH INCOMPLETE IMPERFECTION".

The alternative would have been to sit on the feature for months (or years?) instead of getting large parts of it into developers' hands today. Iterative release also means we've been able to get feedback from real-world usage, and that steers future iterations in terms of design and priority.

Anyway, let's take a look at what fetch can do that XHR cannot:

难道我们就等着所有的特性都出来才开始工作么? 迭代的发布新特性也是因为我们需要在开发中的到现实使用的反馈,引领新的迭代。


Request/Response primitives

XHR kinda smushes the request and response together, meaning they can't be used separately. Fetch is different thanks to the Request and Response constructors. This is particularly useful within a ServiceWorker:


self.addEventListener('fetch', function(event) {
  if (event.request.url === new URL('/', location).href) {
      new Response("<h1>Hello!</h1>", {
        headers: {'Content-Type': 'text/html'}

In the above, event.request is a Request. There's no response yet, and instead of letting the browser go to the network I respond with my own Response. Alternatively, I could get the response from the network using fetch(event.request), or even get a response from the cache.

The Cache API is a store of Responses keyed against Requests, having separates allows you to add your own pairings.

This is in Chrome stable today from within a ServiceWorker. The fetch API is also available from pages in Chrome Beta.

Soon, request.context will be able to tell you the source of that request, so you can tell apart requests triggered by hyperlinks vs etc.

上面,event.request 就是一个Request。但是没有Response。为了不让浏览器去访问网络,我自己实现了一个Response。或者,我可以通过fetch(event.request),也可以从缓存中获取一个Response。

Cache API 就是通过Request来存储Response的存储,并且可以让你自己添加自定义的键值对。

很快,request.context 会告诉你request的内容,所以你就可以根据Request是链接还是图片去触发不同的事件。

'no-cors' and opaque responses

If I request // from this site using XHR or plain fetch it will fail. This is because it's a CORS request and the response doesn't have CORS headers.

However, with fetch, you can make a no-cors request:


但是,使用fetch发送一个no-cors 的请求:

fetch('//', {
  mode: 'no-cors'
}).then(function(response) {
  console.log(response.type); // "opaque"

This is similar to the request an makes. Of course, you can't read the content of the response as it could contain private information, but it can be consumed by other APIs:


self.addEventListener('fetch', function(event) {
    fetch('//', {
      mode: 'no-cors'

The above is fine within a ServiceWorker, as long as the receiver is happy with a no-cors response. is, isn't.

You can also store these responses in the Cache API for use later, which is great for CDN content such as scripts, CSS, and imagery, which often lack CORS headers.

For more on the origin of CORS, see Anne VK's article on same-origin policy.

This all works in Chrome stable today. In Chrome Canary, although you can use fetch() from a page, no-cors isn't enabled there yet (ticket).


你也可以使用Cache API将这些responses保存起来,这对于缺少CORS头的脚本,CSS,图片等非常有用。



XHR lacks streaming. You can get access to .responseText while the request is in progress, but the whole response is still going to buffer into memory.

With fetch, you get access to the low-level body stream. Say I wanted to load a massive CSV and find the value in the cell after the one containing "Jake":




fetch('/big-data.csv').then(function(response) {
  var reader = response.body.getReader();
  var partialCell = '';
  var returnNextCell = false;
  var returnCellAfter = "Jake";
  var decoder = new TextDecoder();

  function search() {
    return {
      partialCell += decoder.decode(result.value || new Uint8Array, {
        stream: !result.done

      // Split what we have into CSV 'cells'
      var cellBoundry = /(?:,|\r\n)/;
      var completeCells = partialCell.split(cellBoundry);

      if (!result.done) {
        // Last cell is likely incomplete
        // Keep hold of it for next time
        partialCell = completeCells[completeCells.length - 1];
        // Remove it from our complete cells
        completeCells = completeCells.slice(0, -1);

      for (var cell of completeCells) {
        cell = cell.trim();

        if (returnNextCell) {
          reader.cancel("No more reading needed.");
          return cell;
        if (cell === returnCellAfter) {
          returnNextCell = true;

      if (result.done) {
        throw Error("Could not find value after " + returnCellAfter);

      return search();

  return search();
}).then(function(result) {
  console.log("Got the result! It's '" + result + "'");
}).catch(function(err) {

Here I'm reading through the CSV (yes, I know my regex is naive), but with only a chunk of content in memory at a given time. Once I find the value I'm looking for, I cancel the stream, closing the connection.

response.body is a ReadableStream as defined by the streams spec. Streaming was planned from the outset, but it's one of the bits we launched without as the spec was still in progress.

TextDecoder is part of the encoding spec. If the chunk it receives via .decode(input, {stream: true}) ends with a partial multi-byte character, it will return and flush everything but that partial. The next call to decode appends onto the partial, hopefully forming a whole character.


response.body是一个 ReadableStream. “流”这部分依然在开发中。

TextDecoder 是编码规范的一部分。通过.decode(input, {stream: true}) 收到的一段内容如果是以一个多字节部分结束,那么将会保留这部分,在下次解码的时候,将内容接在这部分后面,以形成一个完整的内容。

This stuff is starting to land in Canary, here's a demo of the above, and here's a demo with a larger dataset (warning: running the demo may download many megabytes).

Streams are one of the things I'm really looking forward to having on the platform. I want to be able to stream-parse some JSON, generate some HTML as a result, and stream that to the browser's parser. JS-driven apps lack an easy way to get progressive-rendering from a single data source, streams can solve that.

Transform streams are coming soon, which would make the code above simpler. Ideally TextDecoder would be a transform stream, and another transform stream could chunk it into CSV rows. Something like:



fetch('/big-data.csv').then(function(response) {
  var csvStream = response.body
    .pipeThrough(new TextDecoder)
    .pipeThrough(new CSVDecoder); {
    // array of cell values for the first row

Transform streams also become really exciting within a ServiceWorker:


self.addEventListener('fetch', function(event) {
    fetch('video.unknowncodec').then(function(response) {
      var h264Stream = response.body

      return new Response(h264Stream, {
        headers: {'Content-type': 'video/h264'}

In the above, I'm using transform streams to take a video the browser doesn't understand, decode it with JS, and encode it in a format the browser does understand. It'd be amazing to see if the browser could do this in real time.


Stream readers & cloning

As I mentioned before, we initially shipped fetch without streams support so developers could get the other benefits sooner. To make up for a lack of streams & to subsequently offer a simple way to get common data types, we added some readers:


fetch(url).then(function(response) {
  return response.json();

That, as you might expect, reads the whole stream as JSON. Here's the full list of readers:

They exist on Request objects as well as responses, so you can use them to read (for example) POST data within a ServiceWorker.

These are true stream readers, meaning they drain the stream:


  • arrayBuffer()
  • blob()
  • formData()
  • json()
  • text()


fetch(url).then(function(response) {
  return response.json().catch(function() {
    // This does not work:
    return response.text();

The call to .text() fails as the stream has already been read. You can work around this using .clone():


fetch(url).then(function(response) {
  return response.clone().json().catch(function() {
    return response.text();

.clone() opts you into buffering. The clone gets read as JSON, but the original is still there and can be read in another format. Of course, this means the raw response data needs to be kept around in memory until all copies are read or garbage collected.

Alternatively, you could look at the headers of the response:


fetch(url).then(function(response) {
  if (response.headers.get('Content-Type') === 'application/json') {
    return response.json();
  return response.text();

This is another feature fetch has over XHR, you can decide which format to read the body as after you've inspected the headers.


Other bits

There are more features that fetch has over XHR that I'm not going to cover in too much detail, they include:


Headers class

Fetch has a headers class which can be used to read/write headers, and has an ES6 iterator.


Cache control

The cache mode lets you specify the interaction with the cache. As in, should the cache be consulted? Should the response go into the cache if it's valid? Should the response only come from the cache?

The latter is a bit contentious as it can expose user history, so it may come with a CORS restriction before it lands in Chrome.

缓存模式决定了你和缓存的操作方式。缓存应该被访问吗? 当缓存有效时,response可以从缓存读取吗?


No-credential same-origin requests

XHR forces you to serve credentials with requests to the same origin, fetch doesn't. In fact, no-credentials is the default for all requests made by fetch, making it less magic than XHR.


What's missing?

Of course, there are some features XHR has that fetch doesn't have.


Request aborting

This is the big one. In Canary you can cancel the stream, but there's no way to abort a request before headers have arrived.

We're going to fix this using cancellable promises, which other specs will benefit from. Track the discussion of this over at GitHub.



Progress events

Progress events are a high level feature that won't arrive in fetch for now. You can create your own by looking at the Content-Length header and using a pass-through stream to monitor the bytes received.

This means you can explicitly handle responses without a Content-Length differently. And of course, even if Content-Length is there it can be a lie. With streams you can handle these lies however you want.

正在进行的事情都是一些高等级的特性。你可以使用header的Content-Length 来使用“流”模拟接受数据。

这意味着你可以不使用Content-Length 而直接接收流。

Synchronous requests

Noooope. The fetch spec documents them, but they won't be part of the API. Sync requests are awful.


Couldn't this have been built on top of XHR?

XHR was an ugly baby and time has not been kind to it. It's 16 now. In a few years it'll be old enough to drink, and it's enough of a pain in the arse when it's sober.

Sure, a lot of this stuff could have been hacked on top of XHR, but it would have been a hack. Not only did fetch give us an opportunity to add lower-level features without the cruft of a badly designed API, it allowed us to create a better API for the simple things, using modern JavaScript features like promises and iterators.

If you want to stop using XHR today, there's a fetch polyfill. This is built on top of XHR, so it can't do the stuff that XHR can't, but it does give you the benefits of a nicer API.

Let's make fetch happen!

诚然,很多项目都是在Hack XHR之上完成的,但是它始终是hack方法。fetch不仅给了我们使用底层特性的机会,它也让通过现代Javascript的特性,如promise,iterators来帮助我们创建出更加优秀的API。

原文:That's so fetch!

本原创文章未经允许不得转载 | 当前页面:BLOG @ MBIOQ » 理解JavaScript中的Fetch API