从一个可读流对象中检索数据?

369

我该如何从一个ReadableStream对象中获取信息?

我正在使用Fetch API,但是文档并没有说明得很清楚。

响应体以ReadableStream的形式返回,我想访问流中的一个属性。在浏览器开发工具的响应中,我看到这些信息以JavaScript对象的形式组织成属性。

fetch('http://192.168.5.6:2000/api/car', obj)
    .then((res) => {
        if(!res.ok) {
            console.log("Failure:" + res.statusText);
            throw new Error('HTTP ' + res.status);
        } else {
            console.log("Success :" + res.statusText);
            return res.body // what gives?
        }
    })

3
谢谢您的回复。我尝试了 response.Body.json() , 但是出现了 italic TypeError: Cannot read property 'json' of undefined italic 的错误。 这是因为 bodyUsed 属性也被设置为 false 吗?然而,我可以在浏览器开发工具的响应选项卡下查看此正文内容。我想检索出其中的错误消息。 - noob
那么你的问题纯粹与错误400有关吗?如果你将处理程序更改为console.log(res.json());会发生什么?你能看到你期望的数据吗? - Ashley 'CptLemming' Wilson
@noob 如果 res.status == 200,你是不是试图将响应作为流读取? - guest271314
是我自己的问题还是那份文档就明显有误?不过我已经通过这些答案提供的解决方案进行了修复。 - Lucio
我知道已经有一段时间了,但为了保持stackoverflow的优秀,请接受正确答案。那个获得200多赞的答案。 - xaddict
14个回答

474
为了访问来自ReadableStream的数据,您需要调用其中一个转换方法(文档可在此处找到)。

例如:

fetch('https://jsonplaceholder.typicode.com/posts/1')
  .then(function(response) {
    // The response is a Response instance.
    // You parse the data into a useable format using `.json()`
    return response.json();
  }).then(function(data) {
    // `data` is the parsed version of the JSON returned from the above endpoint.
    console.log(data);  // { "userId": 1, "id": 1, "title": "...", "body": "..." }
  });

编辑:如果你的数据返回类型不是JSON,或者你不想要JSON格式,那么请使用text()

例如:

fetch('https://jsonplaceholder.typicode.com/posts/1')
  .then(function(response) {
    return response.text();
  }).then(function(data) {
    console.log(data); // this will be a string
  });

1
谢谢您的回复。我已经尝试过了,但仍然出现相同的错误,即res.body未定义。然而,在第一个then()函数中,我能够检索到状态,似乎只有body是一个ReadableStream对象。它似乎有一个名为locked的属性,该属性设置为true? - noob
1
我尝试从第一个 .then() 函数返回的 JSON 响应中访问 res.body。为了更清晰明了,我在原问题中添加了一个示例。谢谢! - noob
1
太棒了,使用React和Request Native,不知道该如何处理ReadableStream,这个方法解决了我的问题。++ - edencorbin
1
非常感谢您的回复!我已经对所有这些“可读流”API文档和块和文本文件之类的东西一无所知,现在终于有了眉目。这个方法很有效!我已经有一年多没有做后端工作了,所以我的技术变得很生疏。这看起来就像是我以前学习和编写的内容,我知道我忘记了类似的东西-这就是它! - PhilosophOtter
1
链接已失效,请尝试使用此链接:https://developer.mozilla.org/zh-CN/docs/Web/API/ReadableStream - Alex W
显示剩余5条评论

116

有些人可能会发现一个async的例子很有用:

var response = await fetch("https://httpbin.org/ip");
var body = await response.json(); // .json() is asynchronous and therefore must be awaited

json()方法将响应主体从ReadableStream转换为JSON对象。

await语句必须包含在async函数中,但是您可以直接在Chrome控制台中运行await语句(从版本62开始)。


1
有时候,对于你来说真正的答案确实是第二个哈哈,他们为什么要将 .json() 设计成异步的也许很有道理,但这并不是显而易见的。 - Sanchit Batra
1
这对我很有效,而且非常简单。谢谢! - Dan Barron

49

response.json() returns a Promise. Try ...

res.json().then(body => console.log(body));

这里的response是调用fetch(...)方法所返回的结果。


4
我尝试了这个,它打印出了 Promise 而不是 body。 - reectrix
2
尝试链式调用.then函数:fetch(...).then(res => res.json()).then(data => console.log(data)) - Óscar Gómez Alcañiz

26

如果您有一个ReadableStream并想从中获取文本,一个简短的技巧是将其包装在新的Response(或Request)中,然后使用text方法:

let text = await new Response(yourReadableStream).text();

21

虽然有点晚了,但我在使用Sharepoint框架时遇到了从Odata $batch请求产生的ReadableStream无法获得有用信息的问题。

我跟楼上的人遇到了类似的问题,但我的解决方法是使用不同于.json()的转换方法。在我的情况下,.text()运行良好。然而,需要进行一些调整才能从文本文件中获取一些有用的JSON数据。


3
谢谢!这对我有用。我从我的Laravel服务器发送了一条Illuminate http响应,使用简单的return $data;。最终,我能够通过fetch(...).then(response => response.text()).then(data => console.log(data));在浏览器中读取此响应。 - Cameron Hudson
我有一个API,它返回JWT令牌。在这种情况下,.then(response => response.text()).then(data => console.log(data)); 返回未定义。 - Saurabh

20
你可能问错了问题来解决你的问题,但这里是对你实际问题的答案。一个灵感可能是Node.js的源代码stream/consumers模块res.body是一个ReadableStream,它以Uint8Array的形式发出数据块。请注意,其他地方创建的ReadableStream对象可能发出的数据类型不同于Uint8Array,在这些情况下,需要调整本答案中概述的方法。
有多种方法可以消费这样的流:

将流转换为Uint8Array

使用new Response(stream).arrayBuffer()

如果你想一次性获取流的全部内容,最简单的方法是将其包装在一个 Response 对象中。然后,你可以使用其中的一种 方法 将对象作为字符串、JSON 对象、数组缓冲区或其他形式进行获取。例如,要将其作为数组缓冲区进行获取:
export async function streamToArrayBuffer(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
   return new Uint8Array(await new Response(stream).arrayBuffer());
}

请注意,new Response(stream) 只适用于 Uint8Array 流。如果流发出任何其他类型(例如字符串),将导致 TypeError: Received non-Uint8Array chunk 错误。

使用 stream.getReader()

下面的函数将收集所有的块到一个单独的 Uint8Array 中:

function concatArrayBuffers(chunks: Uint8Array[]): Uint8Array) {
    const result = new Uint8Array(chunks.reduce((a, c) => a + c.length, 0);
    let offset = 0;
    for (const chunk of chunks) {
        result.set(chunk, offset);
        offset += chunk.length;
    }
    return result;
}

export async function streamToArrayBuffer(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
    const chunks: Uint8Array[] = [];
    const reader = stream.getReader();
    while (true) {
        const { done, value } = await reader.read();
        if (done) {
            break;
        } else {
            chunks.push(value);
        }
    }
    return concatArrayBuffers(chunks);
}

使用异步迭代器

ReadableStream 实现了 异步迭代器协议。然而,大多数浏览器尚不支持此功能,但在 Node.js 中,您已经可以使用它(如果使用 TypeScript,您需要使用 NodeJS.ReadableStream 接口,参见 此讨论)。

以下代码将收集所有的块并存储在一个单独的 Uint8Array 中:

export async function streamToArrayBuffer(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
    const chunks: Uint8Array[] = [];
    for await (const chunk of stream) {
        chunks.push(chunk);
    }
    return concatArrayBuffers(chunks);
}

将来当浏览器支持Array.fromAsync()时,可以简化为:
export async function streamToArrayBuffer(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
    return concatArrayBuffers(await Array.fromAsync(stream));
}

将流转换为字符串

使用new Response(stream).text()

就像上面描述的数组缓冲区一样,可以使用Response.text()Uint8Array的流转换为字符串:

export async function streamToString(stream: ReadableStream<Uint8Array>): Promise<string> {
   return await new Response(stream).text();
}

使用TextDecoderStream

TextDecoderStream会将UInt8Array块的流转换为string块的流。这样,您可以直接将流的内容收集为字符串。请注意,Firefox浏览器的支持仅在2022年9月才添加,因此您可能还不想在生产环境中使用此功能。

export async function streamToText(stream: ReadableStream<Uint8Array>): Promise<string> {
    let result = '';
    const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
    while (true) {
        const { done, value } = await reader.read();
        if (done) {
            break;
        }

        result += value;
    }
    return result;
}

在支持的浏览器中,您还可以使用异步迭代器协议来消耗这个字符串流。
export async function streamToText(stream: ReadableStream<Uint8Array>): Promise<string> {
    let result = '';
    for (const chunk of stream.pipeThrough(new TextDecoderStream()).getReader())
        result += chunk;
    }
    return result;
}

在支持 Array.fromAsync() 的浏览器中,甚至更简短:
export async function streamToText(stream: ReadableStream<Uint8Array>): Promise<string> {
    const chunks = await Array.fromAsync(stream.pipeThrough(new TextDecoderStream()).getReader()));
    return chunks.join("");
}

使用TextDecoder

要将上述某些函数生成的Uint8Array转换为字符串,您可以使用TextDecoder

const buffer = await streamToArrayBuffer(res.body);
const text = new TextDecoder().decode(buffer);

请注意,这仅适用于整个内容,而不是单个UInt8Array块,因为某些字符可能由多个字节组成,并可能在块之间分割。

将流转换为JSON对象

使用new Response(stream).json()

就像上面描述的数组缓冲区和字符串一样,可以使用Response.json()Uint8Array流解析为JSON:

export async function streamToJson(stream: ReadableStream<Uint8Array>): Promise<unknown> {
   return await new Response(stream).json();
}

使用 JSON.parse()

使用上述任何方法将流转换为字符串,然后使用 JSON.parse() 解析该字符串。


感谢您实际回答了这个问题。 - daydr3amer
5
如果有人拥有一个ReadableStream并希望用简单的一行代码将其中的文本提取出来,一个简单的方法是将其包装在一个新的Response(或Request)对象中,并使用text方法:await new Response(request.body).text()。在此链接中发布了这方面的答案。 - joe

18

请注意,您只能读取流一次,因此在某些情况下,您可能需要克隆响应以便反复读取:

fetch('example.json')
  .then(res=>res.clone().json())
  .then( json => console.log(json))

fetch('url_that_returns_text')
  .then(res=>res.clone().text())
  .then( text => console.log(text))

15

如果只想获取文本响应并且不想将其转换为JSON格式,请使用https://developer.mozilla.org/en-US/docs/Web/API/Body/text,然后使用then来获取Promise的实际结果:

fetch('city-market.md')
  .then(function(response) {
    response.text().then((s) => console.log(s));
  });
或者
fetch('city-market.md')
  .then(function(response) {
    return response.text();
  })
  .then(function(myText) {
    console.log(myText);
  });

1
城市市场.md 太棒了 <3 - KyleMit

4

我不喜欢使用链式的then。第二个then无法访问status。如前所述,'response.json()'返回一个promise。将'response.json()'的then结果返回,类似于第二个then,而且它有一个额外的好处,在响应作用域内。

return fetch(url, params).then(response => {
    return response.json().then(body => {
        if (response.status === 200) {
            return body
        } else {
            throw body
        }
    })
})

链式编程中的 then 可以帮助您检索最终解析值(即 body)。嵌套它们会使您无法在没有回调或某种机制的情况下获取 body 值。想象一下:let body = await fetch(...).then(res => res.json()).then(data => data)。这种嵌套方式行不通。要检查 response.status,可以始终在第一个 thenthrow 一个异常,并将 catch 添加到整个 Promise 链中。 - Óscar Gómez Alcañiz
1
同意。在企业环境中更受欢迎。:) - Nash Worth

4
另一种方法是以块的形式消耗/处理传入的数据流:
async function toJSON(body) {
  const reader = body.getReader();
  const decoder = new TextDecoder();
  const chunks = [];

  async function read() {
    const { done, value } = await reader.read();

    if (done) {
      return JSON.parse(chunks.join(''));
    }

    const chunk = decoder.decode(value, { stream: true });
    chunks.push(chunk);
    return read();
  }

  return read();
}

const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const jsonData = await toJSON(response.body);

console.log(jsonData);

当输入数据过于庞大或者您希望在数据块可用时立即开始处理数据时,这种方法特别有用。

为了对那些对进一步学习感兴趣的人,写了一篇博客文章


1
其他答案通过调用等待并在最后返回最终结果的方法,失去了使用可读流的乐趣,而你的方法允许使用传入的数据。继续努力!你的博客文章对我很有帮助。 - gouessej

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接