Fetch API能执行XMLHttpRequest对象的所有任务,能在Web工作线程等现代Web工具中使用,是WHATWG的一个“活标准”(living standard),用规范原文说,就是“Fetch标准定义请求、响应,以及绑定二者的流程:获取(fetch)”。
Fetch API本身是使用JavaScript请求资源的优秀工具,同时这个API也能够应用在服务线程(service worker)中,提供拦截、重定向和修改通过fetch()生成的请求接口。
XMLHttpRequest可以选择异步,而Fetch API则必须是异步。

24.5.1 基本用法

fetch()方法暴露在全局作用域中,包括主页面执行线程、模块和工作线程。
调用这个方法,浏览器会向给定URL发送请求。

1.分派请求

fetch()只有一个必需的参数input。多数情况下,这个参数是要获取资源的URL。返回一个期约。

  1. let r = fetch('/bar');
  2. console.log(r) // Promise <pending>

URL的格式(相对路径、绝对路径等)的解释与XHR对象一样。
请求完成、资源可用时,期约会解决为一个Response对象。
这个对象是API的封装,可通过它取得相应资源。
获取资源要用这个对象的属性和方法,掌握响应的情况,并将负载转换为有用的形式。
如下所示:

  1. fetch('bar.text')
  2. .then((response) => {
  3. console.log(response);
  4. });
  5. // Response { type: 'basic', url: ...}

2.读取响应

读取响应内容的最简单方式是:取得纯文本格式的内容,要用text()方法。返回一个期约,会解决为取得资源的完整内容。

  1. fetch('bar.text')
  2. .then((response) => {
  3. response.text().then((data) => {
  4. console.log(data)
  5. });
  6. });
  7. // bar.text的内容

内容的结构通常是打平的:

  1. fetch('bar.text')
  2. .then((response) => response.text())
  3. .then((data) => console.log(data));
  4. // bar.text的内容

3.处理状态码和请求失败

Fetch API支持通过Response的status(状态码)和statusText(状态文本)属性检查响应状态。

  1. fetch('/bar')
  2. .then((response) => {
  3. console.log(response.status);
  4. console.log(response.statusText);
  5. })
  6. // 成功获取响应的请求通常会产生值为200的状态码:
  7. // 第一行打印:200,第二行打印:OK
  8. // 请求不存在的资源通常会产生值为404的状态码:
  9. // 第一行打印:404,第二行打印:NotFound
  10. // 请求的URL如果抛出服务器错误会产生值为500的状态码:
  11. // 第一行打印:500,第二行打印:InternalServerError

可显式设置fetch()在遇到重定向时的行为(本章后面会介绍),但默认行为是:跟随重定向并返回状态码不是300~399的响应。
跟随重定向时,响应对象的redirected属性会被设置为true,而状态码仍然是200。
在前面这几个例子中,虽然请求可能失败(如状态码为500),但都只执行了期约的解决处理函数。
事实上,只要服务器返回了响应,fetch()期约都会解决。
这个行为是合理的:系统级网络协议已经成功完成消息的一次往返传输。
至于真正的“成功”请求,则需要在处理响应时再定义。
通常状态码为200时就会被认为成功,其他情况可以被认为未成功。
为区分这两种情况,可在状态码非200~299时检查Response对象的ok属性。
因为服务器没有响应而导致浏览器超时,这样真正的fetch()失败会导致期约被拒绝:

  1. fetch('/hangs-forever')
  2. .then((response) => {
  3. console.log(response);
  4. }, (err) => {
  5. console.log(err);
  6. });
  7. // (浏览器超时后)
  8. // TypeError: "NetworkError when attempting to fetch resource"

违反CORS、无网络连接、HTTPS错配及其他浏览器/网络策略问题都会导致期约被拒绝。
可通过url属性检查通过fetch()发送请求时使用的完整URL

4.自定义选项

只使用URL时,fetch()会发送GET请求,只包含最低限度的请求头。
要进一步配置如何发送请求,需要传入可选的第二个参数init对象。
init对象要按照下表中的键/值进行填充。(见书中图片)

24.5.2 常见Fetch请求模式

与XMLHttpRequest一样,fetch()既可发送数据也可接收数据。
使用init对象参数,可配置fetch()在请求体中发送各种序列化的数据。

1.发送JSON数据

可以像下面这样发送简单JSON字符串:

  1. let payload = JSON.stringify({
  2. foo: 'bar'
  3. });
  4. let jsonHeaders = new Headers({
  5. 'Content-Type': 'application/json'
  6. });
  7. fetch('/send-me-json', {
  8. method: 'POST', // 发送请求体时必须使用一种HTTP方法
  9. body: payload,
  10. headers: jsonHeaders
  11. });

2.在请求体中发送参数

因为请求体支持任意字符串值,所以可以通过它发送请求参数:

  1. let payload = 'foo=bar&baz=qux';
  2. let paramHeaders = new Headers({
  3. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
  4. });
  5. fetch('/send-me-params', {
  6. method: 'POST', // 发送请求体时必须使用一种HTTP方法
  7. body: payload,
  8. headers: paramHeaders
  9. });

3.发送文件

因为请求体支持FormData实现,所以fetch()也可以序列化并发送文件字段中的文件:

  1. let imageFormData = new FormData();
  2. let imageInput = document.querySelector("input[type='file']");
  3. imageFormData.append('image', imageInput.files[0]);
  4. fetch('/img-upload', {
  5. method: 'POST',
  6. body: imageFormData
  7. });
  8. // 这个fetch()实现可以支持多个文件:
  9. let imageFormData = new FormData();
  10. let imageInput = document.querySelector("input[type='file']");
  11. for (let i = 0; i < imageInput.files.length; ++i) {
  12. imageFormData.append('image', imageInput.files[i]);
  13. }
  14. fetch('/img/upload', {
  15. method: 'POST',
  16. body: imageFormData
  17. });

4.加载Blob文件

Fetch API也能提供Blob类型的响应,而Blob又可以兼容多种浏览器API。
一种常见的做法是:明确将图片文件加载到内存,然后将其添加到HTML图片元素。
为此,可用响应对象上暴露的blob()方法。
这个方法返回一个期约,解决为一个Blob的实例。
然后,可将这个实例传给URL.createObjectUrl(),以生成可以添加给图片元素src属性的值.

  1. const imageElement = document.querySelector('img');
  2. fetch('my-image.png')
  3. .then((response) => response.blob())
  4. .then((blob) => {
  5. imageElement.src = URL.createObjectURL(blob);
  6. });

5.发送跨源请求

从不同的源请求资源,响应要包含CORS头部才能保证浏览器收到响应。
没有这些头部,跨源请求会失败并抛出错误。

  1. fetch('//cross-origin.com');
  2. // TypeError: Failed to fetch
  3. // No 'Access-Control-Allow-Origin' header is present on the requested resource
  4. // 如果代码不需要访问响应,也可以发送no-cors请求。
  5. // 此时响应的type属性值为opaque,因此无法读取响应内容。
  6. // 这种方式适合发送探测请求或者将响应缓存起来供以后使用。
  7. const imageElement = document.querySelector('img');
  8. fetch('//cross-origin.com', { method: 'no-cors'})
  9. .then((response) => console.log(response.type));
  10. // opaque

6.中断请求

Fetch API支持通过AbortController/AbortSignal对中断请求。
调用AbortController. abort()会中断所有网络传输,特别适合希望停止传输大型负载的情况。
中断进行中的fetch()请求会导致包含错误的拒绝。

  1. let abortController = new AbortController();
  2. fetch('wikipedia.zip', { signal: abortController.signal })
  3. .catch(() => console.log('中断请求!'));
  4. // 10毫秒后中断请求
  5. setTimeout(() => abortController.abort(), 10);
  6. // 已经中断

24.5.3 Headers对象

Headers对象是所有外发请求和入站响应头部的容器。
每个外发的Request实例都包含一个空的Headers实例,可通过Request.prototype.headers访问;
每个入站Response实例也可通过Response.prototype.headers访问包含着响应头部的Headers对象。
这两个属性都是可修改属性。
另外,使用new Headers()也可以创建一个新实例。

1.Headers与Map的相似之处

Headers对象与Map对象极为相似。这是合理的。
因为HTTP头部本质上是序列化后的键/值对,它们的JavaScript表示则是中间接口。
Headers与Map类型都有get()、set()、has()和delete()等实例方法。
set 设置键或更新值,has 检查键,get 获取值,delete 删除值
Headers和Map都可以使用一个可迭代对象来初始化

  1. let seed = [['foo', 'bar']];
  2. let h = new Headers(seed);
  3. let m = new Map(seed);
  4. console.log(h.get('foo')); // bar
  5. console.log(m.get('foo')); // bar

而且,它们也都有相同的keys()、values()和entries()迭代器接口:

  1. let seed = [['foo', 'bar'], ['baz', 'qux']];
  2. let h = new Headers(seed);
  3. let m = new Map(seed);
  4. console.log(...h.keys()); // baz foo
  5. console.log(...m.keys()); // foo baz
  6. console.log(...h.values()); // qux bar
  7. console.log(...m.values()); // bar qux
  8. console.log(...h.entries()); // (2) ["baz", "qux"] (2) ["foo", "bar"]
  9. console.log(...m.entries()); // (2) ["foo", "bar"] (2) ["baz", "qux"]

2.Headers独有的特性

在初始化Headers对象时,也可以使用键/值对形式的对象,而Map则不可以.

  1. let seed = {foo: 'bar'};
  2. let h = new Headers(seed);
  3. let m = new Map(seed);
  4. console.log(h.get('foo')); // bar
  5. console.log(m.get('foo'));
  6. // Uncaught TypeError: object is not iterable (cannot read property
  7. // Symbol(Symbol.iterator)) at new Map (<anonymous>)

一个HTTP头部字段可以有多个值,而Headers对象通过append()方法支持添加多个值。
在Headers实例中还不存在的头部上调用append()方法相当于调用set()。
后续调用会以逗号为分隔符拼接多个值。

  1. let h = new Headers();
  2. h.append('foo', 'bar');
  3. console.log(h.get('foo')); // bar
  4. h.append('foo', 'baz');
  5. console.log(h.get('foo')); // bar, baz

3.头部护卫

某些情况下,并非所有HTTP头部都可以被客户端修改,而Headers对象使用护卫来防止不被允许的修改。
不同的护卫设置会改变set()、append()和delete()的行为。违反护卫限制会抛出TypeError。
Headers实例会因来源不同而展现不同的行为,它们的行为由护卫来控制。
JavaScript可以决定Headers实例的护卫设置。
下表列出了不同的护卫设置和每种设置对应的行为:
image.png

24.5.4 Request对象

顾名思义,Request对象是获取资源请求的接口。
这个接口暴露了请求的相关信息,也暴露了使用请求体的不同方式。

1.创建Request对象

可通过构造函数初始化Request对象。
为此需要传入一个input参数,一般是URL

  1. let r = new Request('https://foo.com');
  2. console.log(r);
  3. // Request {...}

Request构造函数也接收第二个参数——一个init对象。
这个init对象与前面介绍的fetch()的init对象一样。
没有在init对象中涉及的值则会使用默认值。

  1. // 用所有默认值创建Request对象
  2. console.log(new Request(''));
  3. // Request {
  4. // bodyUsed: false
  5. // cache: "default"
  6. // credentials: "same-origin"
  7. // destination: ""
  8. // headers: Headers {}
  9. // integrity: ""
  10. // isHistoryNavigation: false
  11. // keepalive: false
  12. // method: "GET"
  13. // mode: "cors"
  14. // redirect: "follow"
  15. // referrer: "about:client"
  16. // referrerPolicy: ""
  17. // signal: AbortSignal {aborted: false, onabort: null}
  18. // url: "file:///Users/xxx/Desktop/test.html"
  19. // }
  20. // 用指定的初始值创建Request对象
  21. console.log(new Request('https://foo.com', { method: 'POST' }));
  22. // Request {
  23. // bodyUsed: false
  24. // cache: "default"
  25. // credentials: "same-origin"
  26. // destination: ""
  27. // headers: Headers {}
  28. // integrity: ""
  29. // isHistoryNavigation: false
  30. // keepalive: false
  31. // method: "POST"
  32. // mode: "cors"
  33. // redirect: "follow"
  34. // referrer: "about:client"
  35. // referrerPolicy: ""
  36. // signal: AbortSignal {aborted: false, onabort: null}
  37. // url: "https://foo.com/"
  38. // }

2.克隆Request对象

Fetch API提供了两种不太一样的方式用于创建Request对象的副本:
使用Request构造函数和使用clone()方法。
将Request实例作为input参数传给Request构造函数,会得到该请求的一个副本;
若再传入init对象,则init对象的值会覆盖源对象中同名的值。
这种克隆方式并不总能得到一模一样的副本:
第一个请求的请求体会被标记为“已使用”。
若源对象与创建的新对象不同源,则referrer属性会被清除。
若源对象的mode为navigate,则会被转换为same-origin。

  1. let r1 = new Request('https://foo.com');
  2. let r2 = new Request(r1);
  3. console.log(r2.url); // https://foo.com
  4. let r1 = new Request('https://foo.com');
  5. let r2 = new Request(r1, {method: 'POST'});
  6. console.log(r1.method); // GET
  7. console.log(r2.method); // POST
  8. let r1 = new Request('https://foo.com', {method: 'POST', body: 'foobar'});
  9. let r2 = new Request(r1);
  10. console.log(r1.bodyUsed); // true
  11. console.log(r2.bodyUsed); // false

第二种克隆Request对象的方式是使用clone()方法,这个方法会创建一模一样的副本,任何值都不会被覆盖。
与第一种方式不同,这种方法不会将任何请求的请求体标记为“已使用”。

  1. let r1 = new Request('https://foo.com', {method: 'POST', body: 'foobar'});
  2. let r2 = r1.clone();
  3. console.log(r1.url); // https://foo.com
  4. console.log(r2.url); // https://foo.com
  5. console.log(r1.bodyUsed); // false
  6. console.log(r2.bodyUsed); // false

若请求对象的bodyUsed属性为true(即请求体已被读取),那么上述任何一种方式都不能用来创建这个对象的副本。在请求体被读取之后再克隆会导致抛出TypeError。

3.在fetch()中使用Request对象

fetch()和Request构造函数拥有相同的函数签名并不是巧合。
调用fetch()时,可传入已经创建好的Request实例而不是URL。
与Request构造函数一样,传给fetch()的init对象会覆盖传入请求对象的值。
fetch()会在内部克隆传入的Request对象。
与克隆Request一样,fetch()也不能拿请求体已经用过的Request对象来发送请求。
关键在于,通过fetch使用Request会将请求体标记为已使用。也就是说,有请求体的Request只能在一次fetch中使用。(不包含请求体的请求不受此限制。)
要想基于包含请求体的相同Request对象多次调用fetch(),必须在第一次发送fetch()请求前调用clone()

  1. let r = new Request('https://foo.com');
  2. // 向foo.com发送GET请求
  3. fetch(r);
  4. // 向foo.com发送POST请求
  5. fetch(r, {method: 'POST'});
  6. let r = new Request('https://foo.com', {method: 'POST', body: 'foobar'});
  7. r.text();
  8. fetch(r);
  9. // TypeError: Cannot construct a Request with a Request object that has already been used.
  10. let r = new Request('https://foo.com', {method: 'POST', body: 'foobar'});
  11. fetch(r);
  12. fetch(r);
  13. // TypeError: Cannot construct a Request with a Request object that has already been used.
  14. let r = new Request('https://foo.com', {method: 'POST', body: 'foobar'});
  15. fetch(r.clone());
  16. fetch(r.clone());
  17. fetch(r);

24.5.5 Response对象

顾名思义,Response对象是获取资源响应的接口。
这个接口暴露了响应的相关信息,也暴露了使用响应体的不同方式。

1.创建Response对象

可通过构造函数初始化Response对象且不需要参数。此时响应实例的属性均为默认值,因为它并不代表实际的HTTP响应.

  1. let r = new Response();
  2. console.log(r);
  3. // Response {
  4. // body: (...)
  5. // bodyUsed: false
  6. // headers: Headers {}
  7. // ok: true
  8. // redirected: false
  9. // status: 200
  10. // statusText: ""
  11. // type: "default"
  12. // url: ""
  13. // }

Response构造函数接收一个可选的body参数。
这个body可以是null,等同于fetch()参数init中的body。
还可接收一个可选的init对象,这个对象可以包含下表所列的键和值:
image.png
可以像下面这样使用body和init来构建Response对象:

  1. let r = new Response('foobar', {
  2. status: 418,
  3. statusText: 'I\'m a teapot'
  4. });
  5. console.log(r);
  6. // Response {
  7. // body: (...)
  8. // bodyUsed: false
  9. // headers: Headers {}
  10. // ok: false
  11. // redirected: false
  12. // status: 418
  13. // statusText: "I'm a teapot"
  14. // type: "default"
  15. // url: ""
  16. // }

大多数情况下,产生Response对象的主要方式是调用fetch(),它返回一个最后会解决为Response对象的期约,这个Response对象代表实际的HTTP响应。
下面的代码展示了这样得到的Response对象:

  1. fetch('https://foo.com/')
  2. .then((response) => {
  3. console.log(response);
  4. });

Response类还有两个用于生成Response对象的静态方法:
Response.redirect()和Response.error()。
前者接收一个URL和一个重定向状态码(301、302、303、307或308),返回重定向的Response对象。
提供的状态码必须对应重定向,否则会抛出错误
另一个静态方法Response.error()用于产生表示网络错误的Response对象(网络错误会导致fetch()期约被拒绝)。

  1. console.log(Response.redirect('重定向地址', 301));
  2. Response.redirect('重定向地址', 200);
  3. // 会报错
  4. console.log(Response.error());
  5. // Response {
  6. // body: (...)
  7. // bodyUsed: false
  8. // headers: Headers {}
  9. // ok: false
  10. // redirected: false
  11. // status: 0
  12. // statusText: ""
  13. // type: "error"
  14. // url: ""
  15. // }

2.读取响应状态信息

Response对象包含一组只读属性,描述了请求完成后的状态。
如下表所示:
image.png

3.克隆Response对象

克隆Response对象的主要方式是使用clone()方法。
这个方法会创建一个一模一样的副本,不会覆盖任何值。
这样不会将任何请求的请求体标记为已使用。
若响应对象的bodyUsed属性为true(即响应体已被读取),则不能再创建这个对象的副本。在响应体被读取之后再克隆会导致抛出TypeError。
有响应体的Response对象只能读取一次。(不包含响应体的Response对象不受此限制。)
要多次读取包含响应体的同一个Response对象,必须在第一次读取前调用clone()

  1. let r1 = new Response('foobar');
  2. let r2 = r1.clone();
  3. console.log(r1.bodyUsed); // false
  4. console.log(r2.bodyUsed); // false
  5. let r = new Response('foobar');
  6. r.clone();
  7. // 没有错误
  8. r.text(); // 设置bodyUsed为true
  9. r.clone();
  10. // Uncaught TypeError: Failed to execute 'clone' on 'Response': Response body is already used
  11. let r = new Response('foobar');
  12. r.text().then(console.log); // foobar
  13. r.text().then(console.log);
  14. // Uncaught (in promise) TypeError: Failed to execute 'text' on 'Response': body stream already read
  15. let r = new Response('foobar');
  16. r.clone().text().then(console.log);
  17. r.clone().text().then(console.log);
  18. r.text().then(console.log);
  19. // foobar 打印了3次

此外,通过创建带有原始响应体的Response实例,可以执行伪克隆操作。关键是这样不会把第一个Response实例标记为已读,而是会在两个响应之间共享

  1. let r1 = new Response('foobar');
  2. let r2 = new Response(r1.body);
  3. console.log(r1.bodyUsed); // false
  4. console.log(r2.bodyUsed); // false
  5. r2.text().then(console.log); // foobar
  6. r1.text().then(console.log);
  7. // Uncaught (in promise) TypeError: Failed to execute 'text' on 'Response': body stream already read

24.5.6 Request、Response及Body混入

Request和Response都使用了Fetch API的Body混入,以实现两者承担有效载荷的能力。
这个混入为两个类型提供了只读的body属性(实现为ReadableStream)、只读的bodyUsed布尔值(表示body流是否已读)和一组方法,用于从流中读取内容并将结果转换为某种JavaScript对象类型。
通常,将Request和Response主体作为流来使用主要有两个原因:
一个原因是有效载荷的大小可能会导致网络延迟;
另一个原因是流API本身在处理有效载荷方面是有优势的。
除此之外,最好是一次性获取资源主体。
Body混入提供了5个方法,用于将ReadableStream转存到缓冲区的内存里,将缓冲区转换为某种JavaScript对象类型,以及通过期约来产生结果。在解决之前,期约会等待主体流报告完成及缓冲被解析。这意味着客户端必须等待响应的资源完全加载才能访问其内容。

1.Body.text()

Body.text()方法返回期约,解决为将缓冲区转存得到的UTF-8格式字符串。
下面的代码展示了在Response对象上使用Body.text():
image.png
以下代码展示了在Request对象上使用Body.text():

  1. let request = new Request('https://foo.com', {method: 'POST', body: 'bbbbbbb'});
  2. request.text()
  3. .then(console.log);
  4. // bbbbbbb

2.Body.json()

Body.json()方法返回期约,解决为将缓冲区转存得到的JSON。
下面的代码展示了在Response对象上使用Body.json():
image.png
以下代码展示了在Request对象上使用Body.json():

  1. let request = new Request('https://foo.com',
  2. {method: 'POST', body: JSON.stringify({bar: 'baz'})}
  3. );
  4. request.json()
  5. .then(console.log);
  6. // {bar: "baz"}

3.Body.formData()

浏览器可以将FormData对象序列化/反序列化为主体。
例如,下面这个FormData实例:

  1. let myFormData = new FormData();
  2. myFormData.append('foo', 'bar');

在通过HTTP传送时,WebKit浏览器会将其序列化为下列内容:
image.png
Body.formData()方法返回期约,解决为将缓冲区转存得到的FormData实例。
下面的代码展示了在Response对象上使用Body.formData():
image.png
以下代码展示了在Request对象上使用Body.formData():

  1. let myFormData = new FormData();
  2. myFormData.append('foo', 'bar');
  3. let request = new Request('https://foo.com',
  4. {method: 'POST', body: myFormData}
  5. );
  6. request.formData()
  7. .then((formData) => console.log(formData.get('foo')));
  8. // bar

4.Body.arrayBuffer()

有时可能需要以原始二进制格式查看和修改主体。为此,可以使用Body.arrayBuffer()将主体内容转换为ArrayBuffer实例。
Body.arrayBuffer()方法返回期约,解决为将缓冲区转存得到的ArrayBuffer实例。

5.Body.blob()

有时可能需要以原始二进制格式使用主体,不用查看和修改。为此,可以使用Body.blob()将主体内容转换为Blob实例。
Body.blob()方法返回期约,解决为将缓冲区转存得到的Blob实例。
下面的代码展示了在Response对象上使用Body.blob():
image.png
以下代码展示了在Request对象上使用Body.blob():

  1. let request = new Request('https://foo.com',
  2. {method: 'POST', body: 'abcdefg'}
  3. );
  4. request.blob()
  5. .then(console.log);
  6. // Blob {size: 7, type: "text/plain;charset=utf-8"}

6.一次性流

因为Body混入是构建在ReadableStream之上的,所以主体流只能使用一次。这意味着所有主体混入方法都只能调
用一次,再次调用就会抛出错误。
即使是在读取流的过程中,所有这些方法也会在它们被调用时给ReadableStream加锁,以阻止其他读取器访问。
作为Body混入的一部分,bodyUsed布尔值属性表示ReadableStream是否已摄受(disturbed),意思是读取器是否已经在流上加了锁。这不一定表示流已经被完全读取。
下面的代码演示了这个属性:

  1. let request = new Request('https://foo.com',
  2. {method: 'POST', body: 'foobar'}
  3. );
  4. let response = new Response('foobar');
  5. console.log(request.bodyUsed); // false
  6. console.log(response.bodyUsed); // false
  7. request.text().then(console.log); // foobar
  8. response.text().then(console.log); // foobar
  9. console.log(request.bodyUsed); // true
  10. console.log(response.bodyUsed); // true

7.使用ReadableStream主体

JavaScript编程逻辑很多时候会将访问网络作为原子操作,比如请求是同时创建和发送的,响应数据也是以统一的格式一次性暴露出来的。
这种约定隐藏了底层的混乱,让涉及网络的代码变得很清晰。
从TCP/IP角度来看,传输的数据是以分块形式抵达端点的,而且速度受到网速的限制。
接收端点会为此分配内存,并将收到的块写入内存。
Fetch API通过ReadableStream支持在这些块到达时就实时读取和操作这些数据。
正如Stream API所定义的,ReadableStream暴露了getReader()方法,用于产生ReadableStream-DefaultReader,这个读取器可以用于在数据到达时异步获取数据块。
数据流的格式是Uint8Array。