上一篇我们介绍了ReadableStream,今天我们来介绍一下WritableStream,从字面上理解,它是指可写流,那使用上有什么区别呢?它又会有一些什么样的场景呢?
API介绍
WritableStream是Web API中的一部分,它允许开发者将数据写入到一个目标,这个目标可以是一个文件、一个网络连接,或者任何其他可以接收数据的地方。它提供了一种高效的方式来处理大量数据,而无需一次性将所有数据加载到内存中。该对象带有内置的背压和队列。
基本步骤如下:
- 创建WritableStream实例 :你可以通过
new WritableStream()
来创建一个新的WritableStream实例。 - 写入数据 :使用
write()
方法将数据写入流中。 - 关闭流 :一旦完成写入,使用
close()
方法关闭流,确保所有数据都被正确处理。 - 错误处理 :使用
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:一个包含方法和属性的对象,这些方法和属性定义了构造的流的实例的具体行为。其属性如下:
-
start(controller) :当对象被构造时立刻调用的方法,用于设置流的初始状态和行为。可以异步执行,返回一个promise。传递给这个方法的 controller 参数是一个 WritableStreamDefaultController。开发人员可以在设置时使用它来控制流。
-
write(chunk, controller) :写入数据块到流中。每次写入成功后才会调用下一次。可以异步执行,返回一个promise。
-
close(controller) :完成所有写入操作后关闭流。只有所有写入操作完成后才会调用。可以异步执行,返回一个promise。
-
abort(reason) :立即中断流,丢弃所有等待写入的数据块。可以异步执行,返回一个promise,并提供中断的原因。
queuingStrategy
:可选的,定义流的队列策略的对象。这需要两个参数:
highWaterMark
:非负整数------这定义了在应用背压之前可以包含在内部队列中的分块的最大数量。size(chunk)
:包含参数 chunk 的方法------这表示每个分块所需要使用的字节数。
以上是其构造函数。它的方法主要有这几个:
- abort:中止流,表示生产者不能再向流写入数据(会立刻返回一个错误状态),并丢弃所有已入队的数据。
- close:关闭关联的流。在调用此方法之前编写的所有块都在返回的Promise完成之前发送。
- 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应用程序。