Nodejs教程24:Stream流
我們通常會使用File System模組對檔案進行讀取,如下:
fs.readFile('./test.txt', (error, buffer) => { if (error) { console.error(error) } else { // 讀取檔案成功 res.write(buffer) } }) 複製程式碼
這樣操作簡單有效,但這也存在一些問題:
- 佔用記憶體 使用fs讀取檔案,它是一次性將檔案的所有內容讀取到記憶體中,再一次性發送到客戶端,因此會佔用大量記憶體。
- 資源使用效率低 從磁碟讀取檔案期間,磁碟處於忙碌狀態,而網路處於空閒狀態。 磁碟讀取完成後,開始傳送檔案時,情況正相反,網路處於忙碌狀態,此時磁碟卻處於空閒狀態。
Stream流
相比File System,Stream流讀取檔案是讀一份,發一份,Stream流的寫入操作也有同樣特點,因此可以解決File System在上面提到的2個問題。
接下來實現一個簡單的流,將1.txt檔案的內容寫入到2.txt中:
示例程式碼:/lesson24/stream.js
const fs = require('fs') // 建立一個可讀流。 const readStream = fs.createReadStream('./1.txt') // 建立一個可寫流。 const writeStream = fs.createWriteStream('./2.txt') // 將可讀流讀取的資料,通過管道pipe推送到寫入流中,即可將1.txt的內容,寫入到2.txt中。 readStream.pipe(writeStream) // 讀取出現錯誤時會觸發error事件。 readStream.on('error', (error) => { console.error(error) }) // 寫入完成時,觸發finish事件。 writeStream.on('finish', () => { console.log('finish') }) 複製程式碼
使用Zlib壓縮檔案
可以使用Zlib模組,配合Stream流,實現檔案壓縮功能,如下:
示例程式碼:/lesson24/gzip.js
const fs = require('fs') // 引入zlib模組,用於實現壓縮功能 const zlib = require('zlib') // 建立一個可讀流。 const readStream = fs.createReadStream('./google.jpg') // 建立一個可寫流。 const writeStream = fs.createWriteStream('./google.jpg.gz') // 建立一個Gzip物件,用於將檔案壓縮成.gz檔案 const gzip = zlib.createGzip() // 將可讀流讀取的資料,先通過管道pipe推送到gzip中,再推送到寫入流中。 // 也就是先將可讀流的資料壓縮,再推送到可寫流中。 readStream.pipe(gzip).pipe(writeStream) // 讀取出現錯誤時會觸發error事件。 readStream.on('error', (error) => { console.error(error) }) // 寫入完成時,觸發finish事件。 writeStream.on('finish', () => { console.log('finish') }) 複製程式碼
使用流傳輸檔案到前臺
學習了流,我們就可以更加高效地將檔案傳輸到前臺:
示例程式碼:/lesson24/server.js
const http = require('http') const zlib = require('zlib') const url = require('url') const fs = require('fs') const server = http.createServer((req, res) => { const { pathname } = url.parse(req.url, true) // 建立一個可讀流。 const readStream = fs.createReadStream(`./${pathname}`) // 建立一個Gzip物件,用於將檔案壓縮成.gz檔案 const gzip = zlib.createGzip() // 將讀取的內容,在通過管道推送到res中,該方法不經過壓縮 readStream.pipe(res) // 處理可讀流報錯,防止請求不存在的檔案 readStream.on('error', (error) => { console.error(error); res.writeHead(404) res.write('Not Found') res.end() }) }) server.listen(8080) 複製程式碼
但可以看到,在這個例子裡,雖然實現了使用流傳輸檔案,但並沒有用到gzip壓縮,在傳輸時還是更多地消耗網路資源,接下來可以引入gzip壓縮。
檔案經過gzip壓縮後傳輸到前臺
但此時如果只是簡單的用readStream.pipe(gzip).pipe(res)
傳輸檔案,瀏覽器在訪問時無法直接開啟,而是會觸發檔案下載。
這是因為未設定請求頭屬性content-encoding的值,導致瀏覽器無法識別用gzip壓縮過的檔案,這就需要修改請求頭res.setHeader('content-encoding', 'gzip')
,讓瀏覽器可以識別。
這樣瀏覽器就可以正常開啟檔案了,但若瀏覽器訪問的是不存在的檔案,瀏覽器會報錯“無法訪問此網站”,這是因為請求頭屬性content-encoding已被設定為gzip,但服務端傳給瀏覽器的是Not Found字串,瀏覽器無法識別。
此時可以使用fs.stat方法,先檢查檔案是否存在,若不存在則返回Not Found,若存在則繼續傳輸。
示例程式碼:/lesson24/server_gzip.js
const http = require('http') const zlib = require('zlib') const url = require('url') const fs = require('fs') const server = http.createServer((req, res) => { const { pathname } = url.parse(req.url, true) // 檔案的相對路徑 const filepath = `./${pathname}` // 檢查檔案是否存在 fs.stat(filepath, (error, stat) => { if (error) { console.error(error); res.setHeader('content-encoding', 'identity') res.writeHead(404) res.write('Not Found') res.end() } else { // 建立一個可讀流。 const readStream = fs.createReadStream(filepath) // 建立一個Gzip物件,用於將檔案壓縮成.gz檔案 const gzip = zlib.createGzip() // 向瀏覽器傳送經過gzip壓縮的檔案,設定響應頭,否則瀏覽器無法識別,會自動進行下載。 res.setHeader('content-encoding', 'gzip') // 將讀取的內容,通過gzip壓縮之後,在通過管道推送到res中,由於res繼承自Stream流,因此也可以接收管道的推送。 readStream.pipe(gzip).pipe(res) // 處理可讀流報錯,防止檔案中途被刪除或出錯,導致報錯。 readStream.on('error', (error) => { console.error(error); res.setHeader('content-encoding', 'identity') res.writeHead(404) res.write('Not Found') res.end() }) } }) }) server.listen(8080) 複製程式碼