1. 程式人生 > >極簡 Node.js 入門 - 4.2 初識 stream

極簡 Node.js 入門 - 4.2 初識 stream

> 極簡 Node.js 入門系列教程:[https://www.yuque.com/sunluyong/node](https://www.yuque.com/sunluyong/node) > > 本文更佳閱讀體驗:[https://www.yuque.com/sunluyong/node/stream](https://www.yuque.com/sunluyong/node/stream) ## stream 概念 Node.js 誕生是為了解決 I/O 密集的 Web 效能問題,最常使用的兩個模組就是檔案系統和網路,而這兩個模組都是 stream 的重度使用者,stream 是 Node.js 從入門到進階的必經之路

Node.js 對 stream 是這樣解釋的 >
A stream is an abstract interface for working with streaming data in Node.js. The `stream` module provides an API for implementing the stream interface.
翻譯過來流是 Node.js 中處理流式資料的抽象介面。 `stream` 模組提供了用於實現流介面的物件。基本就是用 stream 解釋自己,第一次使用肯定不理解,但其實我們平時經常用到流
```bash ls | grep *.js ```
命令的含義是 list 當前目錄檔案,把結果交給 grep 命令,按行篩選出拓展名是 js 的內容 使用 `|` 連線兩條命令,把前一個命令的結果作為後一個命令的引數傳入,這樣資料像是水流在管道中傳遞,每個命令類似一個處理器,對資料做一些加工,因此 `|` 被稱為“管道符”,這個處理過程就是流 從術語上講**流是對輸入輸出裝置的抽象,是一組有序的、有起點和終點的位元組資料傳輸手段**

![image.png](https://cdn.nlark.com/yuque/0/2020/png/87727/1586168242175-a0bd20db-f4d2-4cde-af5c-f6837a479f01.png#align=left&display=inline&height=209&margin=%5Bobject%20Object%5D&name=image.png&originHeight=418&originWidth=984&size=110266&status=done&style=none&width=492) ## Node.js stream 型別 從程式角度而言流是有方向的資料,以程式為第一視角,按照流動方向可以分為三種流 1. 裝置流向程式:readable 1. 程式流向裝置:writable 1. 雙向流動:duplex、transform
NodeJS 關於流的操作被封裝到了 [Stream](https://link.zhihu.com/?target=http%3A//nodejs.org/docs/latest/api/stream.html) 模組,這個模組也被多個核心模組所引用。按照 Unix 的哲學:一切皆檔案 1. 普通檔案(txt、jpg、mp4) 1. 裝置檔案(stdin、stdout) 1. 網路檔案(http、net)
在 Node.js 中對檔案的處理多數使用流來完成 ## 小試牛刀 假設寫程式某個功能需要讀取某個配置檔案 **_config.json_**,這時候簡單分析一下 - 資料:config.json 的內容 - 方向:裝置(物理磁碟檔案) -> NodeJS 程式 應該使用 readable 流來做此事(後面章節會具體介紹 API)
```javascript const fs = require('fs'); const FILEPATH = '...'; const rs = fs.createReadStream(FILEPATH); ```
通過 fs 模組提供的 createReadStream() 方法建立了一個可讀的流,這時候 _**config.json**_的內容從裝置流向程式。示例並沒有直接使用 Stream 模組,因為 fs 內部已經引用了 Stream 模組,並做了封裝

讀取到資料後可以對資料進行處理,比如需要寫到某個路徑 DEST ,這時候需要一個 writable 的流,讓資料從程式流向裝置
```javascript const ws = fs.createWriteStream(DEST); ```
兩種流都有了,也就是兩個資料加工器,那麼如何通過類似 Unix 的管道符** | **來連線流呢?在 Node.js 中管道符號就是 pipe() 方法
```javascript const fs = require('fs'); const FILEPATH = 'DEST_PATH_IN_YOUR_DISK'; 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_ 檔案下

這時候就需要用到雙向的流了,假定有一個專門處理字元轉小寫的流 `lowercase` ,那麼程式碼寫出來大概是這樣的
```javascript const fs = require('fs'); const rs = fs.createReadStream('./package.json'); const ws = fs.createWriteStream('./package-lower.json'); rs.pipe(lowercase).pipe(ws); ```
到這裡就和清楚為什麼稱 pipe() 連線的流為加工器了,根據上面說的,必須從一個 readable 流 pipe 到 writable 流: - rs -> lowercase:lowercase 在下游,所以 lower 需要是個 writable 流 - lowercase -> ws:相對而言,lowercase 又在上游,所以 lower 需要是個 readable 流
因此能夠滿足需求的 lowercase 必須是雙向的流,具體使用 duplex 還是 transform 後面章節會提到。當然如果需求還有額外一些處理動作,比如字母還需要轉成 ASCII 碼,假定有一個雙工流 ascii,那麼程式碼簡單改寫
```javascript rs.pipe(lowercase).pipe(acsii).pipe(ws); ```
這樣就完成了 package.json 檔案字母轉小寫後處理成 acsii 碼的需求 ## 為什麼應該使用 stream 有個使用者 Web 線上看視訊的場景,假定我們通過 HTTP 請求返回給使用者電影內容,那麼程式碼可能寫成這樣
```javascript const http = require('http'); const fs = require('fs'); http.createServer((req, res) => { fs.readFile(moviePath, (err, data) => { res.end(data); }); }).listen(8080); ```
這樣的程式碼又兩個明顯的問題 1. 電影檔案需要讀完之後才能返回給客戶,等待時間超長 1. 電影檔案需要一次放入記憶體中,記憶體吃不消
使用 stream 可以把電影檔案一點點的放入記憶體中,然後一點點的返回給客戶(利用了 HTTP 協議的 Transfer-Encoding: chunked 分段傳輸特性),使用者體驗得到優化,同時對記憶體的開銷明顯下降
```javascript const http = require('http'); const fs = require('fs'); http.createServer((req, res) => { fs.createReadStream(moviePath).pipe(res); }).listen(8080); ```
除了上述好處,stream 還讓程式碼優雅了很多,功能邏輯獨立,拓展也比較簡單。比如需要對視訊內容壓縮,我們可以引入一個專門做此事的流,這個流不用關心其它部分做了什麼,只要是接入管道中就可以了
```javascript 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); ```
可以看出來使用流後代碼邏輯變得相對獨立,可維護性也會有一定的改善