剖析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...

相关推荐
戏拈秃笔18 分钟前
前端——在本地搭建Vue单页应用
前端·javascript·vue.js
Elena_Lucky_baby19 分钟前
vue项目创建+eslint+Prettier+git提交规范(commitizen+hooks+husk)
前端·javascript·vue.js
呀呀呀雅楠?1 小时前
vue3(nuxt3)中使用ref获取不到子组件变量
前端·javascript·vue.js·nuxtjs
小怪瘦791 小时前
Vue3使用PDFJS将后端查到的二进制数据转为图片
java·前端·javascript
前端张三2 小时前
vue2中vuedraggable设置部分元素不可拖拽,不可移动
前端·javascript·vue.js
一颗苹果OMG2 小时前
掌握this指向,让你在变量的拷贝中游刃有余
前端·javascript
墨轩尘2 小时前
axios之CancelToken取消请求
前端·javascript
满分观测网友z3 小时前
方案论证项目实现功能
前端·javascript·vue.js·cesium
象野VH3 小时前
编码与加密
javascript·python
xgq3 小时前
JavaScript 中的 CustomEvent
前端·javascript·面试