Node.js 中 Stream API 的使用

scarlex發表於2015-10-22

基本介紹

在 Node.js 中,讀取檔案的方式有兩種,一種是用 fs.readFile,另外一種是利用 fs.createReadStream 來讀取。

fs.readFile 對於每個 Node.js 使用者來說最熟悉不過了,簡單易懂,很好上手。但它的缺點是會先將資料全部讀入記憶體,一旦遇到大檔案的時候,這種方式讀取的效率就非常低下了。

fs.createReadStream 則是通過 Stream 來讀取資料,它會把檔案(資料)分割成小塊,然後觸發一些特定的事件,我們可以監聽這些事件,編寫特定的處理函式。這種方式相對上面來說,並不好上手,但它效率非常高。

事實上, Stream 在 Node.js 中並非僅僅用在檔案處理上,其他地方也可以看到它的身影,如 process.stdin/stdout, http, tcp sockets, zlib, crypto 等都有用到。

本文是我學習 Node.js 中的 Stream API 中的一點總結,希望對大家有用。

特點

  • 基於事件通訊

  • 可以通過 pipe 來連線流

種類

  • Readable Stream 可讀資料流

  • Writeable Stream 可寫資料流

  • Duplex Stream 雙向資料流,可以同時讀和寫

  • Transform Stream 轉換資料流,可讀可寫,同時可以轉換(處理)資料

事件

可讀資料流的事件

  • readable 資料向外流時觸發

  • data 對於那些沒有顯式暫停的資料流,新增data事件監聽函式,會將資料流切換到流動態,儘快向外提供資料

  • end 讀取完資料時觸發。注意不能和 writeableStream.end() 混淆,writeableStream 並沒有 end 事件,只有 .end() 方法

  • close 資料來源關閉時觸發

  • error 讀取資料發生錯誤時觸發

可寫資料流的事件

  • drain writable.write(chunk) 返回 false 之後,快取全部寫入完成,可以重新寫入時就會觸發

  • finish 呼叫 .end 方法時,所有快取的資料釋放後觸發,類似於可讀資料流中的 end 事件,表示寫入過程結束

  • pipe 作為 pipe 目標時觸發

  • unpipe 作為 unpipe 目標時觸發

  • error 寫入資料發生錯誤時觸發

狀態

可讀資料流有兩種狀態:流動態暫停態,改變資料流狀態的方法如下:

暫停態 -> 流動態

  • 新增 data 事件的監聽函式

  • 呼叫 resume 方法

  • 呼叫 pipe 方法

注意: 如果轉為流動態時,沒有 data 事件的監聽函式,也沒有 pipe 方法的目的地,那麼資料將遺失。

流動態 -> 暫停態

  • 不存在 pipe 方法的目的地時,呼叫 pause 方法

  • 存在 pipe 方法的目的地時,移除所有 data 事件的監聽函式,並且呼叫 unpipe 方法,移除所有 pipe 方法的目的地

注意: 只移除 data 事件的監聽函式,並不會自動引發資料流進入「暫停態」。另外,存在 pipe 方法的目的地時,呼叫 pause 方法,並不能保證資料流總是處於暫停態,一旦那些目的地發出資料請求,資料流有可能會繼續提供資料。

用法

讀寫檔案

var fs = require('fs');
// 新建可讀資料流
var rs = fs.createReadStream('./test1.txt');
// 新建可寫資料流
var ws = fs.createWriteStream('./test2.txt');

// 監聽可讀資料流結束事件
rs.on('end', function() {
    console.log('read text1.txt successfully!');
});
// 監聽可寫資料流結束事件
ws.on('finish', function() {
    console.log('write text2.txt successfully!');
});
// 把可讀資料流轉換成流動態,流進可寫資料流中
rs.pipe(ws);

讀取 CSV 檔案,並上傳資料(我在生產環境中寫過)

var fs = require('fs');
var es = require('event-stream');
var csv = require('csv');
var parser = csv.parse();
var transformer = csv.transform(function(record) {
    return record.join(',');
});

var data = fs.createReadStream('./demo.csv');
data
    .pipe(parser)
    .pipe(transformer)
    // 處理前一個 stream 傳遞過來的資料
    .pipe(es.map(function(data, callback) {
        upload(data, function(err) {
            callback(err);
        });
    }))
    // 相當於監聽前一個 stream 的 end 事件
    .pipe(es.wait(function(err, body) {
        process.stdout.write('done!');
    }));

更多用法

可以參考一下 https://github.com/jeresig/node-stream-playground ,進去示例網站之後直接點 add stream 就能看到結果了。

常見坑

  • rs.pipe(ws) 的方式來寫檔案並不是把 rs 的內容 append 到 ws 後面,而是直接用 rs 的內容覆蓋 ws 原有的內容

  • 已結束/關閉的流不能重複使用,必須重新建立資料流

  • pipe 方法返回的是目標資料流,如 a.pipe(b) 返回的是 b,因此監聽事件的時候請注意你監聽的物件是否正確

    • 如果你要監聽多個資料流,同時你又使用了 pipe 方法來串聯資料流的話,你就要寫成:

      data
          .on('end', function() {
              console.log('data end');
          })
          .pipe(a)
          .on('end', function() {
              console.log('a end');
          })
          .pipe(b)
          .on('end', function() {
              console.log('b end');
          });

常用類庫

  • event-stream 用起來有函數語言程式設計的感覺,個人比較喜歡

  • awesome-nodejs#streams 由於其他 stream 庫我都沒用過,所以有需求的就直接看這裡吧

出處

https://scarletsky.github.io/2015/10/22/node-stream-api-learning/

參考資料

阮一峰 - stream介面
nodejs.org Stream
Transforming data with Node.js transform streams
NodeJS: What's the difference between a Duplex stream and a Transform stream?

相關文章