Node.js操作按需資料使用sream API介面,stream 是一個資料集,資料可能不能馬上全部獲取到,他們在緩衝區,不需要在記憶體中。適合處理大資料集或者來自外部的資料來源的資料塊 Node中很多內建模組實現了流式介面:
上面的列表中的原生Node.js物件就是可讀流和可寫流的物件。有些物件是可讀流也是可寫流,如TCP sockets,zlib 和 crypto streams
這些物件是密切相關。當一個HTTP響應在客戶端上是一個可讀流,相應的在服務端是一個可寫流。這是因為在HTTP的情況下,我們基於從一物件(http.IncomingMessage)讀而從另外一個物件(http.ServerResponse)寫
stdio流(stdin,stdout,stderr)在子程式中會有反向流型別。這樣的話就能用非常簡單的方式管道傳送給其他流或者主程式的stdio流。
Node.js中有4個基本的流型別:
- 可讀流(Readable)
- 可寫流(Writable)
- 雙工流(Duplex)
- 轉換流(Transform streams)
- 可讀流是可以被消耗的資料來源的抽象,典型例子就是fs.createReadStream方法。
- 可寫流是可以寫入資料的目的地的抽象,典型例子就是 fs.createWriteStream 方法。
- 雙工流既是可讀的也是可寫的,典型例子是TCP套接字。
- 轉換流是基於雙工流的,它可以用來修改或轉換資料,因為它是寫入和讀取的。 zlib.createGzip 就是一個用gzip來壓縮資料的轉換流例子。你可以認為轉換流就是一個函式,這個函式的輸入是一個可寫流,輸出是一個可讀流,你可能也聽說過把轉換流叫做" 通過流 "。
所有的流都是 EventEmitter 的例項。他們在資料可讀或者可寫的時候發出事件。然而,我們也可以簡單的通過 pipe 方法來使用流資料。
pipe方法:
**readable**.pipe(**writableDest**)
複製程式碼
這簡單的一行,連線了可讀流的輸出——源資料和可寫流的輸入——目標。源必須是可讀流,目標必須是可寫流。當然也可以是雙工流或者轉換流,事實上,如果連線的是一個雙工流,可以鏈式呼叫pipe:
readable
.pipe(transformStream1)
.pipe(transformStream2)
.pipe(finalWrtitableDest)
複製程式碼
pipe方法返回目標流,這使我們能夠執行上面的鏈式呼叫。對於流a(可讀)、b和c(雙工)和d(可寫)
a.pipe(b).pipe(c).pipe(d)
複製程式碼
上面等價於
a.pipe(b)
b.pipe(c)
c.pipe(d)
複製程式碼
pipe 方法是最簡單的方式去使用流,一般建議使用 pipe 方法或使用事件來處理流,但是要避免兩個混合使用。通常,當你使用 pipe 方法時,你不需要使用事件,但是如果你需要用更多定製的方式來處理流,那你可以只用事件。
流事件
除了從可讀流裡讀取資料和向可寫流目標寫資料外,pipe方法將自動管理沿途的一些事情。例如,它處理錯誤、檔案結束以及當一個流比另一個流慢或更快時的情況。
然而,我們也可以直接使用事件來操作流。下面是pipe方法主要用於讀取和寫入資料的事件的簡化等效程式碼:
# readable.pipe(writable) 等於下面
複製程式碼
readable.on('data', (chunk) => {
writable.write(chunk);
});
複製程式碼
readable.on('end', () => {
writable.end();
});
複製程式碼
以下是可讀流可寫流的重要事件以及可用方法:
這些事件和函式在某種程度上是相關的,因為它們通常一起使用。
可讀流中最重要的事件是:
data
事件,每當流將資料塊傳遞給消費者時,它就會觸發。end
事件,當沒有更多的資料從流中被消耗時觸發。
可寫流中最重要的事件是:
drain
事件,這是可寫流可以接收更多資料的訊號。finish
事件,當所有資料都給到底層系統時觸發。
可以結合事件和函式來定製和優化流的使用。使用一個可讀的流,我們可以用pipe
/ unpipe
方法,或read
/ Unshift
/ resume方法。使用一個可寫流,我們可以把它pipe
/ unpipe
目的地,或是寫它的write
方法呼叫end
方法當我們完成。
可讀流的暫停和流(flowing)模式
可讀流有兩種主要模式,這影響我們可以使用它們的方式:
- 它們可以是
暫停(paused)
模式 - 或是
流(flowing)
模式
這些模式有時被稱為拉和推模式。
所有可讀的流預設情況下都是在暫停模式下啟動的,但在需要時可以輕鬆切換到流模式或者返回到暫停狀態。有時,轉換是自動發生。
當一個可讀流處於暫停模式,我們可以使用 read() 方法按需的從流中讀取資料,然而,在流模式下的可讀流,資料是不斷流動的,我們要監聽事件來使用這些資料。
在流模式下,如果沒有使用者處理資料,那麼實際上資料會丟失。這就是為什麼當我們在流模式中有可讀的流時,我們需要一個 data 事件。事實上,只要新增一個 data 事件,就可以將暫停模式轉換為流模式,刪除 data 事件,流將切換回暫停模式。其中一些這樣做事為了與舊的節點流介面向後相容。
這兩個流模式之間手動開關,可以使用 resume() 和 pause() 方法。
當使用 pipe 方法讀取可讀流時,我們不必擔心這些模式,因為pipe自動管理它們。
實現流
當我們談論Node.js中的流,主要有兩種不同的任務:
- 實現流。
- 使用流。
流的實現通常會 引入 (require)stream
模組。
實現可寫流
為了實現可寫流,我們需要使用流模組中的 Writable 建構函式。
const { Writable } = require('stream');
複製程式碼
我們有很多方式來實現一個可寫流。例如,如果我們想要的話,我們可以繼承Writable建構函式。
class myWritableStream extends Writable {
}
複製程式碼
這裡用簡單的建構函式的方法。我們只需給 Writable 建構函式傳遞一些選項並建立一個物件。唯一需要的選項是 Writable 函式,該函式揭露資料塊要往哪裡寫。
const { Writable } = require('stream');
複製程式碼
const outStream = new Writable({
**write**(chunk, encoding, callback) {
console.log(chunk.toString());
callback();
}
});
process.stdin.pipe(outStream);
複製程式碼
這個write函式有3個引數:
- chunk 通常是一個buffer,除非我們配置不同的流。
- encoding 是在特定情況下需要的引數,通常我們可以忽略它。
- callback 是在完成處理資料塊後需要呼叫的函式。這是寫資料成功與否的標誌。若要發出故障訊號,請用錯誤物件呼叫回撥函式。
在 outstream,我們只是用 console.log 把資料塊作為一個字串列印到控制檯,然後不用錯誤物件呼叫 callback 表示成功。這是一個非常簡單的可能也不那麼有用的 echo 流,它把收到的所有資料列印到控制檯。
為了使用這個流,我們可以直接用 process.stdin 這個可讀流,就可以把 process.stdin pipe給 outStream.
執行上面的程式碼,任何我們輸入給 process.stdin 的內容都會被 outStream 的 console.log 輸出到控制檯。
實現這個流不怎麼有用,因為它實際上被實現了而且node內建了,它等同於 process.stdout。以下一行程式碼,就是把 stdin pipe給 stdout ,就能實現之前的效果:
process.stdin.pipe(process.stdout);
複製程式碼
實現可讀流
為了實現可讀流,引用Readable介面並用它構造新物件:
const { Readable } = require('stream');
複製程式碼
const inStream = new Readable({});
複製程式碼
有一個簡單的方法來實現可讀流。我們可以直接把供使用的資料 push 出去。
const { Readable } = require('stream');
const inStream = new Readable();
inStream.push('ABCDEFGHIJKLM');
inStream.push('NOPQRSTUVWXYZ');
inStream.push(null); // No more data
inStream.pipe(process.stdout);
複製程式碼
當 push 一個 null 物件就意味著我們想發出訊號——這個流沒有更多資料了。
使用這個可寫流,可以直接把它pipe給 process.stdout 這個可寫流。
執行以上程式碼,會讀取 inStream 中所有的資料,並輸出在標準輸出流。很簡單,也不是很有用。
我們基本上在pipe給 process.stdout 之前把所有的資料都推到流裡了。更好的方法是按需推送。我們可以通過在一個可讀流的配置實現 read() 方法來做這件事情:
const inStream = new Readable({
**read**(size) {
// there is a demand on the data... Someone wants to read it.
}
});
複製程式碼
當在可讀的流上呼叫讀方法時,實現可以將部分資料推到佇列中。例如,我們可以一次推送一個字母,從字元程式碼65(代表A),並且每推一次增加1:
const inStream = new Readable({
read(size) {
**this.push**(String.fromCharCode(this.currentCharCode++));
if (this.currentCharCode > 90) {
**this.push**(null);
}
}
});
inStream.currentCharCode = 65;
inStream.pipe(process.stdout);
複製程式碼
當從可讀流裡讀資料, read 方法將被持續呼叫,我們就會推送更多的字母。我們需要停止這個迴圈的條件,這就是為什麼會一個if語句當currentcharcode大於90(代表Z)是推送null。
這段程式碼相當於我們開始使用的更簡單的程式碼,但是當使用者要求時,我們正在按需推送資料。你應該經常這樣做。