Node.js fs 模組 APIs 的三種類型

前陣子,我在「海納百川:Node.js Streams」這篇文章的範例中,在使用了 fs.readFile() 這支 API 時,有提到它是 Bulk I/O 這件事,然後再跟 fs.createReadStream() 來比較兩者之間的差異。為了讓你不用再回去那篇文章重看一次,我這裡把程式碼貼過來再稍微整理一下。


我們當時是為了讀取一支 2 GB 這麼大的影音檔:


[使用 fs.readFile() 來實作 ]


我們建立一個 http 伺服器 server.js,它使用 fs.readFile() 來讀檔,當有 request 進來的時候,我們把檔案送過去給瀏覽器。我們順便檢測一下錯誤,若有錯誤就把它印出來。當執行 server.js 並開啟瀏覽器到 http://locahost:3000 來下載檔案時,我們的 server 可能會因此爆炸。 (可能爆炸的原因詳見「海納百川:Node.js Streams」內文說明)


var http = require('http');

var fs = require('fs');


var server = http.createServer(function (req, res) {

   // 利用 fs.readFile() 來讀取 xxx.avi 這支大檔案

   fs.readFile('xxx.avi', function (err, data) {

       if (err) {

           // 如果發生錯誤就把它列印在 console, 並傳回失敗響應給 Client 端

           console.log(err);

           res.writeHead(500);

           res.end(err.message);

       } else {

           // 如果成功, 就使用 res.end() 將檔案資料送給 Client 端

           res.writeHead(200, { 'Content-Type': 'video/avi' });

           res.end(data);

       }

   });

});


server.listen(3000, function () {

   console.log('Secret server is up');

});



[ 改用 Stream 來實作 ]


同樣的讀檔功能,改用 Stream 的方式來實作,一切都沒問題!


var server = http.createServer(function (req, res) {

   res.writeHead(200, { 'Content-Type': 'video/avi' });


   // 將數據來源變成 ReadableStream, 然後 pipe 給 res

   fs.createReadStream('xxx.avi').pipe(res) 

       .on('finish', function () {

           console.log('Sending done.');

       });

});


上面兩種作法的差異

[使用 fs.readFile()]



使用 fs.readFile(pathToFile, function (err, data) {}) 這支 Bulk I/O API (Bulk 是一大坨的意思),底層會將檔案內容先讀進系統緩衝之中,然後在 callback 透過 data 一次給你一大包,就像右邊這張圖的感覺。


這意味著,當你使用這樣的 API 存取 I/O 時,你的 memory footprint 將有那麼一時半刻會衝到很高。如果剛剛好你的系統記憶體負載很繁重,那就只好眼睜睜看著它爆炸了 XDDD....


[使用 fs.createStream()]



那如果我們使用 fs 的 Readable Stream 工廠方法 fs.createReadStream(),將數據來源(檔案) 變成是一個 ReadableStream 呢?現在的感覺會像旁邊這張圖,數據將會是一個 chunk 一個 chunk 地傳送給消耗者,原本一大坨的數據將被切成一小塊一小塊來傳送,這每一小塊的最大容量則是由 Stream 內部緩衝區的最大容量所決定 (預設為 16 kB)。如此,我們就不必等所有數據都到達,然後才一股腦地丟出一大包。

Node.js API 手冊

以上是一點簡單的回顧,接下來我們稍微來看一下官方 API 手冊的說明,這裡做一下摘要:

 

  • fs 模組幫你把標準 POSIX 的 File I/O 操作包裝成一支支的方法
  • File I/O 的方法有 asynchronous 與 synchronous 兩種形式
  • Asynchronous 方法的 callback,一律遵照 Node.js 的 err-back style 慣例 (callback 函式簽署的第一個參數為 error 物件)


Asynchronous 與 Synchronous APIs

以 chmod 為例,fs 提供了非同步的 fs.chmod() 與同步的 fs.chmodSync() 兩種形式:



在 API 說明中的 chmod(2) 連結會指向 Linux Programmer's Manual,你可以在那裡閱讀關於此操作的功能說明。另外,chmod(2) 中的 (2) 用於說明該方法的分類,我們可以看一下 man 的說明如下圖,(2) 是屬於 System Calls。



我們在 fs 模組的手冊中,會看到很多檔案操作的方法都有非同步與同步兩種版本:



這裡附帶說明一下,同步 API 大多為 Node 內部使用,另一種情況則是在我們自己的初始化程式碼中會使用到他們。當我們的程式完成初始化,運行起來之後,整個程式就進到了事件迴圈的模式,除了有特殊需求之外,我們大部分會使用到的都是 fs 提供的非同步 APIs。

Streams

除了上面看到的兩種 API 的形式之外,fs 還有兩個 Stream Class:ReadStream 與 WriteStream。在實作上會更常使用 createReadStream() 與 createWriteStream() 這兩支工廠方法來將目標檔案實例化為 Streams,這在我們最上面提供的範例就有看到 createReadStream() 的用法。當然,Stream 也是屬於「非同步」的介面。



結語

這邊多補充兩個小點子。如果覺得 fs 的 mkdir 不好用,推薦大家使用 substack 寫的 mkdirp,它也是一個被爆量使用的小模組。然後,我常看到有些範例在讀取 json 檔時,會使用 readFile() 或 readFileSync() 將資料讀入後,再 JSON.parse() 來產生 JS 物件。對於 json 檔,在 Node.js 中只要用 var foo = require('path/to/xxx/json') 就可以將 json 讀入成為物件了(這應該蠻多人都知道啦),這是因為 Node 的 require 系統原始碼很佛心幫我們處理掉了。


最後來個總結,Node.js 的 fs 模組提供給我們的三種不同類型的 APIs 有:

  • 同步的:結果(或資料) 以 return 傳回
  • 非同步的:結果(或資料) 傳回給 callback,或說我們使用 callback 來接回結果
  • Stream:結果(或資料) 以事件的方式傳回,或說我們使用 event handler 來接回結果


--

YOTTA 你最專業的學習夥伴,提供優質內容與有趣觀點,擴大豐富你的視野。


內文圖片來源:李健榮 Simen

封面圖片來源:pexels