閱讀更多系列文章請訪問我的GitHub部落格,示例程式碼請訪問這裡。
File System的問題
我們通常會使用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)
複製程式碼