手把手教你撸一个图片压缩工具

609 阅读2分钟

利用tingPng接口压缩,支持压缩失败重新压缩,支持命令行调用,支持npm 安装调用。话不多说,开干。

压缩展示

enter image description here

需要用到的工具

Listr 提供终端可执行的任务列表
cli-table' 允许从node.js脚本在命令行上呈现unicode辅助表(也就是表格呈现结果) chalk 命令行上呈现不同颜色的文字 minimist 解析命令行参数 commander 生成命令行命令。

目录结构

project
│   README.md
│   package.json
│   .gitignore
└───bin
│   │   esigntiny
└───demo
|   │   images
|   │   index.js
└───lib
|   │  index.js
|   │  initTiny.js
|   │  esigntiny.js
|   │  compressTiny.js
|   │  compressFailedImg.js
|   │  resultToChalk.js
|   │  utils.js

bin 用来存放可执行的node文件,用于在命令行调用方式。demo 提供了可直接运行的脚本和需要压缩的图片。lib 是代码源文件。

commander生成命令行命令

#!/usr/bin/env node //node环境下执行

const program = require('commander')
const esigntiny = require('../lib')
const path = require('path')
const argv = require('minimist')(process.argv.slice(2))
let input = argv.i || argv.input
let output = argv.o || argv.output
const { getCwd } = require('../lib/utils.js')

// 命令行提示
program
  .version(require('../package.json').version, '-v --version')
  .usage('<command> [options]')
  .option('-i, --input', 'input directory [require]')
  .option('-o, --output', 'output directory')

// 注册start命令
program.command('start').description('start compress images').action(start)

// 解析命令行
program.parse(process.argv)

// 命令行输入 esigntiny start执行此函数
function start() {
  if (!input) {
    console.log('require input directory')
    return
  }

  input = path.resolve(getCwd(), input)
  if (!!output) output = path.resolve(getCwd(), output)

  esigntiny({
    input: input,
    output: output,
  })
}

if (!program.args.length) {
  program.outputHelp()
}

lib代码分析

实现的逻辑非常简单,首先递归获取inputimage地址,然后调用tinyPng的接口压缩拿到压缩后的图片地址,然后请求图片地址写入目标文件。

lib/index.js

const { normalizeOptions } = require('./utils.js')
const initTiny = require('./initTiny.js')
const compressTiny = require('./compressTiny.js')

const esigntiny = async function (options) {
  // 初始化用户传参
  options = normalizeOptions(options)
  // 1.初始化程序,递归拿到图片地址
  // 2.压缩图片
  const taskExample = compressTiny(initTiny(options))

  taskExample.run().catch((err) => console.log(err))
}

module.exports = esigntiny

index.js主要是标准化参数,创建listr实例,创建获取图片,压缩图片的task

initTiny

const Listr = require('listr')
// 递归获取image地址
const { getImsges } = require('./utils.js')

module.exports = function (options) {
  const taskExample = new Listr()
  taskExample.add(getFiles(options))
  return taskExample
}

function getFiles(options) {
  return {
    title: '获取所有图片数量',
    task: (ctx, task) => {
      ctx.options = options
      ctx.images = getImsges(options.input)
      task.title = `共找到${ctx.images.length}张图`
      if (ctx.images.length === 0) {
        Promise.reject('未找到图片')
      }
    },
  }
}

compressTiny

const resultToChalk = require('./resultToChalk.js')
const compressFailedImg = require('./compressFailedImg.js')
const { tinyFun } = require('./utils.js')

module.exports = function (taskExample) {
  taskExample.add({
    title: '压缩图片',
    task: async (ctx, task) => {
	  // 获取所有的图片的压缩结果
      const imagesRsult = await Promise.all(tinyFun(ctx))
      // 在次压缩失败的图片
      const failedList = await resultToChalk(imagesRsult)
      await compressFailedImg(failedList, ctx.options)
    },
  })

  return taskExample
}

获取所有图片的压缩结果,然后绘制结果表格,然后开启图片的再次压缩。

tinyFun 图片压缩

/**
 * 将每个文件压缩返回promise
 * compressFile包装每一个请求链接
 */
const tinyFun = (ctx) => {
  const { images, options } = ctx
  return images.map((item) => {
    return compressFile(item, options)
  })
}

/**
 * 压缩文件
 */
const compressFile = (filePath, options) => {
  return new Promise((resolve, reject) => {
    createReadStream(filePath).pipe(
      request(parms, (res) => {
        res.on('data', async (info) => {
          try {
            info = JSON.parse(info.toString())
            // console.log('[[[[[]]]]]', info)
            if (/^\s*</g.test(info) || info.error) {
              resolve(
                getMessage({
                  info,
                  filePath,
                  msg: '压缩失败',
                  code: 500,
                })
              )
              return
            }
            resolve(await getImageData(info, options, filePath))
          } catch (e) {
            // console.log(e, '))0')
            resolve(
              getMessage({
                info,
                filePath,
                msg: '接口请求被拒绝',
                code: 500,
              })
            )
          }
        })
      })
    )
  })
}

/**
 * 读取图片,写入文件
 */
const getImageData = (imageInfo, options, filePath) => {
  let output = options.output
  const input = options.input
  const imageUrl = imageInfo.output.url
  const oldSize = (imageInfo.input.size / 1024).toFixed(2)
  const newSize = (imageInfo.output.size / 1024).toFixed(2)
  return new Promise((resolve, reject) => {
    get(imageUrl, (res) => {
      const outDir = path.dirname(output)
      output = filePath.replace(input, output)
      if (!existsSync(outDir)) {
        mkdirSync(outDir)
      }
      res.pipe(createWriteStream(output))

      res.on('end', function () {
        resolve(
          getMessage({
            code: 200,
            filePath,
            msg: '压缩成功',
            info: {
              oldSize,
              newSize,
              imageUrl,
            },
          })
        )
      })
    })
  })
}
/**
 * 接口的文案提示
 */
const getMessage = ({ msg, code, info, filePath }) => {
  return {
    code: code || 400,
    msg: msg || '成功',
    data: {
      filePath,
      info,
    },
  }
}

将每个文件压缩返回promise, compressFile包装每一个请求链接,创建可读流请求tinyPng压缩图片,注意这里要将所有的结果都resolve出去,错误处理交给resultToChalk函数,压缩成功会返回压缩成功之后的图片地址,创建请求写入指定的文件地址。

resultToChalk结果打印

const Table = require('cli-table')
const chalk = require('chalk')
const path = require('path')
const { sleep } = require('./utils.js')

const headArr = {
  head: ['name', 'status', 'old-size(kb)', 'new-size(kb)', 'compress ratio(%)'],
}
// const table = new Table({
//   head: ,
// })

// 获取要打印的数据
const getSuccessInfo = (result) => {
  const data = result.data
  const info = data.info
  const fileName = path.basename(data.filePath)
  const compressRatio = parseFloat(
    ((info.oldSize - info.newSize) / info.oldSize) * 100
  ).toFixed(2)

  return [fileName, 'success', info.oldSize, info.newSize, compressRatio]
}

module.exports = async function (imagesRsult) {
  let totalNewSize = 0,
    totalOldSize = 0,
    successNum = 0,
    failedNum = 0,
    failedList = [],
    table = new Table(headArr)

  if (imagesRsult && imagesRsult.length) {
    imagesRsult.forEach((result) => {
      const filePath = result.data.filePath
      if (result.code === 200) {
        totalNewSize += +result.data.info.newSize
        totalOldSize += +result.data.info.oldSize
        successNum += 1
        table.push(getSuccessInfo(result))
      } else {
        const fileName = path.basename(filePath)
        failedNum += 1
        failedList.push(filePath)
        table.push([fileName, 'failed'])
      }
    })
  }
  await sleep(1000)

  console.log(table.toString())

  const resStr = `图片总数量:${
    imagesRsult.length
  }, 压缩成功:${successNum}, 压缩失败:${failedNum}, 压缩比:${
    ((totalOldSize - totalNewSize) / totalOldSize).toFixed(2) * 100
  } (%)`
  console.log(chalk.red(resStr))

  // 2秒后开启失败的压缩
  await sleep(2000)

  return failedList
}

compressFailedImg 失败结果再次压缩

const { tinyFun } = require('./utils.js')
const Listr = require('listr')
const resultToChalk = require('./resultToChalk.js')

let compressTimes = 1
// 开启新的Listr,创建图片压缩task
module.exports = async function compressFailedImg(failedList, options) {
  const taskExample = new Listr()
  if (compressTimes-- > 0) {
    taskExample.add({
      title: '再次压缩失败图片',
      task: async (ctx, task) => {
        const imagesRsult = await Promise.all(
          tinyFun({
            images: failedList,
            options,
          })
        )
        // 在次压缩失败的图片
        await resultToChalk(imagesRsult)
      },
    })
    taskExample.run().catch((err) => {
      console.log(err)
    })
  }
}

compressFailedImg开启新的压缩Listr,同样调用tinyFun获取压缩后的结果,然后交给resultToChalk处理。

好了基本上就完成了压缩小工具,gitHub地址 希望大家能喜欢。