基于 Node.js 实现压缩和解压缩

压缩格式

zip 和 gzip 是两种我们最常见到的压缩格式,当然,gzip 在 Windows 下很少有人接触。tar 是一种归档格式,它默认不会压缩,需要结合 gzip 来将最终的 tar 文件以 gzip 格式压缩成为一个 tar.gz 文件,通常我们会缩写为 tgz。

为什么没有提到 rar?因为它是专利保护的算法,你可以免费获得解压工具,而压缩工具是需要付费的。所以我们一般应用场景下,很少会提供 rar 压缩文件。

本文将分别介绍 gzip,tar,tgz 和 zip 的压缩和解压缩在 Node.js 下如何实现。

未压缩文件库

本文所使用的未压缩文件库来自于 urllib ,需要先 clone 它下来到指定目录。

git clone https://github.com/node-modules/urllib.git nodejs-compressing-demo

gzip

在 Linux 的世界,每个工具的职责会很纯粹,非常单一,如 gzip,它只会对文件进行压缩,至于文件夹如何打包压缩,跟它没关系,那是 tar 要去负责的事情。

gzip 命令行压缩一个文件

例如我们要将 nodejs-compressing-demo/lib/urllib.js 文件进行 gzip 压缩,会得到一个 urllib.js.gz 文件,源文件会被删除。

$ ls -l nodejs-compressing-demo/lib/urllib.js 
-rw-r--r--  1 a  a  31318 Feb 12 11:27 nodejs-compressing-demo/lib/urllib.js

$ gzip nodejs-compressing-demo/lib/urllib.js

$ ls -l nodejs-compressing-demo/lib/urllib.js.gz 
-rw-r--r--  1 a  a  8909 Feb 12 11:27 nodejs-compressing-demo/lib/urllib.js.gz

# 还原压缩文件
$ gunzip nodejs-compressing-demo/lib/urllib.js.gz

文件大小从 31318 字节减少到 8909 字节,超过 3.5 倍的压缩效果。

还可以通过 pipe 方式,结合 cat 命令,将文件压缩并保存为任意文件:

$ ls -l nodejs-compressing-demo/README.md
-rw-r--r--  1 a  a  13747 Feb 12 11:27 nodejs-compressing-demo/README.md

$ cat nodejs-compressing-demo/README.md | gzip > README.md.gz

$ ls -l README.md.gz 
-rw-r--r--  1 a  a  4903 Feb 12 11:50 README.md.gz

Node.js 实现 gzip

当然,我们不会真的从零开始实现一个 gzip 算法和工具,在 Node.js 的世界,早已有人为你准备好这些基础库,我们只需要开箱即用。本文将会使用 compressing 模块,实现所有压缩和解压缩代码。

为什么会选择 compressing?因为它有足够充分的代码质量和单元测试保证,处于活跃的维护状态,API 非常友好,而且还支持流式接口。

Promise 接口

const compressing = require('compressing');

// 选择 gzip 格式,然后调用 compressFile 方法
compressing.gzip.compressFile('nodejs-compressing-demo/lib/urllib.js', 'nodejs-compressing-demo/lib/urllib.js.gz')
  .then(() => {
    console.log('success');
  })
  .catch(err => {
    console.error(err);
  });

// 解压缩是反响过程,接口都统一为 uncompress
compressing.gzip.uncompress('nodejs-compressing-demo/lib/urllib.js.gz', 'nodejs-compressing-demo/lib/urllib.js2')
  .then(() => {
    console.log('success');
  })
  .catch(err => {
    console.error(err);
  });

结合 async/await 的编程模型,代码写起来就是一个普通的异步 io 操作。

const compressing = require('compressing');

async function main() {
  try {
    await compressing.gzip.compressFile('nodejs-compressing-demo/lib/urllib.js', 
      'nodejs-compressing-demo/lib/urllib.js.gz');
    console.log('success');
  } catch (err) {
    console.error(err);
  }

  // 解压缩
  try {
    await compressing.gzip.uncompress('nodejs-compressing-demo/lib/urllib.js.gz', 
      'nodejs-compressing-demo/lib/urllib.js2');
    console.log('success');
  } catch (err) {
    console.error(err);
  }
}

main();

Stream 接口

需要特别注意的是,使用 Stream 模式编程,需要处理每个 stream 的 error 事件,并且要手动销毁所有 stream

fs.createReadStream('nodejs-compressing-demo/lib/urllib.js')
  .on('error', handleError)
  .pipe(new compressing.gzip.FileStream()) // It's a transform stream
  .on('error', handleError)
  .pipe(fs.createWriteStream('nodejs-compressing-demo/lib/urllib.js.gz2'))
  .on('error', handleError);

// 解压缩,就是 pipe 的方向倒转过来
fs.createReadStream('nodejs-compressing-demo/lib/urllib.js.gz2')
  .on('error', handleError)
  .pipe(new compressing.gzip.UncompressStream()) // It's a transform stream
  .on('error', handleError)
  .pipe(fs.createWriteStream('nodejs-compressing-demo/lib/urllib.js3'))
  .on('error', handleError);

根据官方的 Backpressuring in Streams 推荐,我们应该使用 pump 模块来配合 Stream 模式编程,由 pump 来完成这些 Stream 的清理工作。

const pump = require('pump');

const source = fs.createReadStream('nodejs-compressing-demo/lib/urllib.js');
const target = fs.createWriteStream('nodejs-compressing-demo/lib/urllib.js.gz2');

pump(source, new compressing.gzip.FileStream(), target, err => {
  if (err) {
    console.error(err);
  } else {
    console.log('success');
  }
});

// 解压缩
pump(fs.createReadStream('nodejs-compressing-demo/lib/urllib.js.gz2'), 
    new compressing.gzip.FileStream(), 
    fs.createWriteStream('nodejs-compressing-demo/lib/urllib.js3'), 
    err => {
  if (err) {
    console.error(err);
  } else {
    console.log('success');
  }
});

Stream 接口的优势

Stream 接口看起来比 Promise 接口复杂多了,为何还会有这种应用场景呢?其实在 HTTP 服务领域,Stream 模型会有更大的优势,因为 HTTP 请求本身就是一个 Request Stream,如要将一个上传文件以 gzip 压缩返回,使用 Stream 接口不需要将上传文件保存到本地磁盘,而是直接消费这个文件流。

使用 egg 文件上传的示例代码,我们稍微改造一下,就能实现 gzip 压缩然后返回。

const pump = require('pump');

class UploadFormController extends Controller {
  // ... other codes

  async upload() {
    const stream = await this.ctx.getFileStream();
    // 直接将压缩流赋值给 ctx.body,实现边压缩边返回的流式响应
    this.ctx.body = pump(stream, new compressing.gzip.FileStream());
  }
}

tar | gzip > tgz

gzip 章节可以提前知道,tar 是负责对文件夹进行打包


云邦互联免费空间(免备案,无广告)


【1G免费全能空间,免备案,无广告】 
1G全能空间 + 100M数据库(Mysql 5.5 / SQL Server 2005) 
支持的脚本:ASP、PHP(5.2 - 7.0)、.NET(2.0 / 4.0) 
没有任何限制,详细功能请访问: 
https://www.yunzz.net/host/free.html(云邦互联) 
推广员:ftp208482f 

阅读更多

没有更多推荐了,返回首页