独书先生 Menu

自建node图片压缩服务

需求

图片压缩服务很常见,比如 tinypng.com,压缩图等,但是如何打造自己的图片压缩服务?

思路

首先想到使用tinypng.com提供的API,但是也是有限制或者购买服务,不符合我们自建服务的需要,我们需要完全开源的自有服务,于是了解到了imagemin,有node版本

解决方案

采用koa后台框架搭建node服务

  1. 先安装依赖
npm i imagemin imagemin-giflossy imagemin-mozjpeg imagemin-pngquant imagemin-svgo imagemin-webp
  1. 编写koa基础服务,参考https://www.liaoxuefeng.com/wiki/1022910821149312/1099849448318080

  2. 编写imagemin服务

// 安装依赖:npm i imagemin imagemin-giflossy imagemin-mozjpeg imagemin-pngquant imagemin-svgo imagemin-webp

const fs = require('fs');
const path = require('path');
const imagemin = require('imagemin');
const imageminMozjpeg = require('imagemin-mozjpeg');
const imageminPngquant = require('imagemin-pngquant');
const imageminGiflossy = require('imagemin-giflossy');
const imageminWebp = require('imagemin-webp');
const imageminSvgo = require('imagemin-svgo');

//stream pipeline
const util = require('util');
const stream = require('stream');

const imgMIMEList = {
    svg: 'image/svg+xml',
    png: 'image/png',
    jpg: 'image/jpeg',
    jpeg: 'image/jpeg',
    gif: 'image/gif',
    bmp: 'image/bmp',
    webp: 'image/webp',
    ico: 'image/x-icon',
    tif: 'image/tiff',
    psd: 'application/octet-stream',
}

//TODO:提示压缩率

const fn_imagemin = async (ctx, next) => {

    const file = ctx.request.files.file;    // 获取上传文件
    const reader = fs.createReadStream(file.path);  // 创建可读流
    const ext = file.name.split('.').pop();     // 获取上传文件扩展名
    const dataMIME = "data:" + imgMIMEList[ext] + ";base64,"; //根据扩展名设定媒体类型

    const imgName = getRandomId() + '.' + ext; //压缩图片文件名
    const imgSourcePath = `storage/upload/img`; //压缩源图片文件夹
    const imgResultPath = `storage/result/img`; //压缩目标文件夹

    const imgSourcePathFile = `${imgSourcePath}/${imgName}`; //压缩源图片路径
    const imgSourcePathFileCWD = path.resolve(process.cwd(), imgSourcePathFile); //压缩源图片绝对路径

    const imgSourcePathCWD = path.resolve(process.cwd(), imgSourcePath); //源文件夹绝对路径
    const imgResultPathCWD = path.resolve(process.cwd(), imgResultPath); //目标文件夹绝对路径

    const upStream = fs.createWriteStream(imgSourcePathFileCWD); // 创建可写流 存储源图片
    // reader.pipe(upStream);// 可读流通过管道写入可写流
    const pipeline = util.promisify(stream.pipeline);
    let resultData = null;
    await pipeline(reader, upStream);
    await imageCompress(imgSourcePathFile, imgResultPath, (imgBuffer) => {

        resultData = dataMIME + imgBuffer.toString('base64');

    }).catch((err) => {
        ctx.response.status = 201
    })

    //删除图片
    setTimeout(()=>{
        deleteFile(imgSourcePathCWD,imgName);
        deleteFile(imgResultPathCWD,imgName);
    },0)

    if (file) {
        ctx.response.body = resultData;
    }
    else {
        ctx.response.status = 204;
    }

};

/**
 * 压缩图片
 * @param {String} sourcePath 
 * @param {String} resultPath 
 * @param {Function} callback 
 */
async function imageCompress(sourcePath, resultPath, callback) {
    const files = await imagemin([sourcePath], {
        destination: resultPath,
        plugins: [
            imageminMozjpeg({
                quality: 70
            }),
            imageminPngquant(),
            imageminGiflossy({
                lossy: 80
            }),
            imageminWebp(),
            imageminSvgo()
        ]
    });

    const imgBuffer = files[0].data;
    callback(imgBuffer)
}

/**
 * 获取随机id
 */
function getRandomId() {
    return Math.random().toString(36).substr(2) + Date.now().toString(36);
}


/**
 * 删除某一个包下面的需要符合格式的文件。
 * @param  {String} url  文件路径,绝对路径
 * @param  {String} name 需要删除的文件名称
 * @return {Null}   
 * @author huangh 20170123
 */
function deleteFile(url,name){
    let files = [];

    if( fs.existsSync(url) ) {    //判断给定的路径是否存在

        files = fs.readdirSync(url);    //返回文件和子目录的数组

        files.forEach(function(file,index){

            let curPath = path.join(url,file);

            if(fs.statSync(curPath).isDirectory()) { //同步读取文件夹文件,如果是文件夹,则函数回调
                deleteFile(curPath,name);
            } else {   

                if(file.indexOf(name)>-1){    //是指定文件,则删除
                    fs.unlinkSync(curPath);
                    console.log("删除文件:"+curPath);
                }
            }  
        });
    }else{
        console.log("给定的路径不存在!");
    }

}



module.exports = {
    'POST /upload/img': fn_imagemin
};

注意

本教程在于提供可行思路及核心代码,完整项目程序暂未整理.因为笔者在编写过程中发现,本地windows环境没有问题,但是docker环境下无法正常运行,所以整体的node服务仍然处于研究阶段,希望有兴趣的朋友实验成功后分享出来.

参考

https://github.com/imagemin/imagemin
https://web.dev/use-imagemin-to-compress-images/
https://www.liaoxuefeng.com/wiki/1022910821149312/1099849448318080