Node.jsのStreamを使ってみる

# はじめに
Node.jsを使っていて，Streamに触れる機会が多々あったため，改めて調べてみました。
この記事はその備忘録になります。

# Streamとは
Node.jsに存在しているオブジェクトの一つで，ストリーミングデータを扱うためのオブジェクト。（[公式ドキュメント](https://nodejs.org/api/stream.html)）

Streamを用いることで，下記のようなメリットがあります。

- **非同期処理**であるため，他の処理と並行して実行することができる。
- データを一括で処理するのではなく，**一定量ずつ処理を行う**ため，メモリを圧迫しない。

以下でStreamを使ったサンプルコードを紹介します。

# 実行ファイル作成

### 実行ファイルの完成形

100KBのcsvファイルを読み込み，それをそのまま別ファイルに書き出します。
今回は，[File System モジュール](https://nodejs.org/api/fs.html)で用意されている[fs.ReadStream](https://nodejs.org/api/fs.html#fs_class_fs_readstream)クラスと，[fs.WriteStream](https://nodejs.org/api/fs.html#fs_class_fs_writestream)クラスを利用します。

```javascript:sample.js
// 必要なモジュールを読み込む
const fs = require('fs');

// 読み込み用のStreamを作成
const readStream = fs.createReadStream('100KB.csv');

// 書き込み用のStreamを作成
const writeStream = fs.createWriteStream('output.csv');

// 読み込み用のStreamにイベントリスナーを追加
// dataイベント : データが読み込まれる度に発火する
readStream.on('data', chunk => {
  console.log(`chunk length = ${chunk.length} (bytes)`);
  return writeStream.write(chunk)
});

// endイベント : データの読み込みが全て完了したら発火する
readStream.on('end', () => {
  return writeStream.end()
});
```
この後一つ一つについて詳細に説明します。

### 読み込み用のStreamを作成
```javascript:sample.js（一部）
// 読み込み用のStreamを作成
const readStream = fs.createReadStream('100KB.csv');
```
100KBのファイルを用意し，それを読み込むためのStreamを作成します。（[fs.createReadStreamメソッド](https://nodejs.org/api/fs.html#fs_fs_createreadstream_path_options)）

### 書き込み用のStreamを作成
```javascript:sample.js（一部）
const writeStream = fs.createWriteStream('output.csv');
```
読み込んだファイルを書き込むファイルを用意し，書き込み用のStreamを作成（[fs.createWriteStreamメソッド](https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options)）

### 読み込んだファイルの内容を，別ファイルに書き込む
```javascript:sample.js（一部）
// 読み込み用のStreamにイベントリスナーを追加
// dataイベント : データが読み込まれる度に発火する
readStream.on('data', chunk => {
  console.log(`chunk length = ${chunk.length} (bytes)`);
  return writeStream.write(chunk)
});
```
[dataイベント](https://nodejs.org/api/stream.html#stream_event_data)は，一定量のデータが読み込まれる度に発火します。
dataイベントのコールバック関数には，読み込んだデータが渡されます。（デフォルトでは[Bufferオブジェクト](https://nodejs.org/api/buffer.html)が渡される）
今回は，渡されたデータをそのまま別ファイルに書き込みます。（[writeable.writeメソッド](https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback)）

### 書き込みを終了する
```javascript:sample.js（一部）
// endイベント : データの読み込みが全て完了したら発火する
readStream.on('end', () => {
  return writeStream.end()
});
```
[endイベント](https://nodejs.org/api/stream.html#stream_event_end)は，読み込むデータがもうない場合に発火します。
endイベントが発火したら，ファイルへの書き込みを終了させます。（[writable.endメソッド](https://nodejs.org/api/stream.html#stream_writable_end_chunk_encoding_callback)）

# 実行
```console
$ node sample.js
chunk length = 65536 (bytes)
chunk length = 36864 (bytes)
```
fs.createReadStreamメソッドで作成された読み込み用のStreamが一度に読み込むデータは，[デフォルトでは64KB](https://nodejs.org/api/fs.html#fs_fs_createreadstream_path_options)なので，一度目のdataイベント発火時には**100KB中の64KB**が読み込まれ，2度目のdataイベント発火時に**残りの36KB**が読み込まれている。

# 参考

### Node.js 公式
- [https://nodejs.org/api/stream.html](https://nodejs.org/api/stream.html)
- [https://nodejs.org/api/fs.html](https://nodejs.org/api/fs.html)

### その他
- [Node.js Stream を使いこなす](https://qiita.com/masakura/items/5683e8e3e655bfda6756)
- [highWaterMarkから探るNode.jsのStreamの仕組み](https://techblog.yahoo.co.jp/advent-calendar-2016/node-stream-highwatermark/)