Stream 是Node.js中最重要的元件和模式之一,在構建較複雜的系統時,通常將其拆解為功能獨立的若干部分。這些部分的介面遵循一定的規範,通過某種方式相連,以共同完成較複雜的任務。nodejs的核心模組,基本上都是stream的的例項,比如process.stdout、http.clientRequest
什麼是流?
- 流是一組有序的,有起點和終點的位元組資料傳輸手段
- 它不關心檔案的整體內容,只關注是否從檔案中讀到了資料,以及讀到資料之後的處理
- 流是一個抽象介面,被 Node 中的很多物件所實現。比如HTTP 伺服器request和response物件都是流。
簡單的理解,流就是將大塊的東西,分小塊依次處理。就像你需要從水龍頭上接一杯水,那麼當你擰開水龍頭,水管就會一點點的源源不斷的流出來給你。
那麼流這種方式在程式當中又有什麼優勢呢?先看如下程式碼:
let fs = require('fs');
fs.readFile('./1.txt', 'utf8', function(err, data){
// 1.txt 已經讀取完成
console.log(data);
fs.writeFile('/2.txt', data); // 將內容寫入2.txt中
});
複製程式碼
以上兩個方法是實現的功能是將1.txt檔案讀取到記憶體當中,再將它寫入到2.txt檔案中。但是如果檔案過大就會出現問題了,記憶體容易爆掉。那麼這裡比較合適的方式應該是讀寫交替進行,也就是使用流的方式讀寫檔案,這樣不管檔案有多大,都不會一下子耗盡記憶體,可以安全的執行完。
如下:
let fs = require('fs');
let readStream = fs.createReadStream('./1.txt');
let writeStream = fs.createWriteStream('./2.txt');
readStream.on('data', function(chunk) { // 當有資料流出時,寫入資料,chunk的型別為Buffer
writeStream.write(chunk);
});
readStream.on('end', function() { // 當沒有資料時,關閉資料流
writeStream.end();
});
複製程式碼
流的四種型別
在nodejs中,有四種stream型別:
- Readable - 可讀的流,用來讀取資料 (例如 fs.createReadStream()).
- Writable - 可寫的流,用來寫資料 (例如 fs.createWriteStream()).
- Duplex - 可讀寫的流(雙工流),可讀+可寫 (例如 net.Socket).
- Transform - 轉換流,在讀寫過程中可以修改和變換資料的 Duplex 流 (比如 zlib.createDeflate()(資料壓縮/解壓)).
1、可讀流(Readable streams)
nodejs中常見的可讀流有:fs.createReadStream()、http.IncomingRequest、process.stdin
可讀流createReadStream用法如下:
// 建立可讀流
let rs = fs.createReadStream(path,[options]);
// 設定編碼格式
rs.setEncoding('utf8');
// 監聽open事件,開啟檔案時觸發
rs.on('open', function () {
console.log(err);
});
//流切換到流動模式,資料會被儘可能快的讀出
rs.on("data",function(data){
console.log(data); //讀取到的資料
});
// 該事件會在讀完資料後被觸發
rs.on("end",function(data){
console.log("資料已經讀取完畢");
});
//如果讀取檔案出錯了,會觸發error事件
rs.on("error",function(err){
console.log("something is wrong during processing");
})
//檔案關閉觸發
rs.on('close', function () {
console.log('檔案關閉');
});
複製程式碼
1、path讀取檔案的路徑 2、options
- flags開啟檔案要做的操作,預設為'r'
- encoding預設為null
- start開始讀取的索引位置
- end結束讀取的索引位置(包括結束位置)
- highWaterMark讀取快取區預設的大小64kb 如果指定utf8編碼highWaterMark要大於3個位元組
2、可寫流(Writable streams)
可寫流createReadStream
實現了stream.Readable介面的物件,將物件資料讀取為流資料,當監聽data事件後,開始發射資料
let fs = require("fs");
// 建立一個可以寫入的流,寫入到檔案 1.txt 中
let ws= fs.createWriteStream('1.txt');
let data = '寫入流資料';
// 使用 utf8 編碼寫入資料
ws.write(data,'UTF8');
// 表明接下來沒有資料要被寫入 Writable 通過傳入可選的 chunk 和 encoding 引數,可以在關閉流之前再寫入一段資料 如果傳入了可選的 callback 函式,它將作為 'finish' 事件的回撥函式
ws.end("最後寫入的資料","utf8",function(){
console.log(" 我是'finish' 事件的回撥函式")
});
// 在呼叫了 stream.end() 方法,且緩衝區資料都已經傳給底層系統之後, 'finish' 事件將被觸發。
ws.on('finish', function() {
console.log("寫入完成。");
});
// 寫入時發生錯誤觸發
ws.on('error', function(err){
console.log(err.stack);
});
複製程式碼
// 建立可寫流
let ws = fs.createWriteStream(path,[options]);
複製程式碼
1、path讀取檔案的路徑 2、options
- flags開啟檔案要做的操作,預設為'w'
- encoding預設為utf8
- highWaterMark寫入快取區的預設大小16kb
管道流pipe用法 將資料的滯留量限制到一個可接受的水平,以使得不同速度的來源和目標不會淹沒可用記憶體。 linux經典的管道的概念,前者的輸出是後者的輸入 pipe是一種最簡單直接的方法連線兩個stream,內部實現了資料傳遞的整個過程,在開發的時候不需要關注內部資料的流動
用法:
var from = fs.createReadStream('./1.txt');
var to = fs.createWriteStream('./2.txt');
from.pipe(to); // 就是從1.txt中讀一點就往2.txt中寫一點
複製程式碼
3、雙工流(Duplex streams)
Duplex實際上就是繼承了Readable和Writable。 有了雙工流,我們可以在同一個物件上同時實現可讀和可寫,就好像同時繼承這兩個介面。 重要的是雙工流的可讀性和可寫性操作完全獨立於彼此。這僅僅是將兩個特性組合成一個物件
const {Duplex} = require('stream');
const inoutStream = new Duplex({
write(chunk, encoding, callback) {
console.log(chunk.toString());
callback();
},
read(size) {
this.push((++this.index)+'');
if (this.index > 3) {
this.push(null);
}
}
});
inoutStream.index = 0;
process.stdin.pipe(inoutStream).pipe(process.stdout);
複製程式碼
最常見的Duplex stream應該就是net.Socket
例項了。
4、轉換流(Transform streams)
轉換流的輸出是從輸入中計算出來的,Transform stream是Duplex stream的特例。也就是說,Transform stream也同時可讀可寫,它可以用來修改或轉換資料。然它跟Duplex stream的區別在於,Transform stream的輸出與輸入是存在相關性的。你可以認為轉換流就是一個函式,這個函式的輸入是一個可寫流,輸出是一個可讀流。
對於轉換流,我們不必實現read或write的方法,我們只需要實現一個transform方法,將兩者結合起來。它有write方法的意思,我們也可以用它來push資料。
例如:希望將輸入的內容轉化成大寫在輸出出來
const {Transform} = require('stream');
const upperCase = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase()); // 將輸入的內容放入到可讀流中
callback();
}
});
// 希望將輸入的內容轉化成大寫在輸出出來
process.stdin.pipe(upperCase).pipe(process.stdout);
複製程式碼
常見的Transform stream包括zlib、crypto,這裡有個簡單例子:檔案的gzip壓縮。
let fs = require('fs');
let zlib = require('zlib');
let gzip = zlib.createGzip();
// 將1.txt檔案的內容,打包壓縮成compress.txt.gz
let inFile = fs.createReadStream('./file/1.txt');
let outGz = fs.createWriteStream('./file/compress.txt.gz');
inFile .pipe(gzip).pipe(outGz);
複製程式碼