node实现图片压缩并上传到七牛

2,974 阅读6分钟

一、内容简介

在开发一些图片比较多的页面时,为了提升一些页面性能,加快页面显示,通常会选择把图片压缩后,上传到七牛后直接使用七牛地址,那么开发中写一张图片总共分两步:

  1. 将图片压缩(一般使用tinypng)后保存在本地;
  2. 将压缩后图片上传到七牛获取图片链接后写在代码里
    所以,我用node搭了一个简单的服务,用来直接处理图片压缩和上传:

二、效果预览

可以看到我选择的图片大概有700kb左右,那么最后生成的图片有多大呢? 这张图片上传到七牛后只有197kb,压缩效果还是挺明显的

这个效果预览是使用了之前自己开发的一个vsCode上传图片插件,对vsCode插件开发感兴趣的同学可以去查看之前写的文章,一起学习进步,也可以直接在vscode中搜索插件upload-to-qiniu进行安装。

三、压缩功能实现

实现功能点介绍好了,顺便还给自己的插件打了一个小广告。下面进入正题,如何实现:
其实准确来说,具体的压缩算法我也写不出来,只是利用了几个第三方的方案来实现我们的功能:

  1. squoosh: 是谷歌大佬开发的一款图片压缩工具,通过全局安装npm i -g @squoosh/cli,执行相关命令后实现图片的压缩,直接贴出github地址,大家可以安装在自己本地尝试一下,毕竟谷歌大佬,压缩效果应该还是很不错的;
  2. imagemin: 也是一个比较主流的压缩库,用的人比较多,所以开发了使用这种方式压缩的功能,不过有一个坑,在安装imagemin-pngquant时需要在本地先通过brew安装libpng,否则的话会导致这个库安装不上去;
  3. 通过七牛提供的方法compressImage压缩: 这种方法是在前端项目中安装qiniu-js依赖,使用七牛提供的apicompressImage进行压缩,但是经过测试,发现使用这种方式压缩的效果不是很明显,而且也是在前端调用,所以只提供了七牛的文档
  4. 通过在图片链接中添加七牛参数, 这种链接后添加图片参数的方式也是我最常用的方式,效用比较明显,使用方式也十分简单,只要阅读文档就很容易掌握啦。

1. 项目初始化

先来安装项目中需要的依赖:

"dependencies": {
    "@squoosh/cli": "^0.6.1",
    "express": "^4.17.1",
    "imagemin": "^7.0.1",
    "imagemin-jpegtran": "^7.0.0",
    "imagemin-pngquant": "^9.0.1",
    "multer": "^1.4.2",
    "qiniu": "7.3.2",
    "body-parser": "^1.19.0"
}

其中multer是用来处理前端表单上传的文件,他可以方便我们直接在服务端获取前端上传的文件,会将我们上传的内容保存在指定目录下,所以我新建了一个文件用来保存这些数据: compress文件用来保存压缩后的图片, original文件用来保存压缩前的原图

安装好依赖后在 src/index.js中启动服务,并新建一个image的路由:

const express = require('express')
const app = express()
const bodyParser = require('body-parser')
const imageRouter = require('./router/image')
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.use('/image', imageRouter)

app.listen(3000, () => {
  console.log('serve is listen on 3000')
})

到这里都是基本操作,简单启动一个node服务

2. 确定图片压缩接口的请求参数,定义辅助方法

最重要的一点,我们要知道,我们这个接口要传哪些参数,返回什么数据,先定义好这些内容才可以方便具体功能上的开发:
我直接就是一个接口文档定义出来:
请求方式: POST
请求 URL: serve.lyaayl.com:3000/image/compr…

请求参数

字段字段类型是否必填字段说明
fileFile表单上传图片
typeString: imagemin, squoosh压缩方式,默认为 imagemin
qiniuConfJSON String传入七牛参数 (accessKey, secretKey, scope, domain),默认为个人七牛账号用于 demo 测试

返回结果

{
  "data": {
    "type": "success",  // 是否压缩成功
    "originalSize": "10kb", // 原始图片尺寸
    "compressSize": "1kb", // 压缩图片尺寸
    "url": "xxx" // 图片七牛链接
  },
  "status": {
    "code": 0, // 请求成功
    "msg": "成功"
  }
}

定义请求成功和失败的辅助方法(src/utils/common.js)

// 成功返回
const successRes = (data) => {
  return {
    status: { code: 0, msg: '成功' },
    data,
  }
}

// 失败返回
const errorRes = (msg) => {
  return {
    status: { code: 1, msg },
  }
}

定义相关常量量(src/config.js)

const path = require('path')
// 压缩图片的存储路径
const COMPRESS_IMG_PATH = path.resolve(__dirname, './image/compress/')
// 原始图片的存储路径
const ORIGINAL_IMG_PATH = path.resolve(__dirname, './image/original/')

// 七牛云上传参数
const QI_NIU_CONFIG = {
  accessKey: 'xxx',
  secretKey: 'xxx',
  scope: 'xxx',
  domain: 'xxx',
}

3.实现压缩逻辑

实际思路是很简单的:

  1. 在接受到请求参数后,通过判断压缩类型,使用不同的压缩方法,生成压缩后的图片并保存在original文件下;
  2. 压缩完成后,得到文件名,通过node读取到对应文件上传到七牛,返回七牛地址

先看下我实现图片压缩方法的源码:

router.post('/compress', async function (req, res) {
  const { type = 'imagemin', qiniuConf = JSON.stringify(QI_NIU_CONFIG) } = req.body

  try {
    const data = await compressImage(req.files[0], type, JSON.parse(qiniuConf))
    res.send(successRes(data))
  } catch (err) {
    res.send(errorRes(`压缩失败: ${err}`))
  }
})

是不是很简单呢,这里因为qiniuConf接受的是一个json字符串,所以在设置默认配置时,统一转成字符串。
接下来的方法就是compressImage方法实现压缩,接收文件参数file,压缩类型type,七牛参数qiniuConf

实现imagemin压缩

其实这个逻辑是非常简单的,只要我们使用之前安装的imagemin库就可以处理:

if (type === 'imagemin') {
  // 通过imagemin 进行压缩
  const bufferFile = await getBufferFromFile(filePath)
  const imageBuffer = await imagemin.buffer(bufferFile, {
    plugins: [
      imageminJpegtran(),
      imageminPngquant({
        quality: [0.8, 0.9],
      }),
    ],
  })
  bufferImageToFile(imageBuffer, fileName)
}

其中filePath是我们上传图片后,通过multer处理后保存在original目录下的原图,通过方法getBufferFromFile(其辅助方法这里先说明功能,方法在之前文章node实现图片压缩有介绍)获取图片的buffer,将buffer传入第三发库中实现压缩,压缩完成后,会返回一个buffer文件,最后将buffer写入对应compress目录中即可完成。

实现squoosh压缩

这个方案思路简单,但是要想用好还是很复杂的,我只是用了默认的配置参数来压缩图片,@squoosh/cli这个库压缩图片其实可以转成很多种图片类型,每种不同的图片类型都有不同的压缩质量参数可以设置,先看最重要的代码:

if (type === 'squoosh') {
  // 通过 squoosh 进行压缩
  const cli = getSquooshCli(filePath, extname)
  execSync(cli)
}

通过参数拼接一个需要执行的shell命令,然后使用node模块child_process去执行他,就把图片压缩在对应目录下了,不对,下面才是最重要的代码:

/**
 * 拼接压缩图片命令
 * @param {String} imagePath 原图路径
 * @return {String}
 */
const getSquooshCli = (imagePath, extname) => {
  let imageType = extname === 'jpg' ? 'mozjpeg' : 'oxipng'
  let quality = extname === 'jpg' ? `'{quality: 75}'` : 'auto'
  return `squoosh-cli --${imageType} ${quality} ${imagePath} -d ${COMPRESS_IMG_PATH}`
}

拼接一个需要执行的shell命令,其含义是:通过squoosh-cli 压缩图片imagePath(其中压缩后图片类型为imageType,质量为quality),压缩后放在COMPRESS_IMG_PATH目录下

imageType这里我只选择了压缩成两种最常见的图片类型jpgpng,quality则为图片压缩的质量,这里我是直接使用了默认值,当值设置过低时,可能会出现图片变得不清晰甚至十分模糊,具体的压缩效果大家可以通过squoosh这个网页去尝试,我选择相信大佬的默认值,所以没有进行调整,也没有把这个参数设置成动态的,也为了方便我们的接口不会有过多参数。

4.将压缩后的图片上传到七牛云

上传七牛是直接使用七牛云提供的node依赖qiniu,既然都实现了压缩逻辑,那不如再加一个普通的上传图片到七牛云,反正上传方法是一样的:

// 上传图片到七牛
router.post('/upload', async function (req, res) {
  try {
    const { filename } = req.files[0]
    const { qiniuConf = JSON.stringify(QI_NIU_CONFIG) } = req.body
    const filePath = path.join(ORIGINAL_IMG_PATH, filename)
    const url = await uploadImageToQiniu(filePath, JSON.parse(qiniuConf))
    res.send(
      successRes({
        url,
      })
    )
  } catch (err) {
    res.send(errorRes(err))
  }
})

这里最重要的就是上传到七牛的方法uploadImageToQiniu,接受图片的路径和七牛配置参数:

/**
 * 上传图片到七牛
 * @param {string} imagePath 上传图片的路径
 * @param {Object} qiniuConf 七牛参数
 * @return {Promise<string>} 返回上传成功后图片地址
 */
async function uploadImageToQiniu(imagePath, qiniuConf) {
  return new Promise((resolve, reject) => {
    const { accessKey, secretKey, scope, domain } = qiniuConf
    const config = new qiniu.conf.Config()
    const formUploader = new qiniu.form_up.FormUploader(config)
    const putExtra = new qiniu.form_up.PutExtra()
    const token = getToken(accessKey, secretKey, scope)

    // 上传内容
    const key = path.parse(imagePath).name
    const uploadItem = path.normalize(imagePath)
    formUploader.putFile(token, key, uploadItem, putExtra, function (respErr, respBody) {
      if (respErr) {
        reject(respErr)
      } else {
        const { key } = respBody
        resolve(`${domain}/${key}`)
        // 删除image文件中的图片文件
        setTimeout(() => {
          clearImageFile()
        }, 0)
      }
    })
  })
}

其中方法大多都是七牛提供的api,最终返回上传后的图片地址,上传完成后,执行clearImageFile,将之前压缩图片和原图都从目录中删除,就完成上传七牛的逻辑啦。

四、前端demo展示

在开发中其实是发现了一个坑:通过squoosh压缩png类型图片是会非常非常慢,但是压缩jpg的图片就会比较快,也正是因为这个原因,在vscode插件中是使用imagemin方式进行压缩。如果以后解决了这个问题,也会及时分享出来,并开发一个对应的前端页面方便一些同学直接使用。 到这里主要的压缩上传功能已经实现,接下来就是写一个前端界面测试一下,我们的功能是否好用: 简单写了一个前端界面,用来测试,这里只贴出最主要的上传代码,样式什么的相信大家都很熟练了:

  handleInputChange(el) {
    const files = el.target.files
    const file = files[0]
    const param = new FormData()
    param.append('file', file)
    param.append('qiniuConf', JSON.stringify(upConfig))

    if (!this.upType) {
      axios
        .post('/api/image/upload', param, {
          headers: { 'Content-Type': 'multipart/form-data' }
        })
        .then(res => {
          console.log('上传到七牛云', res.data)
        })
    } else {
      param.append('type', this.upType)
      axios
        .post('/api/image/compress', param, {
          headers: { 'Content-Type': 'multipart/form-data' }
        })
        .then(res => {
          console.log(`通过${this.upType}方法压缩后`, res.data)
        })
    }
  }

这个就是前端上传代码,new FormData()后,将需要的参数file、type、qiniuConf传入,看下控制台的打印: 发现已经可以返回压缩后的图片链接

五、上传到服务器

node端的压缩逻辑已经实现,接着就将代码上传到自己的服务器上,运行时有几个点要注意:

  1. 必须在服务器上安装一个brew,然后安装之前提到过的依赖libpng,这个安装需要更新服务器上的git和curl版本
  2. 这个项目中使用了squoosh,安装依赖时必须使用npm install,使用yarn 会报错,虽然不知道为啥,但是确实会失败,如果想本地运行也是一样的,必须使用npm install

这里就不太多介绍如果在服务器上运行了,毕竟也不是很专业。拉下代码,用pm2跑起来,能请求就可以啦。

六、最后

因为工作中一直都在使用图片压缩后再上传的方法,时间久了容易对写代码不感兴趣,所以专门找了几个可以前端实现压缩的方案,并封装了node代码和vscode插件,以后可以更开心的开发啦^ _ ^。

重要提示: 如果有安装upload-to-qiniu这个插件来上传图片,虽然不设置七牛的参数也会使用默认账号上传,但因为是个人七牛账号,空间比较小,用作demo演示还行,会经常删除七牛上的图片,所以建议大家如果在实际项目中要使用自己公司的七牛账号~~
最后一句
已经将图片压缩上传的全部代码上传到github,欢迎大家点个start^ ^。感谢阅读🙏