【Node.js】青梅煮酒,聊聊zlib壓縮

語言: CN / TW / HK

theme: awesome-green highlight: atelier-dune-light


「本文正在參與技術專題徵文Node.js進階之路,點選檢視詳情

前言

完成對Node.js的從瞭解到熟練的進階這個Flag設立已久,久到去年就有它了。驚蟄已過,風暖雲開,隔年的Flag是時候拿出來實現了。出去踏青or在家碼字,我決定選擇後者。

至少對Node.js的探索,今年能有一個完美的歎號。

目標明確

這個明媚的春日,特別適合窗前看書。光影斑駁,再來杯白開水。最近看zlib壓縮的API,發現無論從理解還是使用上都比較陌生,所以挑了一些看著感興趣的API進行進一步的摸索。

隨波逐流無歸處,乘風破浪濟滄海

zlib 壓縮

瞧一瞧,一個壓縮/解壓功能包含了多少知識點?

檔案壓縮和解壓的實現

```js let zlib = require('zlib'); const { createReadStream, createWriteStream } = require('fs'); const { pipeline } = require('stream');

/* * 壓縮過程中錯誤捕獲方法 / const onError = err => { if (err) { console.error('An error occurred:', err); process.exitCode = 1; } };

/* * 壓縮或者解壓方法 type值為zip執行壓縮方法,type值為ungzip執行解壓縮方法 / function zipFunc(source, destination, type) { const gzip = zlib.createGzip(); const ungzip = zlib.createGunzip(); switch (type) { case 'zip': return pipeline(source, gzip, destination, onError); case 'ungzip': // 或者用pipeline方法,return pipeline(source, ungzip, destination, onError); return source.pipe(ungzip).pipe(destination); default: return pipeline(source, gzip, destination, onError); } } // 壓縮 const source = createReadStream('./zlib/input.txt'); const destination = createWriteStream('./zlib/input.txt.gz'); zipFunc(source, destination, 'zip');

// 解壓 const source = createReadStream('./zlib/input.txt.gz'); const destination = createWriteStream('./zlib/input.txt'); zipFunc(source, destination, 'ungzip'); ```

  • 執行壓縮操作時,zlib目錄下生成input.txt.gz檔案;
  • 執行解壓操作時,zlib目錄下生成input.txt檔案;

zlib-01.png

pipeline

stream.pipeline()方法,用於在流和生成器之間進行管道轉發錯誤並正確清理並在管道完成時提供回撥。

難以理解的功能介紹?

上面這段功能介紹,我看了好多遍, 並沒有理解,尤其是對於管道的概念比較模糊。於是搜了一下stream的文章,發現了一篇好文《Node.js 中的一股清流:理解 Stream(流)的基本概念》,寫的很詳細易懂,它裡有這樣一段話:

管道是一種機制,是將一個流的輸出作為另一流的輸入。它通常用於從一個流中獲取資料並將該流的輸出傳遞到另外的流。管道操作沒有限制,換句話說,管道用於分步驟處理流資料。

所以在進行檔案壓縮的時候使用stream.pipeline()提供一個完成資料流處理的管道,管道內可以傳輸多個流,管道任務結束後提供回撥。

用法

js stream.pipeline(source[, ...transforms], destination, callback)

屬性

source:可讀流

...tranforms:雙工流(同時實現 Readable 和 Writable 介面的流)

destination:可寫流

callback:管道完成時的回撥

pipe

readable.pipe() 方法將可寫流繫結到可讀流,使其自動切換到流動模式並將其所有資料推送到繫結的可寫流。 將這句話總結一下,pipe方法的主要用途是從可讀流中讀取資料寫入可寫流。

用法

js readable.pipe(destination[, options])

示例

可以看官方的示例,簡單易懂,將 readable 中的所有資料通過管道傳輸到名為 file.txt 的檔案中:

js const fs = require('fs'); const readable = getReadableStreamSomehow(); const writable = fs.createWriteStream('file.txt'); // 可讀流的所有資料進入 'file.txt'。 readable.pipe(writable);

也可以將多個 Writable 流繫結到單個 Readable 流。

readable.pipe() 方法返回對目標流的引用,從而可以建立管道流鏈

js 也可以將多個 Writable 流繫結到單個 Readable 流。 readable.pipe() 方法返回對目標流的引用,從而可以建立管道流鏈

js const fs = require('fs'); const r = fs.createReadStream('file.txt'); const z = zlib.createGzip(); const w = fs.createWriteStream('file.txt.gz'); r.pipe(z).pipe(w);

stream 流

什麼是流?

看下官網的介紹。

流是用於在 Node.js 中處理流資料的抽象介面。 stream 模組提供了用於實現流介面的 API。

流可以是可讀的、可寫的、或兩者兼而有之。 所有的流都是 EventEmitter 的例項。

我看完,好像懂了又好像沒懂。但是我找到了一篇講的非常好的文章,《一文搞定 Node.js 流 (Stream)》

這篇文章裡面對流的介紹,我感覺懂了一些

stream(流)是一種抽象的資料結構。就像陣列或字串一樣,流是資料的集合。

不同的是,流可以每次輸出少量資料,而且它不用存在記憶體中。

比如,對伺服器發起 http 請求的 request/response 物件就是 Stream。

總結一下,使用流可以將檔案資源拆分成小塊進行處理,減輕伺服器壓力。

明白了流的作用,就知道為什麼檔案壓縮要使用Stream提供的模組方法了。如果想對Stream進行更深入的瞭解,推薦閱讀《一文搞定 Node.js 流 (Stream)》,寫的詳情且通俗易懂。

壓縮 HTTP 的請求和響應

gzip、deflate 和 br

  • gzip是一種資料格式,預設且目前僅使用deflate演算法壓縮data部分;
  • deflate是同時使用了LZ77演算法與哈夫曼編碼(Huffman Coding)的一個無損資料壓縮演算法。
  • Brotli 通過變種的 LZ77 演算法、Huffman 編碼以及二階文字建模等方式進行資料壓縮,與其他壓縮演算法相比,它有著更高的壓縮效率。

官網示例的本地實驗

我再官網給出的示例的基礎上,將http的響應內容生成不同的檔案,可以看出壓縮過和未經過壓縮的檔案的檔案大小是有區別的。

示例程式碼

```js // 客戶端請求示例 const zlib = require('zlib'); const http = require('http'); const fs = require('fs'); const { pipeline } = require('stream');

const request = http.get({ host: 'example.com', path: '/', port: 80, headers: { 'Accept-Encoding': 'br,gzip,deflate' }, }); request.on('response', response => { console.log(response.headers['content-encoding'], 'headers'); const output = fs.createWriteStream('./zlib/example.com_index_default.html'); // const output = fs.createWriteStream('./zlib/example.com_index_gzip.html'); // const output = fs.createWriteStream('./zlib/example.com_index_deflate.html');

const onError = err => { if (err) { console.error('An error occurred:', err); process.exitCode = 1; } };

switch (response.headers['content-encoding']) { case 'br': pipeline(response, zlib.createBrotliDecompress(), output, onError); break; case 'gzip': pipeline(response, zlib.createGzip(), output, onError); break; case 'deflate': pipeline(response, zlib.createGzip(), output, onError); break; default: pipeline(response, output, onError); break; } }); ``` - 未經過壓縮的檔案大小是1.2k; - 壓縮過的檔案大小是600多B;

zlib-02.png

小結

對http請求和響應的壓縮,我還有待在實際應用場景中研究和實踐,單純實現官網的例子,我感覺自己沒有完全掌握。

總結

在過去一個月的學習中,雖然都是碎片化的學習,但是隨著技術的積累,形成一套屬於自己的學習體系,可以幫助更快更好的掌握新技術。

接下來,就是實踐的階段了,雖然工作中沒有使用Node.js開發的場景,但是自己可以創造專案,正好我有一個現成的小程式,可以開發一套文章管理後臺系統。