fs.readFile 去讀取一個 2GB 的檔案,Node.js 會試圖把這 2GB 的資料全部塞進伺服器的記憶體 (RAM) 裡。如果同時有十個使用者請求這個檔案,你的伺服器記憶體會瞬間爆滿,直接當機(Out of Memory)。要搞懂這兩個名詞,請想像你要把一個巨大的水塔(大檔案)裡的水,轉移到另一個空水塔(使用者端)。
fs.readFile):準備一個和水塔一樣大的超級大水桶,把水全部裝進去,再一次倒過去。這個超級大水桶,就是伺服器的記憶體。我們將結合昨天的 HTTP 模組,寫一個能夠「串流」傳輸檔案的伺服器。就算檔案有幾 GB,這支程式的記憶體佔用也能穩定維持在幾十 MB 以內。
在 hello-node 資料夾下,新增一個 stream-demo.js。我們順便教你怎麼用傳統方式寫入大量資料來製造一個大檔案(請注意這一步會稍等幾秒鐘):
import fs from 'fs';
console.log(" 正在製造假的大檔案,請稍候...");
const file = fs.createWriteStream('./big-data.txt');
// 寫入 10 萬行文字,製造出一個稍微大一點的檔案
for (let i = 0; i < 100000; i++) {
file.write(`這是第 ${i} 行:Node.js Stream 降維打擊測試資料!\n`);
}
file.end();
console.log(" 大檔案製造完成!");
在終端機執行 node stream-demo.js,你的資料夾會多出一個 big-data.txt。
現在,清空剛才的 stream-demo.js(或直接修改它),換成以下結合 HTTP 與 Stream 的程式碼:
import http from 'http';
import fs from 'fs';
const server = http.createServer((req, res) => {
if (req.url === '/') {
// 設定標頭,告訴瀏覽器這是一份純文字檔案
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
// 1. 建立「讀取流 (水管的進水口)」
const readStream = fs.createReadStream('./big-data.txt', 'utf-8');
// 2. 將讀取流透過 pipe (管線) 直接對接到 res (回應流 / 水管的出水口)
readStream.pipe(res);
// 監聽進階事件 (可選):當資料流結束時,印出提示
readStream.on('end', () => {
console.log(" 串流傳輸完畢!");
});
// 錯誤處理:檔案不存在時的防禦
readStream.on('error', (err) => {
res.writeHead(500);
res.end("伺服器發生錯誤");
console.error("讀取失敗:", err.message);
});
} else {
res.writeHead(404);
res.end("Not Found");
}
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(` Stream 伺服器啟動!請前往:http://localhost:${PORT}`);
});
node stream-demo.js。http://localhost:3000。
雖然這個範例的檔案不到 10MB,但即使你把剛才的回圈加到 1000 萬行(變成幾 GB 的檔案),這段使用了 readStream.pipe(res) 的程式碼也絕對不會因為記憶體不足而崩潰。
水桶與水管的比喻把 Buffer、Stream 和記憶體差異講得很直觀,也順帶帶到 pipe 的背壓處理。範例先送出 200 後,如果串流途中才 error,這時 500 標頭可能已來不及改;你下一篇會示範 pipeline() 或 headersSent 的錯誤處理嗎?