流媒体API的实例介绍

326 阅读4分钟

使用流,我们可以从网络或其他来源接收资源,并在第一个比特到达时立即处理它。

与其等待资源完全下载后再使用它,我们可以立即使用它。

什么是流

我想到的第一个例子是加载一个YouTube视频--你不必在开始观看之前完全加载它。

或者流媒体直播,你甚至不知道内容何时结束。

内容甚至不需要结束。它可以无限期地生成。

流媒体API

流媒体API使我们能够处理这类内容。

我们有2种不同的流模式:从流中读取,以及向流中写入。

可读流在所有现代浏览器中都可用,除了IE浏览器。

可写流在Firefox和Internet Explorer上不可用。

像往常一样,请查看caniuse.com,了解有关这一问题的最新信息。

我们先来看看可读流

可读流

说到可读流,我们有3类对象。

  • ReadableStream
  • ReadableStreamDefaultReader
  • ReadableStreamDefaultController

我们可以使用ReadableStream对象来消费流。

这里是可读流的第一个例子。Fetch API允许从网络上获得一个资源,并将其作为一个流来使用。

const stream = fetch('/resource')
  .then(response => response.body)

fetch响应的body 属性是一个ReadableStream 对象实例。这就是我们的可读流。

读者

在一个ReadableStream 对象上调用getReader() ,会返回一个ReadableStreamDefaultReader 对象,即阅读器。我们可以这样得到它。

const reader = fetch('/resource').then(response => response.body.getReader())

我们以块为单位读取数据,其中一个块是一个字节或一个类型的数组。块在流中被排队,我们一次读一个块。

一个流可以包含不同种类的块。

一旦我们有了一个ReadableStreamDefaultReader 对象,我们就可以使用read() 方法来访问数据。

一旦创建了一个阅读器,这个流就被锁定了,没有其他的阅读器可以从它那里获得块,直到我们对它调用releaseLock()

你可以通过tee一个流来实现这个效果,后面会有更多的介绍

从一个可读流中读取数据

一旦我们有了一个ReadableStreamDefaultReader 对象实例,我们就可以从它那里读取数据。

这就是你如何从flaviocopes.com网页的HTML内容流中,逐个字节地读取第一块内容(由于CORS的原因,你可以在该网页上打开的DevTools窗口中执行)。

fetch('https://flaviocopes.com/')
  .then(response => {
    response.body
      .getReader()
      .read()
      .then(({value, done}) => {
        console.log(value)
      })
  })

Uint8Array

如果你打开每个单组的数组项目,你会得到单项。那些是字节,存储在一个Uint8Array

bytes stored in Uint8Array

你可以使用Encoding API将这些字节转换为字符。

const decoder = new TextDecoder('utf-8')
fetch('https://flaviocopes.com/')
  .then(response => {
    response.body
      .getReader()
      .read()
      .then(({value, done}) => {
        console.log(decoder.decode(value))
      })
  })

这将打印出页面中加载的字符。

Printed characters

这个新版本的代码加载了流中的每一个块,并打印了它。

(async () => {
  const fetchedResource = await fetch('https://flaviocopes.com/')
  const reader = await fetchedResource.body.getReader()

  let charsReceived = 0
  let result = ''

  reader.read().then(function processText({ done, value }) {
    if (done) {
      console.log('Stream finished. Content received:')
      console.log(result)
      return
    }

    console.log(`Received ${result.length} chars so far!`)

    result += value

    return reader.read().then(processText)
  })
})()

我将其包装在一个async 立即调用的函数中,以使用await

我们创建的processText()函数接收一个有两个属性的对象。

  • done 如果流结束,我们得到了所有的数据,则为true
  • value 收到的当前块的值

我们创建这个递归函数来处理整个流。

创建一个流

警告:Edge和Internet Explorer不支持

我们刚刚看到了如何消费一个由Fetch API生成的可读流,这是一个开始使用流的好方法,因为用例很实用。

现在让我们看看如何创建一个可读流,这样我们就可以用我们的代码来访问一个资源。

我们之前已经使用了一个ReadableStream 对象。现在让我们使用new 关键字创建一个全新的对象。

const stream = new ReadableStream()

现在这个流不是很有用。它是一个空的流,如果有人想从它那里读取数据,就没有数据。

我们可以在初始化过程中通过一个对象来定义流的行为方式。这个对象可以定义这些属性。

  • start 一个在创建可读流时调用的函数。在这里你连接到数据源并执行管理任务。
  • pull 在没有达到内部队列高水位的情况下,反复调用一个函数来获取数据
  • cancel 当流被取消时调用的函数,例如在接收端调用 方法时。cancel()

下面是一个对象结构的赤裸裸的例子。

const stream = new ReadableStream({
  start(controller) {

  },
  pull(controller) {

  },
  cancel(reason) {

  }
})

start() 和 得到一个控制器对象,是 对象的一个实例,它让你控制流的状态和内部队列。pull() ReadableStreamDefaultController

为了将数据添加到流中,我们调用controller.enqueue() ,并传递持有我们数据的变量。

const stream = new ReadableStream({
  start(controller) {
    controller.enqueue('Hello')
  }
})

当我们准备关闭流时,我们调用controller.close()

cancel() 得到一个 ,这是取消流时提供给 方法调用的一个字符串。reason ReadableStream.cancel()

我们还可以传递一个可选的第二个对象,它决定了排队策略。它包含2个属性。

  • highWaterMark 可以存储在内部队列中的块的总数量。我们在之前谈论 的时候提到了这个pull()
  • size,一个你可以用来改变块大小的方法,用字节表示
{
  highWaterMark,
  size()
}

这些主要用于控制流的压力,特别是在管道链的背景下,这在Web APIs中仍然是试验性的。

当一个流的highWaterMark ,一个背压信号被发送到管道中的前几个流,告诉它们放慢数据压力。

我们有2个内置对象来定义排队策略。

  • ByteLengthQueuingStrategy 等待,直到块的累计大小(字节)超过指定的高水位线
  • CountQueuingStrategy 等待,直到累积的数据块数量超过指定的高水位线

设置32字节的高水位线的例子。

new ByteLengthQueuingStrategy({ highWaterMark: 32 * 1024 }

设置1块高水位线的例子。

new CountQueuingStrategy({ highWaterMark: 1 })

我提到这一点是为了告诉你,你可以控制流入流的数据量,并与其他角色进行通信,但我们不打算讨论更多细节,因为事情很快就会变得复杂。

发放流

之前我提到,一旦我们开始读取一个流,它就被锁定了,其他的读者不能访问它,直到我们对它调用releaseLock()

然而,我们可以使用流本身的tee() 方法来复制该流。

const stream = //...
const tees = stream.tee()

tees 现在是一个包含2个新流的数组,你可以使用 和 来读取这些流。tees[0] tees[1]

可写流

当涉及到可写流时,我们有3类对象。

  • WritableStream
  • WritableStreamDefaultReader
  • WritableStreamDefaultController

我们可以使用WritableStream对象来创建我们以后可以消费的流。

这就是我们如何创建一个新的可写流。

const stream = new WritableStream()

我们必须传递一个对象,以便发挥作用。这个对象将有以下可选的方法实现。

  • start() 当对象被初始化时被调用
  • write() 当一个块准备好被写入水槽时被调用(在写入前保存流数据的底层结构)。
  • close() 当我们完成写块时被调用
  • abort() 当我们想发出错误信号时被调用

下面是一个骨架。

const stream = new WritableStream({
  start(controller) {

  },
  write(chunk, controller) {

  },
  close(controller) {

  },
  abort(reason) {

  }
})

start(),close()write() 被传递给控制器,一个WritableStreamDefaultController 对象实例。

至于ReadableStream() ,我们可以传递第二个对象给new WritableStream() ,以设置排队策略。

例如,让我们创建一个流,给定一个存储在内存中的字符串,创建一个消费者可以连接到的流。

我们首先定义一个解码器,我们将使用Encoding API TextDecoder() 构造函数将我们收到的字节转化为字符。

const decoder = new TextDecoder("utf-8")

我们可以初始化实现close() 方法的WritableStream,当消息被完全接收并且客户端代码调用它时,它将打印到控制台。

const writableStream = new WritableStream({
  write(chunk) {
    //...
  },
  close() {
    console.log(`The message is ${result}`)
  }
})

我们通过初始化一个ArrayBuffer并将其添加到大块中来开始实现write() 。然后,我们使用Encoding API的decoder.decode()方法将这个作为字节的块解码成一个字符。然后我们把这个值添加到一个result ,我们在这个对象外面声明这个字符串。

let result

const writableStream = new WritableStream({
  write(chunk) {
    const buffer = new ArrayBuffer(2)
    const view = new Uint16Array(buffer)
    view[0] = chunk
    const decoded = decoder.decode(view, { stream: true })
    result += decoded
  },
  close() {
    //...
  }
})

现在WritableStream对象已经被初始化。

我们现在去实现将使用这个流的客户端代码。

我们首先从writableStream 对象中获取WritableStreamDefaultWriter 对象。

const writer = writableStream.getWriter()

接下来我们定义一个要发送的消息。

然后,我们初始化编码器,我们要发送到流中的字符进行编码

const encoder = new TextEncoder()
const encoded = encoder.encode(message, { stream: true })

在这一点上,字符串已经被编码为一个字节数组。现在,我们在这个数组上使用forEach 循环,将每个字节发送到流中。在每次调用流写入器的write() 方法之前,我们检查ready 属性,该属性返回一个承诺,所以我们只在流写入器准备好时写入。

encoded.forEach(chunk => {
  writer.ready.then(() => {
    return writer.write(chunk)
  })
})

现在我们唯一错过的是关闭写入器。forEach 是一个同步循环,这意味着我们只有在每个项目被写入后才能达到这一点。

我们仍然检查ready 属性,然后我们调用close()方法。

writer.ready.then(() => {
  writer.close()
})