commitizen + imagemin + inquirer 实现diff图片自动压缩

1,194 阅读5分钟

前端性能优化,提升页面加载速度,对图片资源的处理是必不可少的

前言

在我们日常开发的前端项目中,图片资源会占用很大的比例,尤其是对于游戏类的项目而言,因此考虑到前端性能优化,提高页面加载速度,在图片资源必不可少的前提下,减少图片的体积就变得尤为重要。

思路

一开始我使用到的方案是: 使用 commitizen + imagemin 来实现图片的自动压缩。 该思路是,在不影响原有的项目基础下,在每次提交前,diff新的图片资源,只对新增和发生修改的图片进行压缩,并将要提交的图片资源替换为压缩后的文件进行提交。

该方案使用了一段时间后,产生了一个新的问题 - 那就是diff时有冲突,等开发人员解决完冲突之后重新提交代码,合入的图片资源会被当作新增的进行图片压缩

那么如何解决这个问题呢? 有想过几条思路,如:

  1. 提交的时候遍历项目下的所有图片资源,比较得出真正的新增或修改的图片资源(效率太慢,需要做很多无用功,也会导致提交代码需要等待很长的时间)
  2. 在gitlab的ci/cd中进行压缩,最终也无法解决这个问题,再次被否

突然想到一个思路: 既然机器无法智能化,它无法得知开发是真的提交一段新的代码还是解决冲突后在提交的操作,但是开发人员可以得知这个操作。 也就是可以直接给出是否需要压缩的选项给到开发人员,由开发来决定是否需要压缩图片

优化思路

最终版本的方案是: 使用 commitizen + imagemin+inquirer.prompt 来实现图片的自动压缩。

该思路是: 在原有的基础下,使用inquirer.prompt 用户与命令行交互工具,提供是否需要压缩和不需要压缩的选项,让开发自己来判断,就能规避图片被多次重复压缩的场景。

具体实现

安装依赖

项目资源暂时只用到png,jpg,webp 类型的资源,如还需压缩其他类型的资源,可以查询相关的imagemin类型工具依赖

npm install commitizen cz-conventional-changelog imagemin imagemin-pngquant imagemin-mozjpeg imagemin-webp inquirer -D

package.json

// package.json 
{ 
.... 
 "scripts": {
    "imagemin": "node imagemin.js",
    "commit": "git add -A && npm run imagemin && git-cz"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "imagemin": "^7.0.0",
    "imagemin-mozjpeg": "^8.0.0",
    "imagemin-pngquant": "^9.0.2",
    "imagemin-webp": "^6.0.0",
    "inquirer": "^8.0.0",
    "npmlog": "^6.0.0",
    "commitizen": "^4.1.2",
    "cz-conventional-changelog": "^3.2.0"
  },
  "config": {
    "commitizen": {
      "path": "node_modules/cz-conventional-changelog"
    }
  }
}

imagemin.js 核心文件

获取diff 改动的文件

/**
 * 只有新增和修改才会进行压缩
 * 重复名,删除,复制的直接过滤
 * add, delete, modified, renamed, copied
 */
const checkOperateType = (status) => {
    const operateTypes = ['a', 'm'];
    return operateTypes.includes(status);
};

/** 文件类型判断, 暂只判断jpg,png,webp类型的图片 */
const checkImgType = (name) => {
    const filesType = ['jpg', 'jpeg', 'png', 'webp'];
    return filesType.includes(name);
};

/** 获取diff 改动文件 */
const getDiffFiles = () => {
    // 通过git diff 命令拿到本次提交涉及的变动文件
    const root = process.cwd();
    const files = execSync('git diff --cached --name-status HEAD').toString().split('\n');
    const result = [];
    files.forEach((file) => {
        if (!file) return;
        const temp = file.split(/[\n\t]/);
        const status = temp[0].toLowerCase();
        const filePath = root + '/' + temp[1];
        const extName = path.extname(filePath).slice(1);
        /** 非图片格式和不满足状态的跳过处理 */
        if (checkImgType(extName) && checkOperateType(status)) {
            result.push({
                path: filePath, // 绝对路径
                subpath: temp[1], // 相对路径
                extName: extName, // 扩展名
            });
        }
    });
    return result;
};

压缩处理


/** 获取需要压缩的资源目录 */
const getParentFolder = (imgs) => {
    const parentFolder = {};
    imgs.forEach((x) => {
        // 根据不同父级目录分类
        const pf = x.subpath.slice(0, x.subpath.lastIndexOf('/'));
        parentFolder[pf]
            ? parentFolder[pf].push(x.subpath)
            : parentFolder[pf] = [x.subpath];
    });
    return parentFolder;
};

/** 压缩图片的配置 */

async function imgConfiguration(imgFile, pf) {
    await imagemin(imgFile, {
        // 原图片目录 pf
        destination: pf, // 生成图片的目录
        plugins: [
            // minQuality,maxQuality 表示可配置的最小和最大压缩质量像素
            imageminPngquant({
                speed: 1,
                quality: [minQuality, maxQuality],
            }),
            imageminMozjpeg({
                quality: ((maxQuality + minQuality) * 100) / 2,
            }),
        ],
        use: [
            imageminWebp({
                quality: minQuality * 100,
            }),
        ],
    });
}

/** 获取压缩图片的配置 */
async function imgConfiguration(imgFile, pf) {
    await imagemin(imgFile, {
        // 原图片目录 pf
        destination: pf, // 生成图片的目录
        plugins: [
            imageminPngquant({
                speed: 1,
                quality: [minQuality, maxQuality],
            }),
            imageminMozjpeg({
                quality: ((maxQuality + minQuality) * 100) / 2,
            }),
        ],
        use: [
            imageminWebp({
                quality: minQuality * 100,
            }),
        ],
    });
}
/** 对需要压缩的图片进行处理 */
const compressPics = async (files) => {
    const parentFolder = getParentFolder(files);
    try {
        for (let pf in parentFolder) {
            if (parentFolder.hasOwnProperty(pf)) {
                await imgConfiguration(parentFolder[pf], pf);
            }
        }
        execSync('git add -A');
    } catch (e) {
        log.warn(e);
        // 退出构建,暂停jenkins
        process.exit(1);
    }
};

用户与命令行交互选择


/** 
* commit 类型
* 0 正常提交,需要压缩
* 1 合入冲突,不需要压缩
*/
const CommitType = {
    NORMALLY_COMMIT: 0,
    MERGE_COMMIT: 1,
};


/**
 * 对提交的类型进行判断
 */
async function checkImage() {
    let commitType = CommitType.NORMALLY_COMMIT;
    // diff出来需要压缩的图片
    const imgDiff = getDiffFiles();
    /** 没有图片直接跳过选择,默认不需要压缩图片 */
    if (!(imgDiff && imgDiff.length)) return;
    try {
        const promise = new Promise((resolve) => {
            inquirer
                .prompt([
                    {
                        type: 'list',
                        name: 'commitType',
                        message: 'Which type of code to commit?',
                        choices: [
                            'Normally Commit(正常压缩)',
                            'Merge Commit (合入代码)',
                        ],
                    },
                ])
                .then((ans) => {
                    switch (ans.commitType) {
                        case 'Normally Commit(正常压缩)':
                            commitType = CommitType.NORMALLY_COMMIT;
                            break;
                        case 'Merge Commit (合入代码)':
                            commitType = CommitType.MERGE_COMMIT;
                            break;
                        default:
                            break;
                    }
                    resolve();
                });
        });
        await promise;
    } catch (e) { }
    if (commitType === CommitType.NORMALLY_COMMIT) {
        log.info('pre-commit hook start imagemin! \n');
        compressPics(imgDiff);
    }
}

总结

由于项目使用了webpack,可以使用webpackimage-webpack-loader来压缩图片。但是这种方案弊端

  1. 就是webpack每次构建的时候都要处理一次图片压缩,会影响到webpack的构建速度
  2. webpack 无法每次只压缩新增的图片资源,忽略原有的图片资源

最终权衡之下才选择用上述的方案,当然,该方案仍然有不足的地方,比如:合入提交不需要压缩,但是点击回车键过快,默认就选择了压缩,需要自己重新撤销合并操作,并重新解决冲突,需要开发人员自己注意。