共计 3896 个字符,预计需要花费 10 分钟才能阅读完成。
团队开启了一个新项目,希望能在原来项目的工程化基础上再进一步,于是想到了图片自动压缩。
这里的图片自动压缩并不是在 webpack 构建阶段压缩,而是在 git commit
的时候进行。
pre-commit
pre-commit 是 git hook 众多钩子中的一个,在每次 git commit
前执行,可以是 shell 等任何可执行的脚本文件,通过返回 0 or 1 来表示 commit 是否通过。在 bash 中,非零返回值代表失败.
pre-commit.js
市面上有许多通过 node.js 来封装 pre-commit 的 npm 包,我使用了功能最精简的pre-commit.js
, 按 README 在 package.json 中简单配置后,可以看到 .git/hooks/pre-commit 脚本如下所示:
#!/bin/bash
./node_modules/pre-commit/hook
RESULT=$?
[$RESULT -ne 0] && exit 1
exit 0
pre-commit.js 重写了原来的示例钩子,在 git commit
时执行自己的 /hook
脚本.
hook bash
# 之前的代码主要确认 node 环境
# git commit --dry-run 的情况(测试性提交,执行动作不产生结果),不对提交作任何处理
if [[$* == *--dry-run*]]; then
if [[-z "$BINARY"]]; then # 但还是要检查下 NODE 是否存在,不存在返回非零值代表失败
exit 1
fi
else
# 用 NODE 执行 'pre-commit/index.js' 模块
"$BINARY" "$("$BINARY"-e"console.log(require.resolve('pre-commit'))")"
fi
最终pre-commit/index.js
将读取 package.json 中配置好的路径来执行指定的脚本。
pre-commit/index.js
// 执行自定义钩子脚本的核心部分
Hook.prototype.run = function runner() {
var hooked = this;
(function again(scripts) {
// 脚本数组为空,则返回 0,执行本次 commit .
if (!scripts.length) return hooked.exit(0);
// 否则弹出一个脚本来执行
var script = scripts.shift();
// 使用的是异步执行子进程方法 child_process.spawn.
spawn(hooked.npm, ['run', script, '--silent'], {
env: process.env,
cwd: hooked.root,
stdio: [0, 1, 2]
}).once('close', function closed(code) {
// 监听了执行结束的 close 事件,递归继续执行下一个脚本
if (code) return hooked.log(hooked.format(Hook.log.failure, script, code));
again(scripts);
});
})(hooked.config.run.slice(0));
};
从上述代码中可以看出,即使我们定义的钩子脚本有异步处理逻辑也是可以的,因为整个脚本都是新开了一个异步的子进程来执行的,通过监听 close 事件来确认异步逻辑执行完毕,最终 exit(0)通知成功,执行 commit。
TinyPNG
TinyPNG 是一个专门进行有损压缩 PNG 图像的网站,经过它特有的算法,能够降低图像 70% – 80% 的大小。TinyPNG 提供在线 API,每月 500 张免费压缩额度,可以通过 npm 包 tinify.js 调用。
const execSync = require('child_process').execSync;
const path = require('path');
const tinify = require('tinify');
tinify.key = 'tDvJPN2Fd4yjtHJDSwQQtQQZWTldVls7'; // tinypng 提供的用户 key
console.log('pre-commit hook start! \n');
let diff = getDiffFiles();
compressPics(diff);
function getDiffFiles(type) {
// pre-commit 钩子本身不传递参数
//https://git-scm.com/docs/githooks/1.7.4#_pre_commit
// 所以通过 git diff 命令拿到本次提交涉及的变动文件
let root = process.cwd();
let files = execSync('git diff --cached --name-status HEAD').toString().split('\n');
let result = [];
// add, delete, modified, renamed, copied
type = type || 'admrc';
let types = type.split('').map(t => {return t.toLowerCase();
});
files.forEach(file => {if (!file) {return;}
let temp = file.split(/[\n\t]/);
let status = temp[0].toLowerCase();
let filePath = root + '/' + temp[1];
let extName = path.extname(filePath).slice(1);
if (types.length && ~types.indexOf(status)) {
result.push({
status: status, // admrc 中的一个
path: filePath, // 绝对路径
subpath: temp[1], // 相对路径
extName: extName, // 扩展名
})
}
});
return result;
}
function compressPics(files) {let pngs = files.filter(file => file.extName === 'png' && ['a', 'm'].includes(file.status));
console.log(pngs);
pngs.forEach(file => {let source = tinify.fromFile(file.subpath);
source.toFile(file.subpath)
.then(res => {
++flag;
console.log(file.subpath + 'has been compressed !');
execSync('git add .', {encoding: 'utf8'});
})
.catch(err => {console.log(err);
process.exit(1);
})
})
}
该方法使用了带 key 的在线接口,而且大大延长 commit 时长,还受到每月 500 张限制。
imagemin-pngquant.js
鉴于 tinipng 的缺点,我换了一个 npm 包,使压缩过程能够在本地完成。
const imagemin = require('imagemin');
const imageminPngquant = require('imagemin-pngquant');
let parentFolder = {};
pngs.forEach(x => { // 根据不同父级目录分类
let pf = x.subpath.slice(0, x.subpath.lastIndexOf('/'));
parentFolder[pf] ? parentFolder[pf].push(x.subpath) : parentFolder[pf] = [x.subpath];
});
for (let pf in parentFolder) {imagemin(parentFolder[pf], { // 原图片目录
destination: pf, // 生成图片的目录
plugins: [
imageminPngquant({
speed: 1,
quality: [0.40, 0.50],
})
]
})
.then(res => {execSync('git add .');
})
.catch(err => {console.log(err);
process.exit(1);
})
}
实际检验在上述配置下,imagemin-pngquant
具有 80% 的压缩率,高于 tinypng
,唯一的例外是一张色彩空间为RGB
的图片,这张图片在同批测试中仅得到了 20% 的压缩率,在 tinyPNG
中虽然获得了 50% 的压缩率,但也属于同批图片中效率最低的一张。
因此在 UI 交付图片时,我们应该关注下色彩空间是否为 sRGB,如果不是,应该打回重新切。正常情况下,用于 Web 程序的图片,颜色通道应为 sRGB,因为绝大部分浏览器是使用 sRGB 色彩空间来渲染图片的。
Why Should We Use sRGB in Broswer
Preparing Images Color for the Web
除了压缩效率压过 tinyPNG
一头,在时间上 iamgemin-pngquant
几乎是秒转换,同批图片通过 tinyPNG
的 api 转换需要花费 10 – 20s。
webp
将 png
格式的图片转换为 webp
格式,可以节约更多的空间,在 lighthouse
性能检测中拿到更高的得分,但 IOS
系统目前还是无法直接兼容它,需要做转换,很难在工作流中做自动化的处理。
The End
不知道你的团队是如何处理图片压缩这一块的工作的?希望能一起分享