剖析JS里的流式请求—TransformStream

881 阅读8分钟

未命名__2024-07-05+10_00_57.png

前言

前几天介绍了WritableStream和ReadableStream,今天我们一起来学习TransformStream。

01 为什么需要TransformStream

TransformStream 是 Streams API 中的重要组成部分,它允许开发者在数据块通过流时对其进行处理和转换。大家可能会好奇,为啥一定要用TransformStream?不用不行吗?

下面我们来看一个例子:文本数据块转换为大写

function fetchAndTransformText() {
  fetch("./lorem-ipsum.txt")
    .then(response => response.text())
    .then(text => {
      const upperCaseText = text.toUpperCase();
      document.body.append(upperCaseText);
    })
    .catch(error => console.error('Error:', error));
}
fetchAndTransformText();

在这个例子中,我们通过读取整个文本文件并一次性处理数据,然后将结果追加到 DOM 中。此方法适用于小文件,但对于大文件或实时数据处理,效率较低且无法实现流式处理。也就是说如果读取时间需要5分钟,那用户就需要等待5分钟才能看到结果,这显然是十分不友好的。

你或许又会说,如果我文件不大呢?那确实,如果只是几kb的数据,确实不需要TransformStream。杀鸡焉用牛刀,我们视情况而定即可。

02 TransformStream对象介绍

TransformStream 构造函数会创建一个新的 TransformStream 对象,该对象包含一对流:用于写入数据的 WritableStream 和用于读取数据的 ReadableStream。

iShot_2024-07-05_09.40.24.png转存失败,建议直接上传图片文件

其构造函数的语法如下:

new TransformStream()
new TransformStream(transformer)
new TransformStream(transformer, writableStrategy)
new TransformStream(transformer, writableStrategy, readableStrategy)

transformer(可选):一个对象,包含以下方法:

  • start(controller):在 TransformStream 构造时调用。
  • transform(chunk, controller):当有数据块准备转换时调用。
  • flush(controller):所有数据块转换完成且可写侧即将关闭时调用。

writableStrategy 和 readableStrategy(可选):定义排队策略的对象,包含 highWaterMark 和 size(chunk) 参数,用于控制内部队列和反压处理 (MDN Web Docs) (WhatWG Streams)。

在构造 TransformStream 时,会内部创建 TransformStreamDefaultController,提供用于操作关联的 ReadableStream 和 WritableStream 的方法:

  • enqueue(chunk):将数据块添加到可读侧。
  • error(e):将流置于错误状态。
  • terminate():关闭可读侧并将可写侧置于错误状态 (MDN Web Docs)。

如果没有提供 transformer 参数,那么结果将是一个恒等流,它将所有写入可写端的分块转发到可读端,并且不做任何改变。

恒等流(Identity Stream)是一种特殊的转换流(TransformStream),它不对流中的数据进行任何修改,直接将数据从可写端(WritableStream)传输到可读端(ReadableStream)。在恒等流中,数据保持原样,不做任何转换或处理。

在以下示例中,一个恒等转换流被用于向一个管道添加缓冲。

const writableStrategy = new ByteLengthQueuingStrategy({
  highWaterMark: 1024 * 1024,
});
readableStream
  .pipeThrough(new TransformStream(undefined, writableStrategy))
  .pipeTo(writableStream);

在TransformStream使用过程中,最重要的还是定义transform的处理逻辑。

03 TransformStream使用场景

TransformStream 在许多实际业务中都具有重要的应用价值,特别是在处理和转换数据流的场景中。以下是几个真实业务使用 TransformStream 的示例:

  1. 数据压缩和解压缩
    • 场景描述:在网络传输中,对数据进行压缩可以显著减少传输时间和带宽消耗。接收数据后需要解压缩以进行进一步处理。
    • 应用示例:
const { createGzip, createGunzip } = require('zlib');
const { pipeline, Transform } = require('stream');
const fs = require('fs');
// 使用 TransformStream 进行压缩
const gzip = createGzip();
const source = fs.createReadStream('input.txt');
const destination = fs.createWriteStream('input.txt.gz');
pipeline(source, gzip, destination, (err) => {
  if (err) {
    console.error('Compression failed:', err);
  } else {
    console.log('File successfully compressed');
  }
});
  1. 视频处理和转码
    • 场景描述:在视频流媒体服务中,将视频从一种格式实时转码为另一种格式,以适应不同的客户端设备和网络条件。
    • 应用示例:
const { createServer } = require('http');
const { Transform } = require('stream');
const { exec } = require('child_process');
// 创建一个 TransformStream 进行视频转码
class VideoTranscoder extends Transform {
  _transform(chunk, encoding, callback) {
    // 使用 ffmpeg 进行转码
    const ffmpeg = exec('ffmpeg -i input -f mp4 -');
    ffmpeg.stdin.write(chunk);
    ffmpeg.stdout.on('data', (data) => this.push(data));
    ffmpeg.stderr.on('data', (data) => console.error(data.toString()));
    ffmpeg.on('close', callback);
  }
}
createServer((req, res) => {
  req.pipe(new VideoTranscoder()).pipe(res);
}).listen(8000);
  1. 实时数据转换和过滤
    • 场景描述:在物联网(IoT)应用中,传感器不断发送数据,需要对这些数据进行实时处理和过滤,然后将处理后的数据传送到数据库或另一个服务。
    • 应用示例:
const { Transform } = require('stream');
// 创建一个 TransformStream 进行数据过滤
class SensorDataFilter extends Transform {
  _transform(chunk, encoding, callback) {
    const data = JSON.parse(chunk);
    if (data.value > 10) { // 过滤条件:值大于 10
      this.push(chunk);
    }
    callback();
  }
}
sensorDataStream.pipe(new SensorDataFilter()).pipe(databaseStream);
  1. 文件格式转换
    • 场景描述:在文件处理系统中,用户上传的文件需要转换为特定格式以供使用或存储。
    • 应用示例:
const { Transform } = require('stream');
const csv = require('csv-parser');
class CSVToJSON extends Transform {
  constructor() {
    super({ readableObjectMode: true });
  }
  _transform(chunk, encoding, callback) {
    const data = chunk.toString();
    const lines = data.split('\n');
    lines.forEach(line => {
      const [key, value] = line.split(',');
      this.push(JSON.stringify({ [key]: value }));
    });
    callback();
  }
}
fs.createReadStream('input.csv')
  .pipe(csv())
  .pipe(new CSVToJSON())
  .pipe(fs.createWriteStream('output.json'));

TransformStream 在数据压缩、视频转码、实时数据处理和文件格式转换等多个场景中展现了其强大的数据流处理能力。这些示例展示了 TransformStream 如何在实际业务中提升效率和性能。

03 TransformStream常见问题

TransformStream 提供了强大的数据流处理能力,但在实际使用过程中可能会遇到一些问题。以下是常见问题的描述及其解决方案,附带相应的代码示例。

问题 1:处理错误和异常

  • 描述:在数据转换过程中可能会发生各种错误,例如数据格式不正确、网络故障等。如果这些错误没有得到妥善处理,可能会导致整个流的处理失败。
  • 解决方案:使用 TransformStreamDefaultController 的 error() 方法来处理和传播错误,并在 start、transform 和 flush 方法中添加错误处理逻辑。
const faultyTransformStream = new TransformStream({
  start(controller) {
    // 初始化工作
  },
  transform(chunk, controller) {
    try {
      // 处理数据块
      if (chunk.includes('error')) {
        throw new Error('模拟错误');
      }
      controller.enqueue(chunk.toUpperCase());
    } catch (error) {
      controller.error(error);
    }
  },
  flush(controller) {
    // 完成处理
  }
});
// 使用 TransformStream 处理数据流
fetch("./data.txt")
  .then(response => response.body.pipeThrough(faultyTransformStream))
  .then(stream => {
    // 处理转换后的数据流
  })
  .catch(error => console.error('处理过程中出错:', error));

问题 2:反压处理

  • 描述:在数据流处理过程中,如果数据生产速度超过消费速度,可能会导致反压问题,即数据积压。此时需要有效管理数据流的速率。
  • 解决方案:使用适当的排队策略和 highWaterMark 参数来控制数据流的速率,并确保在 TransformStream 中正确处理反压。
const readableStrategy = new ByteLengthQueuingStrategy({ highWaterMark: 1024 });
const writableStrategy = new ByteLengthQueuingStrategy({ highWaterMark: 1024 });
const controlledTransformStream = new TransformStream({
  transform(chunk, controller) {
    controller.enqueue(chunk.toUpperCase());
  }
}, writableStrategy, readableStrategy);
// 使用 TransformStream 处理数据流
fetch("./data.txt")
  .then(response => response.body.pipeThrough(controlledTransformStream))
  .then(stream => {
    // 处理转换后的数据流
  })
  .catch(error => console.error('处理过程中出错:', error));

问题 3:复杂数据转换

  • 描述:当需要对数据进行复杂的转换时,如跨多种数据格式转换,可能会遇到性能问题或处理逻辑复杂的问题。
  • 解决方案:在 transform 方法中,确保逻辑简洁且高效,同时可以将复杂转换分解为多个简单的 TransformStream 进行流水线处理。
const complexTransformStream = new TransformStream({
  async transform(chunk, controller) {
    try {
      // 假设 chunk 是 JSON 字符串
      const data = JSON.parse(chunk);
      // 进行复杂转换,例如数据格式转换
      const transformedData = await someComplexTransformation(data);
      controller.enqueue(JSON.stringify(transformedData));
    } catch (error) {
      controller.error(error);
    }
  }
});
async function someComplexTransformation(data) {
  // 模拟复杂转换逻辑
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(data.map(item => item.toUpperCase()));
    }, 1000);
  });
}
// 使用 TransformStream 处理数据流
fetch("./data.json")
  .then(response => response.body.pipeThrough(complexTransformStream))
  .then(stream => {
    // 处理转换后的数据流
  })
  .catch(error => console.error('处理过程中出错:', error));

问题 4:数据块边界问题

  • 描述:在处理数据块时,可能会遇到数据块不完整或跨数据块边界的问题,特别是在处理流媒体或实时数据时。
  • 解决方案:在 transform 方法中实现逻辑来处理跨数据块的情况,确保每个数据块的完整性。
const boundaryTransformStream = new TransformStream({
  start() {
    this.buffer = '';
  },
  transform(chunk, controller) {
    this.buffer += chunk;
    const parts = this.buffer.split('\n');
    this.buffer = parts.pop(); // 保留最后一个不完整的部分
    parts.forEach(part => controller.enqueue(part));
  },
  flush(controller) {
    if (this.buffer.length > 0) {
      controller.enqueue(this.buffer); // 处理最后的残留数据
    }
  }
});
// 使用 TransformStream 处理数据流
fetch("./data.txt")
  .then(response => response.body.pipeThrough(boundaryTransformStream))
  .then(stream => {
    // 处理转换后的数据流
  })
  .catch(error => console.error('处理过程中出错:', error));

TransformStream 在数据流处理过程中可能遇到处理错误、反压、复杂转换以及数据块边界问题等。通过适当的错误处理、反压管理、优化转换逻辑以及确保数据块完整性,可以有效解决这些问题,从而提升数据流处理的可靠性和性能。

总结

TransformStream 提供了一种强大的方式来处理数据流中的转换操作,它具有以下优势:

  • 流式处理:TransformStream 允许数据以流的形式进行处理,这意味着可以逐步处理数据,而不是一次性加载到内存中,这对于处理大文件或实时数据流非常有用。
  • 异步和非阻塞:TransformStream 的转换操作是异步的,不会阻塞主线程,这有助于提高应用程序的响应性和性能。
  • 链式操作:TransformStream 可以与 ReadableStream 和 WritableStream 一起使用,形成链式调用,使得数据流的处理更加灵活和模块化。
  • 错误处理:TransformStream 提供了错误处理机制,可以在转换过程中捕获和处理错误,避免整个流的处理失败。
  • 可定制性:开发者可以根据需要定义转换逻辑,实现各种复杂的数据转换操作。

总之,TransformStream 提供了一种高效、灵活且易于管理的方式来处理数据流中的转换操作。