剖析JS里的流式请求—WritableStream

上一篇我们介绍了ReadableStream,今天我们来介绍一下WritableStream,从字面上理解,它是指可写流,那使用上有什么区别呢?它又会有一些什么样的场景呢?

API介绍

WritableStream是Web API中的一部分,它允许开发者将数据写入到一个目标,这个目标可以是一个文件、一个网络连接,或者任何其他可以接收数据的地方。它提供了一种高效的方式来处理大量数据,而无需一次性将所有数据加载到内存中。该对象带有内置的背压和队列。

基本步骤如下:

  1. 创建WritableStream实例 :你可以通过new WritableStream()来创建一个新的WritableStream实例。
  2. 写入数据 :使用write()方法将数据写入流中。
  3. 关闭流 :一旦完成写入,使用close()方法关闭流,确保所有数据都被正确处理。
  4. 错误处理 :使用catch()方法来捕获并处理可能出现的错误。
javascript 复制代码
const writableStream = new WritableStream({
  start(controller) {
    // 初始化逻辑
  },
  write(chunk, controller) {
    // 写入数据逻辑
    console.log(`Writing chunk: ${chunk}`);
  },
  close(controller) {
    // 关闭流的逻辑
    console.log('Stream closed');
  },
  abort(reason) {
    // 处理流被中止的逻辑
    console.error(`Stream aborted: ${reason}`);
  }
});

同样的,我们先来介绍一下它的使用方法。首先看的是构造函数:

scss 复制代码
new WritableStream(underlyingSink)
new WritableStream(underlyingSink, queuingStrategy)

underlyingSink:一个包含方法和属性的对象,这些方法和属性定义了构造的流的实例的具体行为。其属性如下:

  1. start(controller) :当对象被构造时立刻调用的方法,用于设置流的初始状态和行为。可以异步执行,返回一个promise。传递给这个方法的 controller 参数是一个 WritableStreamDefaultController。开发人员可以在设置时使用它来控制流。

  2. write(chunk, controller) :写入数据块到流中。每次写入成功后才会调用下一次。可以异步执行,返回一个promise。

  3. close(controller) :完成所有写入操作后关闭流。只有所有写入操作完成后才会调用。可以异步执行,返回一个promise。

  4. abort(reason) :立即中断流,丢弃所有等待写入的数据块。可以异步执行,返回一个promise,并提供中断的原因。

queuingStrategy:可选的,定义流的队列策略的对象。这需要两个参数:

  1. highWaterMark:非负整数------这定义了在应用背压之前可以包含在内部队列中的分块的最大数量。
  2. size(chunk):包含参数 chunk 的方法------这表示每个分块所需要使用的字节数。

以上是其构造函数。它的方法主要有这几个:

  1. abort:中止流,表示生产者不能再向流写入数据(会立刻返回一个错误状态),并丢弃所有已入队的数据。
  2. close:关闭关联的流。在调用此方法之前编写的所有块都在返回的Promise完成之前发送。
  3. getWriter:返回WritableStreamDefaultWriter的新实例,并将流锁定到该实例。当流被锁定时,其他人不能再获取writer。

应用场景

下面来介绍一下它的实际应用场景。

1、文件上传

假设你正在开发一个网页,用户可以上传图片或文档。使用WritableStream,你可以将文件上传到服务器。

javascript 复制代码
// 创建一个用于文件上传的WritableStream
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (event) => {
  const file = event.target.files[0];
  const writableStream = new WritableStream({
    write(chunk) {
      // 这里可以是向服务器发送数据的代码
      fetch('/upload', {
        method: 'POST',
        body: chunk,
      });
    },
    close() {
      console.log('文件上传完成');
    },
    error(err) {
      console.error('上传失败:', err);
    }
  });
  // 读取文件并写入WritableStream
  const reader = file.stream();
  reader.pipeTo(writableStream);
});

2、实时日志记录

在Web应用程序中,你可能需要实时记录用户的行为或系统日志。

javascript 复制代码
// 创建一个WritableStream,用于写入日志文件
const logStream = new WritableStream({
  write(chunk) {
    // 将日志数据写入到服务器或文件系统
    console.log(chunk); // 这里可以替换为写入文件的代码
  }
});
// 记录日志
function logMessage(message) {
  logStream.write(message + '\n');
}
logMessage('用户访问了页面');

3、数据流转换

在数据传输过程中,可能需要对数据进行转换,比如将JSON数据转换为字符串。

javascript 复制代码
// 创建WritableStream,用于转换数据
const transformStream = new WritableStream({
  write(chunk) {
    // 假设chunk是JSON对象,需要转换为字符串
    const stringifiedChunk = JSON.stringify(chunk);
    // 将转换后的数据写入到某个目标
    console.log(stringifiedChunk); // 这里可以是其他写入操作
  }
});
// 写入JSON数据并转换
const jsonData = { key: 'value' };
transformStream.write(jsonData);

4、音频/视频流处理

使用WritableStream处理音频或视频流,例如将解码后的音频数据写入到Web Audio API。

ini 复制代码
// 假设你有一个解码后的音频数据流
const audioData = ...; // 音频数据
// 创建WritableStream,用于处理音频数据
const audioContext = new AudioContext();
const audioStream = new WritableStream({
  write(chunk) {
    // 创建一个AudioBufferSourceNode并播放音频
    const source = audioContext.createBufferSource();
    source.buffer = chunk; // 假设chunk是一个AudioBuffer
    source.connect(audioContext.destination);
    source.start();
  }
});
// 将音频数据写入流
audioStream.write(audioData);

这些示例展示了WritableStream在不同场景下的应用,从文件上传到实时日志记录,再到数据转换和媒体流处理,WritableStream都能提供灵活且强大的解决方案。

常见问题

经过前面两节的介绍,我们了解了WritableStream的常用API以及基础的用法。那在真实业务中,我们还会遇到什么问题呢?下面我们一起来看看。

1、写入时如何确定合适的块大小?

首先,需要明确的是没有一个可用的公式可以计算最佳的块大小。但可以通过测试和性能反馈帮助确定最佳的块大小,以及在不同条件下块大小的适应性。

但也不是无从下手,WriteableStream有一个属性值desiredSize,整数,只读,用于表示还需要多少大小才能填满内部队列,如果流无法成功写入(由于出错或中止排队),则该值将为空值,如果流关闭,则为零。因此检测到这个指小于0的时候可以等待。

2、在处理流的过程中,可能会遇到流的状态不一致的问题,例如试图向已经关闭的流写入数据。

在每次写入之前,检查流的状态。可以通过 writableStream.locked 属性来检查流是否已被锁定,从而决定是否可以写入数据。

scss 复制代码
if (!writableStream.locked) {
  const writer = writableStream.getWriter();
  writer.write(data).then(() => writer.close());
} else {
  console.error('Stream is locked');
}

3、在高并发场景下,多个写入操作可能会导致数据写入的顺序错乱或资源争用问题

使用锁机制或队列来管理并发写入,确保写入操作按顺序执行。

scss 复制代码
const writeQueue = [];
function enqueueWrite(data) {
  return new Promise((resolve, reject) => {
    writeQueue.push({ data, resolve, reject });
    if (writeQueue.length === 1) {
      processQueue();
    }
  });
}
function processQueue() {
  if (writeQueue.length === 0) return;
  const { data, resolve, reject } = writeQueue[0];
  const writer = writableStream.getWriter();
  writer.write(data)
    .then(() => {
      writer.releaseLock();
      resolve();
      writeQueue.shift();
      processQueue();
    })
    .catch(error => {
      writer.releaseLock();
      reject(error);
      writeQueue.shift();
      processQueue();
    });
}

4、在流处理中发生错误后,如何有效地进行错误恢复是一个难点。

在错误处理逻辑中,可以设计重试机制或回退策略,确保流处理能够顺利恢复。

javascript 复制代码
const maxRetries = 3;
let retryCount = 0;
const writableStream = new WritableStream({
  write(chunk) {
    return new Promise((resolve, reject) => {
      function attemptWrite() {
        try {
          // 写入数据逻辑
          resolve();
        } catch (error) {
          if (retryCount < maxRetries) {
            retryCount++;
            console.warn(`Retry ${retryCount} after error:`, error);
            attemptWrite();
          } else {
            reject(error);
          }
        }
      }
      attemptWrite();
    });
  },
  close() {
    // 关闭流的逻辑
  },
  abort(reason) {
    console.error('Stream aborted:', reason);
  }
});

5、频繁的小块写入操作可能会影响性能,导致较高的处理开销。

可以使用缓冲区暂存数据,然后批量写入,减少写入操作的频率

scss 复制代码
const buffer = [];
const bufferSize = 1024; // 根据实际情况设置缓冲区大小
function writeToStream(data) {
  buffer.push(data);
  if (buffer.length >= bufferSize) {
    flushBuffer();
  }
}
function flushBuffer() {
  const writer = writableStream.getWriter();
  writer.write(buffer.splice(0, buffer.length)).then(() => {
    writer.releaseLock();
  });
}

总结

WritableStream是JavaScript中一个非常有用的工具,它提供了一种灵活、高效的方式来处理数据写入。通过本篇文章,我们探讨了它的使用方法、技巧、注意事项以及应用场景,并提供了实际案例来增强理解。希望这能帮助你更好地利用WritableStream来优化你的Web应用程序。

关联文章:juejin.cn/post/738479...

相关推荐
转角羊儿5 分钟前
uni-app文章列表制作⑧
前端·javascript·uni-app
Bio Coder1 小时前
学习用 Javascript、HTML、CSS 以及 Node.js 开发一个 uTools 插件,学习计划及其周期
javascript·学习·html·开发·utools
凹凸曼打不赢小怪兽2 小时前
react 受控组件和非受控组件
前端·javascript·react.js
忠实米线3 小时前
使用pdf-lib.js实现pdf添加自定义水印功能
前端·javascript·pdf
明辉光焱4 小时前
[Electron]总结:如何创建Electron+Element Plus的项目
前端·javascript·electron
牧码岛4 小时前
Web前端之汉字排序、sort与localeCompare的介绍、编码顺序与字典顺序的区别
前端·javascript·web·web前端
云空4 小时前
《InsCode AI IDE:编程新时代的引领者》
java·javascript·c++·ide·人工智能·python·php
咔咔库奇5 小时前
ES6基础
前端·javascript·es6
bug爱好者5 小时前
如何解决sourcetree 一打开就闪退问题
前端·javascript·vue.js
徐小夕5 小时前
Flowmix/Docx 多模态文档编辑器:V1.3.5版本,全面升级
前端·javascript·架构