Web Streams и их применение

Web Streams — это интерфейс, позволяющий работать с потоками данных в браузере и Node.js. Поток (stream) представляет собой последовательность данных, которая может обрабатываться постепенно, без необходимости загружать весь объём сразу. Основные типы потоков в Web Streams: ReadableStream (для чтения данных), WritableStream (для записи данных), TransformStream (для преобразования данных на лету).

Потоки особенно полезны при работе с большими файлами, сетевыми запросами и динамическими данными, позволяя уменьшить нагрузку на память и повысить отзывчивость приложений.


ReadableStream

ReadableStream предоставляет интерфейс для чтения данных кусками. Основные компоненты:

  • Source — источник данных, который может быть синхронным или асинхронным.
  • Reader — объект, который извлекает данные из потока.

Простейший пример создания потока, который выдаёт последовательность чисел:

const stream = new ReadableStream({
  start(controller) {
    for (let i = 1; i <= 5; i++) {
      controller.enqueue(i);
    }
    controller.close();
  }
});

const reader = stream.getReader();
async function read() {
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    console.log(value);
  }
}
read();

Ключевой момент: enqueue добавляет данные в поток, close завершает поток, а getReader() позволяет асинхронно читать данные кусками.


WritableStream

WritableStream используется для записи данных. Основные методы:

  • write(chunk) — записывает кусок данных.
  • close() — завершает запись.
  • abort(reason) — прекращает поток с ошибкой.

Пример записи текста в консоль через поток:

const writable = new WritableStream({
  write(chunk) {
    console.log('Записано:', chunk);
  },
  close() {
    console.log('Поток завершён');
  }
});

const writer = writable.getWriter();
writer.write('Первый фрагмент');
writer.write('Второй фрагмент');
writer.close();

Особенность: getWriter() предоставляет интерфейс для безопасной записи в поток.


TransformStream

TransformStream объединяет чтение и запись, позволяя преобразовывать данные «на лету». Он содержит:

  • writable — входящий поток.
  • readable — выходящий поток.

Пример преобразования текста в верхний регистр:

const upperCaseTransform = new TransformStream({
  transform(chunk, controller) {
    controller.enqueue(chunk.toUpperCase());
  }
});

const writer = upperCaseTransform.writable.getWriter();
const reader = upperCaseTransform.readable.getReader();

writer.write('тест');
writer.close();

(async () => {
  const { value, done } = await reader.read();
  if (!done) console.log(value); // 'ТЕСТ'
})();

Важное замечание: TransformStream позволяет создавать цепочки обработки данных, что делает его идеальным для фильтрации, шифрования и сжатия.


Применение Web Streams в Lit

Lit (ранее LitElement) интегрируется с Web Streams через реактивные свойства и шаблоны. Потоки позволяют эффективно рендерить данные по мере их поступления.

Пример асинхронного рендеринга списка из потокового источника:

import { LitElement, html, css } from 'lit';

class StreamedList extends LitElement {
  static properties = {
    items: { type: Array }
  };

  constructor() {
    super();
    this.items = [];
    this.loadItems();
  }

  async loadItems() {
    const stream = new ReadableStream({
      async start(controller) {
        for (let i = 1; i <= 5; i++) {
          await new Promise(res => setTimeout(res, 500));
          controller.enqueue(`Элемент ${i}`);
        }
        controller.close();
      }
    });

    const reader = stream.getReader();
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      this.items = [...this.items, value];
    }
  }

  render() {
    return html`
      <ul>
        ${this.items.map(item => html`<li>${item}</li>`)}
      </ul>
    `;
  }
}

customElements.define('streamed-list', StreamedList);

Ключевой момент: Поток позволяет рендерить элементы постепенно, улучшая пользовательский опыт и снижая нагрузку на DOM.


Потоки и Fetch API

Fetch API поддерживает потоковое чтение тела ответа через response.body. Это особенно полезно для больших файлов или потоковой передачи данных в реальном времени.

const response = await fetch('/large-file.txt');
const reader = response.body.getReader();
let decoder = new TextDecoder();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  console.log(decoder.decode(value));
}

Особенность: использование потоков с fetch предотвращает загрузку всего файла в память, что критично при работе с большими объёмами данных.


Цепочки потоков и композиция

Web Streams поддерживают соединение нескольких потоков в цепочку:

const source = new ReadableStream({
  start(controller) {
    controller.enqueue('data1');
    controller.enqueue('data2');
    controller.close();
  }
});

const upperCaseTransform = new TransformStream({
  transform(chunk, controller) {
    controller.enqueue(chunk.toUpperCase());
  }
});

const destination = new WritableStream({
  write(chunk) {
    console.log(chunk);
  }
});

source.pipeThrough(upperCaseTransform).pipeTo(destination);

Принцип: pipeThrough применяет преобразование к потоку, pipeTo направляет поток в WritableStream. Такой подход упрощает обработку данных в несколько этапов, улучшая читаемость и поддержку кода.


Потоки и реактивность Lit

Web Streams идеально сочетаются с реактивными свойствами Lit. Потоки позволяют обновлять состояние компонента постепенно, что делает интерфейс отзывчивым без полной перерисовки.

  • ReadableStream — для поступления данных.
  • WritableStream — для записи состояния или логирования.
  • TransformStream — для преобразования данных перед рендером.

Использование потоков в Lit повышает производительность при работе с асинхронными источниками данных, такими как API, WebSocket или большие локальные файлы.