前端图片压缩上传

227 阅读3分钟

基本原理

重点就是借助 canvas 提供的 API 进行压缩:参考文档

canvas.toDataURL(type, encoderOptions)

压缩图片基本步骤

  1. 获取图片

  2. 将图片 “画” 在 canvas 上面

  3. 调用 canvas.toDataURL 对图片进行压缩

  4. 得到压缩后的图片代码 base64 格式

具体实现可根据需要,自己自定义。

压缩图片上传小案例

前端代码

<input type="file" id="upload" onchange="compressPic(this.files[0])" >
<p>图片源大小:<span id="source"></span></p>
<p>压缩图片大小:<span id="dest"></span></p>
<img id="pic" />

<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.26.1/axios.min.js"></script>
<script>
  const ori = document.getElementById('source')
  const compress = document.getElementById('dest')
  
  function compressPic(file) {
    const reader = new FileReader()
    reader.onload = function (e) {
      const canvas = document.createElement('canvas') 
      const ctx = canvas.getContext("2d") 
      const image = new Image() 
      image.src = e.target.result 
      setTimeout(() => { // image.src 赋值过程是异步的(猜测)
        canvas.width = image.width
        canvas.height = image.height
        ctx.drawImage(image, 0, 0, canvas.width, canvas.height)
        const data = canvas.toDataURL('image/jpeg', 0.5) // 第二个参数为 1 代表不压缩
        uploadPic('compress-pic', data)
        
        // 显示图片和始末文件大小便于观察
        document.getElementById('pic').src = data
        ori.innerText = returnFileSize(baseToFile('ori',image.src).size)
        compress.innerText = returnFileSize(baseToFile('com', data).size)
        
      }, 0)
    }
    reader.readAsDataURL(file)
  }

  // 上传
  function uploadPic (fileName, data) {
    const file = baseToFile(fileName, data)
    const formData = new FormData()
    fileName = 'singleImage' // 要与后台规定的名称一致
    formData.append(fileName, file)
    axios({
      method: 'post',
      url: 'http://127.0.0.1:3030/upload',
      headers: {'Content-Type': 'multipart/form-data'},
      data: formData
    })
  }

  //将 base64 转换为 file
  function baseToFile (fileName, data) {
    const arr = data.split(',') // 分开类型和代码
    const type = arr[0].match(/:(.*?);/)[1] // 取出类型
    const b = [...window.atob(arr[1])] // 解码 base 64
    const u8a = new Uint8Array(b.length) 
    b.forEach((v,i) => { u8a[i] = v.charCodeAt(0) }) // 解码内容变成 unit8 内容
    fileName = 'demo.jpg' // 在前端声明是什么文件, 方便后端识别
    return new File([u8a], fileName, { type })
  }

  // 计算单位
  function returnFileSize(number) {
    if(number < 1024) {
      return number + 'bytes';
    } else if(number >= 1024 && number < 1048576) {
      return (number/1024).toFixed(1) + 'KB';
    } else if(number >= 1048576) {
      return (number/1048576).toFixed(1) + 'MB';
    }
  }
</script>

express 搭建的后端代码

const express = require('express')
const multer = require('multer')
const cors = require('cors')

const app = express()
const port = 3030

app.listen(port, () => {console.log(port)})
app.use(cors())

const storage = multer.diskStorage({
  destination: './files',
  filename: (req, file, callback) => {
    callback(null, `${Date.now()}-${file.originalname}`)
  }
})

const fileFilter = (req, file, callback) => {
    const reg = /^image\/.+/
    if (reg.test(file.mimetype))
      callback(null, true)
    else 
      callback(new Error('only upload image'), false)
  }
const multerUpload = multer({storage, fileFilter}).single('singleImage')

app.post('/upload', multerUpload, (req, res) => {
  res.send('ok')
})

纪录学习过程中遇到的知识点

获取用户上传的图片

主要利用了 <input type="file">File 对象。input 参考文档File 参考文档

基本使用

<input type="file" id="upload" onchange="compressPic(this.files[0])" >

<script>
function compressPic(file) {} // file 类型是 File 对象
</script>

注意一点就是,如果 onchange 写在 <script> 中的话,是这样获取 file 的:

<input type="file" id="upload" >

<script>
document.querySelector('#upload').onchange = (e) => {
    compressPic(e.target.files[0])
}
function compressPic(file) {}
</script>

获取用户选择的图片数据 base64 形式

方法就是利用 FileReader 对象。参考文档

基本使用

const reader = new FileReader()
reader.onload = function (e) {
    // e.target.result 就是文件数据: base64 格式
}
reader.readAsDataURL(file) // 此 file 为 File 对象

简单说明,onload 时间是在 reader.readAsDataURL(file) 完成后才执行的,因此才能够获取到图片的数据。

图片对象

可以有两种创建方式创建一个图片节点对象,两者是等效的。参考文档

const img = document.createElement("img")
var img = new Image() 

前面得到的 base64 数据可以直接赋值给 img.src 属性。需要注意的是,该赋值过程应该是异步的,所以无法直接在赋值后就获取到,所以想要获取它的于是,可以写在 setTimeout(() => {}, 0) 中(此处无参考文献,属于个人猜测)

闭包:向事件回调函数传值

这是刚开始看到的闭包代码,第一眼居然没立马反应过来是闭包,故记录一下。参考文件

const reader = new FileReader()
reader.onload = ((aImg) => {
  console.log('1')
  return function (e) {
    aImg.src = e.target.result
    console.log('3')
  }
})(img)
console.log('2')

简单解释:先执行了匿名函数,并传入一个 img 参数,然后返回了一个函数,作为 onload 回调函数,秒就秒在该回调函数中可以访问到 aImg 变量。

canvas 压缩图片

此处用到的关于 canvas 的 API 有 :

getContext() 参考文档

drawImage() 参考文档

toDataURL() 参考文档

主要代码是如下

// 创建 canvas 节点元素
const canvas = document.createElement('canvas') 
// 创建 canvas 上下文,用于绘制图片
const ctx = canvas.getContext("2d") 
// image.width 和 image.height 是图片本身宽高
// canvas.width 和 canvas.height 设置的是 canvas 元素展示区域的大小
canvas.width = image.width
canvas.height = image.height
// 文档中的 dx dy dHeight dWidth 控制的就是 canvas 区域的上下边界和 显示的图片的宽高
// 文档中的 sx sy dHeight sWidth 修改的 “原图” 的上下边界和宽度
// “原图” 加双引号表示的是抽象的原图,真正的原图(前获取到的数据)并不会被修改。
ctx.drawImage(image, 0, 0, canvas.width, canvas.height)
// toDataURL 是对 drawImage 画出来后的图进行压缩,想要压缩,第一个参数只有 'image/jpeg' 和 'image/webp' 可选
const data = canvas.toDataURL('image/jpeg', 0.5) // 第二个参数为 1 代表不压缩

base64 转换为 file

使用的是 new File() 来转换。参考文档

在此之前,因为 File() 接收的是  DOMString 对象的 Array  所以还会用到下面的 API

split() 参考文档

match() 参考文档

atob() 参考文档

Uint8Array() 参考文档

charCodeAt() 参考文档

具体代码:

const arr = data.split(',') // 分开类型和代码
const type = arr[0].match(/:(.*?);/)[1] // 取出类型
const b = [...window.atob(arr[1])] // 解码 base 64
const u8a = new Uint8Array(b.length) 
b.forEach((v,i) => { u8a[i] = v.charCodeAt(0) }) // 解码内容变成 unit8 内容
fileName = 'demo.jpg' // 在前端声明是什么文件, 方便后端识别
return new File([u8a], fileName, { type })