上一篇文章讲了如何使用 pngquant

这次我们来试试使用 node.js 封装一下 pngquant,实现一个自动化批量压缩图片的脚本

准备

有关如何使用 node.js 开发一个命令行工具,可以参考我之前写的一篇文章

首先我们先初始化一个 nodejs 项目

1
2
3
mkdir node-pngquant
cd node-pngquant
npm init -y

接着创建一个 bin 目录,并在该目录下新建一个 js 脚本

1
2
3
mkdir bin
cd bin
touch pngquant.js

然后配置 package.json 文件,文件内容类似于这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "pngquant",
"version": "1.0.0",
"main": "index.js",
"type": "module", //
"bin": {
"node-pngquant": "bin/pngquant.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

接着回到项目根目录,安装 commander 依赖

1
2
cd .. # 返回根目录
npm install commander

js 代码编写

然后我们开始编写 js 脚本

首先导入需要的包

1
2
3
4
5
6
7
#!/usr/bin/env node

import { program } from "commander"; // 用户配置命令
import { exec } from "node:child_process"; // 用于执行shell脚本
import path from "node:path"; // 用于处理路径
import fs from "node:fs"; // 用于操作文件
import { fileURLToPath } from "node:url"; // 用于获取路径

接着使用 commander 构建版本命令和一些参数

有关 commander 的使用,可以参考官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 获取package.json文件中的版本号
const packageJsonPath = new URL("../package.json", import.meta.url);
const packageJson = JSON.parse(
fs.readFileSync(fileURLToPath(packageJsonPath), "utf-8")
);

// 定义一个版本获取的命令并添加描述
program
.version(packageJson.version, "-v, --version", "输出当前工具版本号")
.description(
`一个使用node.js封装的pngquant命令行工具,用于批量压缩处理图片,开发者为✨星凪✨
使用格式为
node-pngquant [options] [dir] [output] [quality]
例如
node-pngquant -d ./imgs -o ./output -q 85
可以简写为
node-pngquant ./imgs ./output 85
`
);

// 定义帮助命令
program.helpOption("-h, --help", "输出帮助信息");

// 定义压缩目录参数
program.option("-d, --dir <dir>", "指定要压缩的图片目录");

// 定义压缩质量的参数
program.option("-q, --quality <quality>", "指定压缩质量,范围0-100,默认85");

// 定义输出目录的参数
program.option("-o, --output <output>", "指定输出目录");

接着我们需要定义一个默认的压缩参数

process.cwd()用于获取当前的工作目录

1
2
3
4
5
6
// process.cwd()用于获取当前的工作目录
const params = {
dir: path.resolve(process.cwd()),
quality: 85,
output: path.resolve(process.cwd()),
};

接下载在写一个工具函数,用于判断文件是否是一个目录

使用 Promise 的好处在于避免了回调函数的使用,可以让代码更加简洁

1
2
3
4
5
6
7
8
9
10
11
const checkIsDirectory = (dir) => {
return new Promise((resolve, reject) => {
fs.stat(dir, (err, stats) => {
if (err) {
reject(err);
} else {
resolve(stats.isDirectory());
}
});
});
};

接着我们开始编写压缩单个文件的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 接受一个文件名
const compressPng = (fileName, params) => {
return new Promise((resolve, reject) => {
// 执行pngqaunt脚本命令,脚本命令是使用模板字符串进行拼接的
exec(
`pngquant ${path.join(params.dir, fileName)} --quality=${
params.quality
} --output=${path.join(params.output, "compress", fileName)}`,
(err, _stdout, _stderr) => {
if (err) {
// 压缩失败
reject(err);
} else {
// 压缩成功,输出压缩的文件名
console.log(fileName);
resolve();
}
}
);
});
};

最后一步,获取用户输入的参数并执行命令

options 为输入的参数,例如 node-pngquant -d ./imgs,会获取到 ./imgs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 执行命令,options为用户通过[-options]输入的参数,例如node-pngquant -d ./imgs,会获取到 ./imgs
program.action(async (options) => {
try {
// 如果输入是标准模式
if (options.dir) {
params.dir = path.resolve(options.dir);
}
if (options.quality) {
params.quality = options.quality;
}
if (options.output) {
params.output = path.resolve(options.output);
}
// 如果用户的输入参数是简写模式,需要单独处理,例如 node-pngquant ./imgs ./output 85
if (Object.keys(options).length === 0) {
if (program.args[0]) {
params.dir = path.resolve(program.args[0]);
}
if (program.args[1]) {
params.output = path.resolve(program.args[1]);
}
if (program.args[2]) {
params.quality = program.args[2];
}
}
// 先判断是不是目录
const isDirectory = await checkIsDirectory(params.dir);
if (!isDirectory) {
console.error("输入路径不是一个目录");
return;
}
// 获取目录下的所有文件
const files = fs.readdirSync(params.dir, {
withFileTypes: true,
encoding: "utf-8",
});
// 判断是否存在compress目录,不存在则创建作为输出目录
if (!fs.existsSync(path.join(params.output, "compress"))) {
fs.mkdirSync(path.join(params.output, "compress"));
}
// 遍历目录下的所有文件
for (const file of files) {
// 判断文件是否是文件,是否以.png结尾
if (!file.isFile() || !file.name.endsWith(".png")) {
continue;
}
// 如果文件是png文件则执行压缩函数
await compressPng(file.name, params);
}
} catch (error) {
console.error("发生了未知错误\n", error);
}
});

// 获取命令行的参数
program.parse(process.argv);

效果预览

输入

1
node-pngquant -d ./imgs -o ./ -q 85

输出信息

1
2
3
4
a-xingguang2x.png
sticker-fs8.png
sticker.png
test.png

总代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#!/usr/bin/env node

import { program } from "commander";
import { exec } from "node:child_process";
import path from "node:path";
import fs from "node:fs";
import { fileURLToPath } from "node:url";

// 获取package.json文件中的版本号
const packageJsonPath = new URL("../package.json", import.meta.url);
const packageJson = JSON.parse(
fs.readFileSync(fileURLToPath(packageJsonPath), "utf-8")
);

// 定义一个版本获取的命令并添加描述
program
.version(packageJson.version, "-v, --version", "输出当前工具版本号")
.description(
`一个使用node.js封装的pngquant命令行工具,用于批量压缩处理图片,开发者为✨星凪✨
使用格式为
node-pngquant [options] [dir] [output] [quality]
例如
node-pngquant -d ./imgs -o ./output -q 85
可以简写为
node-pngquant ./imgs ./output 85
`
);

// 定义帮助命令
program.helpOption("-h, --help", "输出帮助信息");

// 定义压缩目录参数
program.option("-d, --dir <dir>", "指定要压缩的图片目录");

// 定义压缩质量的参数
program.option("-q, --quality <quality>", "指定压缩质量,范围0-100,默认85");

// 定义输出目录的参数
program.option("-o, --output <output>", "指定输出目录");

// process.cwd()用于获取当前的工作目录
const params = {
dir: path.resolve(process.cwd()),
quality: 85,
output: path.resolve(process.cwd()),
};

/**
* 判断路径是否是一个目录
* @param {*} dir 路径
* @returns
*/
const checkIsDirectory = (dir) => {
return new Promise((resolve, reject) => {
fs.stat(dir, (err, stats) => {
if (err) {
reject(err);
} else {
resolve(stats.isDirectory());
}
});
});
};

/**
* 压缩单个文件
* @param {*} fileName 文件名
* @param {*} params 压缩参数
* @returns
*/
// 接受一个文件名
const compressPng = (fileName, params) => {
return new Promise((resolve, reject) => {
// 执行pngqaunt脚本命令,脚本命令是使用模板字符串进行拼接的
exec(
`pngquant ${path.join(params.dir, fileName)} --quality=${
params.quality
} --output=${path.join(params.output, "compress", fileName)}`,
(err, _stdout, _stderr) => {
if (err) {
// 压缩失败
reject(err);
} else {
// 压缩成功,输出压缩的文件名
console.log(fileName);
resolve();
}
}
);
});
};

// 执行命令,options为用户通过[-options]输入的参数,例如node-pngquant -d ./imgs,会获取到 ./imgs
program.action(async (options) => {
try {
// 如果输入是标准模式
if (options.dir) {
params.dir = path.resolve(options.dir);
}
if (options.quality) {
params.quality = options.quality;
}
if (options.output) {
params.output = path.resolve(options.output);
}
// 如果用户的输入参数是简写模式,需要单独处理,例如 node-pngquant ./imgs ./output 85
if (Object.keys(options).length === 0) {
if (program.args[0]) {
params.dir = path.resolve(program.args[0]);
}
if (program.args[1]) {
params.output = path.resolve(program.args[1]);
}
if (program.args[2]) {
params.quality = program.args[2];
}
}
// 先判断是不是目录
const isDirectory = await checkIsDirectory(params.dir);
if (!isDirectory) {
console.error("输入路径不是一个目录");
return;
}
// 获取目录下的所有文件
const files = fs.readdirSync(params.dir, {
withFileTypes: true,
encoding: "utf-8",
});
// 判断是否存在compress目录,不存在则创建作为输出目录
if (!fs.existsSync(path.join(params.output, "compress"))) {
fs.mkdirSync(path.join(params.output, "compress"));
}
// 遍历目录下的所有文件
for (const file of files) {
// 判断文件是否是文件,是否以.png结尾
if (!file.isFile() || !file.name.endsWith(".png")) {
continue;
}
// 如果文件是png文件则执行压缩函数
await compressPng(file.name, params);
}
} catch (error) {
console.error("发生了未知错误\n", error);
}
});

// 获取命令行的参数
program.parse(process.argv);

总结

我将这个包发布到了 npm,有兴趣的话可以使用 npm install node-pngquant -g 全局安装并使用。

不想使用了可以使用 npm uninstall node-pngquant -g 来删除

其实很多命令行工具都可以使用 node.js 进行封装并使用,达成更好的效果