JS `Service Worker` `ReadableStream` `ByteLengthQueuingStrategy` 与 `CountQueuingStrategy`

同学们,早上好! 今天咱们聊聊Service Worker、ReadableStream,以及它们的好伙伴:ByteLengthQueuingStrategy和CountQueuingStrategy。 这几个家伙,听起来是不是像在念咒语? 别怕,今天咱就把它掰开了揉碎了,用大白话讲明白。

Service Worker:网页的贴身保镖兼跑腿小弟

首先,Service Worker是什么? 简单来说,它就是浏览器在后台默默运行的一段JS代码。 它的主要职责是:

  1. 拦截网络请求: 就像一个门卫,所有网页发出的请求都要先经过它的审查。
  2. 缓存资源: 它可以把常用的资源(比如图片、JS、CSS)缓存起来,下次再用的时候直接从缓存里拿,速度嗖嗖的。
  3. 推送消息: 即使网页关闭了,它也能接收服务器推送的消息,然后通知用户。

想象一下,你的网页有个贴身保镖,帮你缓存东西,拦截坏人的请求,还能在你不在线的时候给你送信,是不是很酷?

ReadableStream:数据的流水线

接下来,咱们说说ReadableStream。 这是一个用于读取数据的流。 你可以把它想象成一个水管,水(数据)从一头流进去,你从另一头取出来。

ReadableStream有三个核心概念:

  • Source: 数据的来源,比如网络请求、文件读取等。
  • Reader: 读取数据的工具,就像水龙头一样,可以控制水的流速。
  • Queue: 一个队列,用于存储已经读取但尚未处理的数据。

ReadableStream的主要作用是: 它可以把大的数据分割成小块,一块一块地读取,避免一次性加载所有数据导致内存溢出。 就像吃烤肉,一口一口吃,而不是一口吞。

ByteLengthQueuingStrategy 和 CountQueuingStrategy:队列管理员

现在,重头戏来了,ByteLengthQueuingStrategy 和 CountQueuingStrategy 这两个家伙是干嘛的? 它们是用来管理ReadableStream的队列的。 它们决定了队列何时满了,何时应该停止读取数据。

  • ByteLengthQueuingStrategy:按字节长度排队

    这个策略是根据队列中数据的字节长度来判断队列是否已满。 比如,你可以设置队列的最大字节长度为1MB,当队列中的数据达到1MB时,ReadableStream就会暂停读取,直到队列中的数据被处理掉一部分。

    就像一个仓库,你设置了仓库的最大容量,当货物堆满的时候,就不能再往里搬了,得先卖掉一些才能继续进货。

  • CountQueuingStrategy:按数据块数量排队

    这个策略是根据队列中数据块的数量来判断队列是否已满。 比如,你可以设置队列的最大数据块数量为100,当队列中的数据块达到100时,ReadableStream就会暂停读取,直到队列中的数据被处理掉一部分。

    就像一个停车场,你设置了停车位的最大数量,当车位满了的时候,就不能再让车进来了,得先有车开走才能继续停车。

代码实战:用ReadableStream下载文件,并用ByteLengthQueuingStrategy控制流量

光说不练假把式,咱们来写一段代码,演示如何用ReadableStream下载文件,并用ByteLengthQueuingStrategy控制流量。

async function downloadFile(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  const contentLength = response.headers.get('Content-Length');
  const total = parseInt(contentLength, 10) || 0;
  let loaded = 0;

  const reader = response.body.getReader();
  const strategy = new ByteLengthQueuingStrategy({ highWaterMark: 65536 }); // 64KB
  const stream = new ReadableStream({
    start(controller) {
      function push() {
        reader.read().then(({ done, value }) => {
          if (done) {
            controller.close();
            return;
          }
          controller.enqueue(value);
          loaded += value.byteLength;
          const progress = total ? Math.round((loaded / total) * 100) : -1;
          console.log(`Download progress: ${progress}%`);
          push();
        }).catch(error => {
          console.error("Stream error", error);
          controller.error(error);
        });
      }
      push();
    },
    cancel(reason) {
      console.log("Stream cancelled", reason);
      reader.cancel(reason).catch(err => console.error("Cancel error", err));
    }
  }, strategy);

  // 将ReadableStream转换为Blob对象
  const blob = await new Response(stream).blob();

  // 创建下载链接
  const urlObject = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = urlObject;
  a.download = 'downloaded_file.dat'; // 设置下载文件名
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(urlObject);
}

// 调用示例
downloadFile('https://example.com/large_file.dat'); // 替换为你的文件URL

这段代码做了以下几件事:

  1. 发起网络请求: 使用fetch函数下载文件。
  2. 获取ReadableStream:response.body中获取ReadableStream。
  3. 创建ByteLengthQueuingStrategy: 设置队列的最大字节长度为64KB。 highWaterMark: 65536 这就是设置队列最大长度的地方。
  4. 创建ReadableStream: 使用ReadableStream构造函数,并传入ByteLengthQueuingStrategy
  5. 读取数据:start方法中,不断地从ReadableStream中读取数据,并将其放入队列中。 controller.enqueue(value); 这就是把数据放入队列的操作。
  6. 更新进度: 每次读取到数据,都更新下载进度。
  7. 转换为Blob并下载: 将ReadableStream转换为Blob对象,然后创建一个下载链接,让用户下载文件。

在这个例子中,ByteLengthQueuingStrategy确保了队列中的数据不会超过64KB,从而避免了内存溢出。 如果下载的文件很大,但你的内存有限,这个策略就非常有用了。

代码实战:使用CountQueuingStrategy处理WebSocket数据

再来一个例子,咱们用CountQueuingStrategy处理WebSocket数据。

function handleWebSocket(websocket) {
  const strategy = new CountQueuingStrategy({ highWaterMark: 10 }); // 限制队列最多10条消息
  const stream = new ReadableStream({
    start(controller) {
      websocket.onmessage = event => {
        controller.enqueue(event.data); // 将接收到的消息放入队列
      };

      websocket.onclose = () => {
        controller.close();
      };

      websocket.onerror = error => {
        console.error("WebSocket error", error);
        controller.error(error);
      };
    },
    cancel(reason) {
      console.log("Stream cancelled", reason);
      websocket.close();
    }
  }, strategy);

  const reader = stream.getReader();

  async function read() {
    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) {
          console.log("Stream complete");
          break;
        }
        console.log("Received message:", value);
        // 在这里处理接收到的消息
      }
    } catch (error) {
      console.error("Error reading from stream", error);
    } finally {
      reader.releaseLock(); // 释放锁,允许取消流
    }
  }

  read(); // 开始读取数据
}

// 假设你已经建立了WebSocket连接
const websocket = new WebSocket('wss://example.com/socket');
websocket.onopen = () => {
  console.log("WebSocket connected");
  handleWebSocket(websocket);
};

这段代码做了以下几件事:

  1. 连接WebSocket: 创建一个WebSocket连接。
  2. 创建CountQueuingStrategy: 设置队列的最大数据块数量为10。 highWaterMark: 10 这就是限制队列最多只能放10条消息的地方。
  3. 创建ReadableStream: 使用ReadableStream构造函数,并传入CountQueuingStrategy
  4. 接收WebSocket消息: 当WebSocket收到消息时,将其放入队列中。 controller.enqueue(event.data); 这就是把WebSocket消息放入队列的操作。
  5. 读取数据: 不断地从ReadableStream中读取数据,并处理这些数据。

在这个例子中,CountQueuingStrategy确保了队列中的消息不会超过10条,避免了因为消息积压导致的问题。 如果WebSocket连接不稳定,或者处理消息的速度跟不上接收消息的速度,这个策略就很有用了。

ByteLengthQueuingStrategy 和 CountQueuingStrategy 的对比

为了方便大家理解,咱们来个表格,对比一下ByteLengthQueuingStrategy 和 CountQueuingStrategy。

特性 ByteLengthQueuingStrategy CountQueuingStrategy
队列限制依据 队列中数据的字节长度 队列中数据块的数量
适用场景 大文件下载、需要控制流量的场景 WebSocket数据处理、消息队列等场景
优点 可以精确控制内存使用,避免内存溢出 简单易用,适用于处理离散的数据块
缺点 需要知道数据的字节长度,对于某些类型的数据可能不适用 无法精确控制内存使用,可能导致内存溢出
highWaterMark 设置队列的最大字节长度 设置队列的最大数据块数量

总结:灵活运用,事半功倍

今天咱们聊了Service Worker、ReadableStream,以及ByteLengthQueuingStrategy 和 CountQueuingStrategy。 它们都是Web开发中非常有用的工具,可以帮助我们更好地处理数据,优化性能,提升用户体验。

记住,没有万能的解决方案,只有最合适的解决方案。 在实际开发中,要根据具体的场景选择合适的策略,灵活运用,才能事半功倍。

好了,今天的讲座就到这里,大家有什么问题吗?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注