天天看點

Node.js之StreamNode.js之Stream

Node.js之Stream

對于大部分有後端經驗的的同學來說 Stream 對象是個再合理而常見的對象,但對于前端同學 Stream 并不是那麼理所當然,github 上甚至有一篇 9000 多 Star 的文章介紹到底什麼是 Stream —— stream-handbook。為了更好的了解 Stream,在這篇文章的基礎上簡單總結概括一下。

什麼是 Stream

在 Unix 系統中流就是一個很常見也很重要的概念,從術語上講流是對輸入輸出裝置的抽象。

ls | grep *.js           

類似這樣的代碼我們在寫腳本的時候經常可以遇到,使用 | 連接配接兩條指令,把前一個指令的結果作為後一個指令的參數傳入,這樣資料像是水流在管道中傳遞,每個指令類似一個處理器,對資料做一些加工,是以 | 被稱為 “管道符号”。

NodeJS 中 Stream 的幾種類型

從程式角度而言流是有方向的資料,按照流動方向可以分為三種流

裝置流向程式:readable

程式流向裝置:writable

雙向:duplex、transform

NodeJS 關于流的操作被封裝到了 Stream 子產品,這個子產品也被多個核心子產品所引用。按照 Unix 的哲學:一切皆檔案,在 NodeJS 中對檔案的處理多數使用流來完成

普通檔案

裝置檔案(stdin、stdout)

網絡檔案(http、net)

有一個很容易忽略的知識點:在 NodeJS 中所有的 Stream 都是 EventEmitter 的執行個體。

小例子

我們寫程式忽然需要讀取某個配置檔案 config.json,這時候簡單分析一下

資料:config.json 的内容

方向:裝置(實體磁盤檔案) -> NodeJS 程式

我們應該使用 readable 流來做此事

const fs = require('fs');
const FILEPATH = '...';

const rs = fs.createReadStream(FILEPATH);           

通過 fs 子產品提供的

createReadStream()

方法我們輕松的建立了一個可讀的流,這時候 config.json 的内容從裝置流向程式。我們并沒有直接使用 Stream 子產品,因為 fs 内部已經引用了 Stream 子產品,并做了封裝。

有了資料後我們需要處理,比如需要寫到某個路徑 DEST ,這時候我們遍需要一個 writable 的流,讓資料從程式流向裝置。

const ws = fs.createWriteStream(DEST);           

兩種流都有了,也就是兩個資料加工器,那麼我們如何通過類似 Unix 的管道符号

|

來連結流呢?在 NodeJS 中管道符号就是

pipe()

方法。

const fs = require('fs');
const FILEPATH = '...';

const rs = fs.createReadStream(FILEPATH);
const ws = fs.createWriteStream(DEST);

rs.pipe(ws);           

這樣我們利用流實作了簡單的檔案複制功能,關于 pipe() 方法的實作原理後面會提到,但有個值得注意地方:資料必須是從上遊 pipe 到下遊,也就是從一個 readable 流 pipe 到 writable 流。

加工一下資料

上面提到了 readable 和 writable 的流,我們稱之為加工器,其實并不太恰當,因為我們并沒有加工什麼,隻是讀取資料,然後存儲資料。

如果有個需求,把本地一個 package.json 檔案中的所有字母都改為小寫,并儲存到同目錄下的 package-lower.json 檔案下。

這時候我們就需要用到雙向的流了,假定我們有一個專門處理字元轉小寫的流 lower,那麼代碼寫出來大概是這樣的

const fs = require('fs');

const rs = fs.createReadStream('./package.json');
const ws = fs.createWriteStream('./package-lower.json');

rs.pipe(lower).pipe(ws);           

這時候我們可以看出為什麼稱 pipe() 連接配接的流為加工器了,根據上面說的,必須從一個 readable 流 pipe 到 writable 流:

rs -> lower:lower 在下遊,是以 lower 需要是個 writable 流

lower -> ws:相對而言,lower 又在上遊,是以 lower 需要是個 readable 流

有點推理的趕腳呢,能夠滿足我們需求的 lower 必須是雙向的流,具體使用 duplex 還是 transform 後面我們會提到。

當然如果我們還有額外一些處理動作,比如字母還需要轉成 ASCII 碼,假定有一個流 ascii 那麼我們代碼可能是

rs.pipe(lower).pipe(acsii).pipe(ws);           

同樣 ascii 也必須是雙向的流。這樣處理的邏輯是非常清晰的,那麼除了代碼清晰,使用流還有什麼好處呢?

為什麼應該使用 Stream

有個使用者需要線上看視訊的場景,假定我們通過 HTTP 請求傳回給使用者電影内容,那麼代碼可能寫成這樣

const http = require('http');
const fs = require('fs');

http.createServer((req, res) => {
   fs.readFile(moviePath, (err, data) => {
      res.end(data);
   });
}).listen(8080);           

這樣的代碼又兩個明顯的問題

電影檔案需要讀完之後才能傳回給客戶,等待時間超長

電影檔案需要一次放入記憶體中,相似動作多了,記憶體吃不消

用流可以講電影檔案一點點的放入記憶體中,然後一點點的傳回給客戶(利用了 HTTP 協定的 Transfer-Encoding: chunked 分段傳輸特性),使用者體驗得到優化,同時對記憶體的開銷明顯下降

const http = require('http');
const fs = require('fs');
http.createServer((req, res) => {
   fs.createReadStream(moviePath).pipe(res);
}).listen(8080);           

除了上述好處,代碼優雅了很多,拓展也比較簡單。比如需要對視訊内容壓縮,我們可以引入一個專門做此事的流,這個流不用關心其它部分做了什麼,隻要是接入管道中就可以了

const http = require('http');

const fs = require('fs');

const oppressor = require(oppressor);

http.createServer((req, res) => {

   fs.createReadStream(moviePath)

      .pipe(oppressor)

      .pipe(res);

}).listen(8080);           

可以看出來,使用流後,我們的代碼邏輯變得相對獨立,可維護性也會有一定的改善,關于幾種流的具體使用方式且聽下回分解。

繼續閱讀