NodeJS Stream 二:什麼是 Stream

對於大部分有後端經驗的的同窗來講 Stream 對象是個再合理而常見的對象,但對於前端同窗 Stream 並非那麼理所固然,github 上甚至有一篇 9000 多 Star 的文章介紹到底什麼是 Stream —— stream-handbook。爲了更好的理解 Stream,在這篇文章的基礎上簡單總結歸納一下。javascript

什麼是 Stream

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

ls | grep *.js

相似這樣的代碼咱們在寫腳本的時候常常能夠遇到,使用 | 鏈接兩條命令,把前一個命令的結果做爲後一個命令的參數傳入,這樣數據像是水流在管道中傳遞,每一個命令相似一個處理器,對數據作一些加工,所以 | 被稱爲 「管道符號」。前端

NodeJS 中 Stream 的幾種類型

從程序角度而言流是有方向的數據,按照流動方向能夠分爲三種流java

  1. 設備流向程序:readable
  2. 程序流向設備:writable
  3. 雙向:duplex、transform

NodeJS 關於流的操做被封裝到了 Stream 模塊,這個模塊也被多個核心模塊所引用。按照 Unix 的哲學:一切皆文件,在 NodeJS 中對文件的處理多數使用流來完成node

  1. 普通文件
  2. 設備文件(stdin、stdout)
  3. 網絡文件(http、net)

有一個很容易忽略的知識點:在 NodeJS 中全部的 Stream 都是 EventEmitter 的實例。git

小例子

咱們寫程序突然須要讀取某個配置文件 config.json,這時候簡單分析一下github

  • 數據:config.json 的內容
  • 方向:設備(物理磁盤文件) -> NodeJS 程序

咱們應該使用 readable 流來作此事shell

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

const rs = fs.createReadStream(FILEPATH);

經過 fs 模塊提供的 createReadStream() 方法咱們輕鬆的建立了一個可讀的流,這時候 config.json 的內容從設備流向程序。咱們並無直接使用 Stream 模塊,由於 fs 內部已經引用了 Stream 模塊,並作了封裝。json

有了數據後咱們須要處理,好比須要寫到某個路徑 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);

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

  1. 電影文件須要讀完以後才能返回給客戶,等待時間超長
  2. 電影文件須要一次放入內存中,類似動做多了,內存吃不消

用流能夠講電影文件一點點的放入內存中,而後一點點的返回給客戶(利用了 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);

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

相關文章
相關標籤/搜索