前端图片压缩上传

Author Avatar
Hongxu 3月 08, 2019

图片压缩

背景

最近业务中遇到了用户抱怨说上传图片时间太长,感觉上是系统特别慢。

所以打算对图片上传进行优化,前端压缩后再上传文件。

方案

经过搜索后目前大家常用的图片压缩方案有这么几种

  1. canvas 前端压缩 (移动端对 canvas 支持情况较好,能解决现有问题)

  2. 对图片尺寸压缩 (打卡需要用户与背景的合照,不能对尺寸压缩会丢失信息)

  3. 上传图片后端压缩 (需要解决的问题是上传慢,这种方法不适合当前场景)

对比后我选择了第一种方案,就是 canvas 压缩图片的方案

实现

流程

有个前提条件 由于项目中是调用用户相机拍照得到的文件,所以我不需要 check 文件类型

  1. 检查文件大小是否需要压缩

  2. 使用 FileReader 对图片文件进行读取,并转化为 Base64 编码的 dataURL

  3. 用上一步得到的 Base64 编码的图片转化为一个 Image DOM 对象 (因为 canvas 的 drawImage 方法传入的参数只支持 Canvas​Image​Source)

  4. 调用 canvas 接口画出图像并转为压缩后的 dataURL

  5. 将压缩后的文件上传到服务器

图片压缩

file2DataURL 实现

// 图片文件转 Base64 编码
const file2DataURL = (file, callback) => {
  const reader = new FileReader();
  reader.onload = function () {
    const dataURL = this.result;
    callback(dataURL);
  };
  reader.readAsDataURL(file);
};

dataURL2Image 实现

// dataURL 转 img 对象
const dataURL2Image = (dataURL, callback) => {
  const img = new Image();
  img.onload = () => {
    callback(img);
  };
  img.src = dataURL;
};

canvasCompress 实现

// canvas 压缩图片
const canvasCompress = (img, quality, type) => {
  const canvas = document.createElement('canvas');
  const { width, height } = img;
  canvas.width = width;
  canvas.height = height;

  const context =  canvas.getContext('2d');
  context.drawImage(img, 0, 0, width, height);
  const compressedDataURL = canvas.toDataURL(type, quality);
  return compressedDataURL;
};

dataURL2Blob 实现

// dataURL 转 blob 流
const dataURL2Blob = (dataURL, type) => {
  const text = window.atob(dataURL.split(",")[1]);
  const buffer = new ArrayBuffer(text.length);
  const ubuffer = new Uint8Array(buffer);

  for (let i = 0; i < text.length; i++) {
      ubuffer[i] = text.charCodeAt(i);
  }
  const blob = new window.Blob([buffer], { type });
  return blob;
};

compress 实现

/**
 * @param {File} source  图片源文件
 * @param {Number} quality 压缩质量 - 默认不压缩 0 ~ 1
 * @param {Number} maxSize 大小限制 - 默认 1 MB,(1M以上就要压缩)
 */
const compress = (source, callback, quality = 1, maxSize = 1) => {
  // quality 参数错误不压缩直接返回
  if(quality > 1) {
    return callback(source);
  }

  // 单位转换
  maxSize = maxSize * 1024 * 1024;
  // 对 img 参数做简单的判断;
  if(!source || !(source instanceof File)){
    return callback(source);
  }
  // 图片小于限制则不压缩
  if(source.size <= maxSize) {
    return callback(source);
  }

  try {
    const type = source.type;
    file2DataURL(source, (dataURL) => {
      dataURL2Image(dataURL, (img) => {
        const compressedDataURL = canvasCompress(img, quality, type);
        const compressedResult = dataURL2Blob(compressedDataURL, type);
        compressedResult.fileName = source.name;
        callback(compressedResult);
      });
    });
  } catch (e) {
    // process error
  }
};

目前使用回调的方式调用最终看起来会有点乱,大家也可以优化改成 Promise + async await 的方式这样看起来结构会好很多。

参考文档

FileReader

Canvas​Rendering​Context2D.draw​Image()

HTMLCanvas​Element​.toDataURL()

Blob