基于Node.js实现压缩和解压缩的方法


Posted in Javascript onFebruary 13, 2018

压缩格式

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 是负责对文件夹进行打包:package:的。

例如要对 nodejs-compressing-dem o 整个文件夹打包成一个文件发送给别人,可以通过 tar 命令完成。

$ tar -c -f nodejs-compressing-demo.tar nodejs-compressing-demo/

$ ls -l nodejs-compressing-demo.tar
-rw-r--r-- 1 a a 206336 Feb 12 14:01 nodejs-compressing-demo.tar

如大家所见,tar 打包出来的文件一般都比较大,因为它是未压缩的,大小跟实际文件夹总大小接近。所以我们都会在打包同时进行压缩。

$ tar -c -z -f nodejs-compressing-demo.tgz nodejs-compressing-demo/

$ ls -l nodejs-compressing-demo.tgz
-rw-r--r-- 1 a a 39808 Feb 12 14:07 nodejs-compressing-demo.tgz

tar 和 tgz 超过 5 倍大小的差异,可以大大减少网络传输带宽。

Node.js 实现 tgz

Promise 接口

先使用 compressing.tar.compressDir(sourceDir, targetFile) 将一个文件夹打包到一个 tar 文件,然后使用上文的 gzip 压缩方式,将 tar 文件压缩为 tgz 文件。

const compressing = require('compressing');

compressing.tar.compressDir('nodejs-compressing-demo', 'nodejs-compressing-demo.tar')
 .then(() => {
  return compressing.gzip.compressFile('nodejs-compressing-demo.tar',
   'nodejs-compressing-demo.tgz');
 });
 .then(() => {
  console.log('success');
 })
 .catch(err => {
  console.error(err);
 });

// 解压缩
compressing.gzip.uncompress('nodejs-compressing-demo.tgz', 'nodejs-compressing-demo.tar')
 .then(() => {
  return compressing.tar.uncompress('nodejs-compressing-demo.tar',
   'nodejs-compressing-demo2');
 });
 .then(() => {
  console.log('success');
 })
 .catch(err => {
  console.error(err);
 });

结合 async/await 的编程模型,代码写起来会更加容易阅读:

const compressing = require('compressing');

async function main() {
 try {
  await compressing.tar.compressDir('nodejs-compressing-demo',
   'nodejs-compressing-demo.tar');
  await compressing.gzip.compressFile('nodejs-compressing-demo.tar',
   'nodejs-compressing-demo.tgz');
  console.log('success');
 } catch (err) {
  console.error(err);
 }
 
 // 解压缩
 try {
  await compressing.gzip.uncompress('nodejs-compressing-demo.tgz',
   'nodejs-compressing-demo.tar');
  await compressing.tar.uncompress('nodejs-compressing-demo.tar',
   'nodejs-compressing-demo2');
  console.log('success');
 } catch (err) {
  console.error(err);
 }
}

main();

Stream 接口

通过 compressing.tar.Stream 类,可以动态添加任意文件、文件夹到一个 tar stream 对象中,非常灵活。

const tarStream = new compressing.tar.Stream();
// dir
tarStream.addEntry('dir/path/to/compress');
// file
tarStream.addEntry('file/path/to/compress');
// buffer
tarStream.addEntry(buffer);
// stream
tarStream.addEntry(stream);

const destStream = fs.createWriteStream('path/to/destination.tgz');
pump(tarStream, new compressing.gzip.FileStream(), destStream, err => {
 if (err) {
  console.error(err);
 } else {
  console.log('success');
 }
});

zip

zip 其实可以看作是 tar + gzip 的「商业化」结合,它让使用者不需要区分是压缩文件还是压缩文件夹,反正用我 zip 就对了。

使用 zip 命令行工具压缩一个文件夹的例子:

$ zip -r nodejs-compressing-demo.zip nodejs-compressing-demo/
 adding: nodejs-compressing-demo/ (stored 0%)
 adding: nodejs-compressing-demo/test/ (stored 0%)
 ...
 adding: nodejs-compressing-demo/.travis.yml (deflated 36%)

$ ls -l nodejs-compressing-demo.*
-rw-r--r-- 1 a a 206336 Feb 12 14:06 nodejs-compressing-demo.tar
-rw-r--r-- 1 a a  39808 Feb 12 14:07 nodejs-compressing-demo.tgz
-rw-r--r-- 1 a a  55484 Feb 12 14:34 nodejs-compressing-demo.zip

通过 tgz 和 zip 文件大小对比,可以看出默认的压缩参数下,gzip 的效果会比 zip 好。

Node.js 实现 zip

实现代码跟 tar 类似,只不过默认是压缩的,不需要再添加 gzip 的过程。

const compressing = require('compressing');

compressing.zip.compressDir('nodejs-compressing-demo', 'nodejs-compressing-demo.zip')
 .then(() => {
  console.log('success');
 })
 .catch(err => {
  console.error(err);
 });

// 解压缩
compressing.zip.uncompress('nodejs-compressing-demo.zip', 'nodejs-compressing-demo3')
 .then(() => {
  console.log('success');
 })
 .catch(err => {
  console.error(err);
 });

总结

基于 Node.js 实现的压缩和解压缩是否比想象中简单?感谢 npm 这个巨人,让我们编程也能拥有命令行工具那样简单的体验。

无论是 Promise 接口,还是 Stream 接口,都有它最合适的场景,你会选择了吗?

到此,你拥有的压缩和解压缩能力,你能够做什么样的服务和功能呢?

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
js 变量类型转换常用函数与代码[比较全]
Dec 01 Javascript
Jquery 插件开发笔记整理
Jan 17 Javascript
JS实现双击屏幕滚动效果代码
Oct 28 Javascript
Javascript动画效果(1)
Oct 11 Javascript
前端开发必知的15个jQuery小技巧
Jan 22 Javascript
Javascript之深入浅出prototype
Feb 06 Javascript
Android中Okhttp3实现上传多张图片同时传递参数
Feb 18 Javascript
从零开始学习Node.js系列教程二:文本提交与显示方法
Apr 13 Javascript
详解axios在vue中的简单配置与使用
May 10 Javascript
vue使用axios时关于this的指向问题详解
Dec 22 Javascript
详解JavaScript中typeof与instanceof用法
Oct 24 Javascript
vue中data里面的数据相互使用方式
Jun 05 Vue.js
Vue打包后出现一些map文件的解决方法
Feb 13 #Javascript
nginx部署访问vue-cli搭建的项目的方法
Feb 12 #Javascript
vue2.0实现前端星星评分功能组件实例代码
Feb 12 #Javascript
nginx+vue.js实现前后端分离的示例代码
Feb 12 #Javascript
Vue+webpack项目基础配置教程
Feb 12 #Javascript
详解vue-admin和后端(flask)分离结合的例子
Feb 12 #Javascript
jquery+css3实现熊猫tv导航代码分享
Feb 12 #jQuery
You might like
追求程序速度,而不是编程的速度
2008/04/23 PHP
php开启openssl的方法
2014/05/15 PHP
PHP中SESSION的注销与清除
2015/04/16 PHP
求帮忙修改个php curl模拟post请求内容后并下载文件的解决思路
2015/09/20 PHP
php array_walk 对数组中的每个元素应用用户自定义函数详解
2016/11/18 PHP
PHP面向对象之领域模型+数据映射器实例(分析)
2017/06/21 PHP
如何做到打开一个页面,过几分钟自动转到另一页面
2007/04/20 Javascript
AJAX分页的代码(后台asp.net)
2011/02/14 Javascript
JQuery跳出each循环的方法
2015/04/16 Javascript
jQuery语法小结(超实用)
2015/12/31 Javascript
浅谈时钟的生成(js手写简洁代码)
2016/08/20 Javascript
微信小程序 location API实例详解
2016/10/02 Javascript
深入理解JS实现快速排序和去重
2016/10/17 Javascript
vue操作下拉选择器获取选择的数据的id方法
2018/08/24 Javascript
vue-cli3 karma单元测试的实现
2019/01/18 Javascript
详解如何使用webpack打包多页jquery项目
2019/02/01 jQuery
如何检查一个对象是否为空
2019/04/11 Javascript
js遍历详解(forEach, map, for, for...in, for...of)
2019/08/28 Javascript
JavaScript 正则应用详解【模式、欲查、反向引用等】
2020/05/13 Javascript
JS addEventListener()和attachEvent()方法实现注册事件
2021/01/11 Javascript
[01:06:18]DOTA2-DPC中国联赛 正赛 Phoenix vs Dynasty BO3 第二场 1月26日
2021/03/11 DOTA
深入浅析ImageMagick命令执行漏洞
2016/10/11 Python
浅谈python socket函数中,send与sendall的区别与使用方法
2017/05/09 Python
Python进程间通信Queue消息队列用法分析
2019/05/22 Python
python实现大战外星人小游戏实例代码
2019/12/26 Python
pytorch ImageFolder的覆写实例
2020/02/20 Python
Python post请求实现代码实例
2020/02/28 Python
appium+python adb常用命令分享
2020/03/06 Python
浅谈Python3多线程之间的执行顺序问题
2020/05/02 Python
.img/.hdr格式转.nii格式的操作
2020/07/01 Python
NFL欧洲商店(德国):NFL Europe Shop DE
2018/11/03 全球购物
大学生职业生涯规划书模版
2013/12/30 职场文书
英文推荐信格式范文
2014/05/09 职场文书
应聘教师自荐书
2014/06/16 职场文书
总经理岗位职责
2015/02/04 职场文书
2015年大学组织委员个人工作总结
2015/10/23 职场文书