【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开发的场景,但是自己可以创造项目,正好我有一个现成的小程序,可以开发一套文章管理后台系统。