基于Taro开发,不限图片大小的上传处理(支持H5、微信小程序、抖音小程序等)

515 阅读2分钟
  • 微信小程序处理图片
  1. 图片的压缩处理,可以是后端处理,也可以前端处理。哪为啥上传需要前端处理,下载需要后端处理呢?

    因为,涉及到流量。上传图片太大、下载图片太大,那么流量消耗就大。所以上传需要前端做处理。

  2. 前端如何实现根据上传的图片大小适当压缩呢?

    如:上传20K的图片无需压缩,上传10M的压缩成2M,上传4M的图片,也压缩成2M。这个逻辑如何实现?

  3. 通过一个递归调用,来判断图片是否小于2M,如果满足则提交该图片。如下代码(compressImage.ts):

import Taro from '@tarojs/taro';

export type compressImageResult = {
  resultCode: string;
  filePath?: string;
  errorDesc?: string;
};

/**
 *参数
 * @param oldFilePath 原图地址
 * @param filePath 压缩后的图片地址
 * @param limitSize 图片大小kb
 * @param quality 图片质量
 * @param step 图片质量每次降低多少
 * @param callback 回调
 */
export type compressImageParams = {
  oldFilePath: string;
  filePath?: string;
  limitSize?: number;
  quality?: number;
  step?: number;
  callback: (res: compressImageResult) => void;
};

export const compressImage = (params: compressImageParams) => {
  if (!params.limitSize) {
    params.limitSize = 2048; // 2M 大小
  }
  if (!params.quality) {
    params.quality = 70; // 图片质量 默认70
  }
  if (!params.step) {
    params.step = 5; // 图片质量每次降低多少
  }

  const path = params.filePath ?? params.oldFilePath!;
  Taro.getFileSystemManager().getFileInfo({
    filePath: path,
    success: async res => {
      if (res.size <= 1024 * 1024 * 10) {
        params.callback({
          resultCode: 'FAIL',
          filePath: '',
          errorDesc: '图片超过了10M'
        });

        return;
      }

      // console.log(`图片压缩size:${res.size / 1024}kb`, `quality:${params.quality}`);

      if (res.size > 1024 * params.limitSize!) {
        Taro.compressImage({
          src: params.oldFilePath!,
          quality: params.quality! - params.step!,
          success(result: Taro.compressImage.SuccessCallbackResult) {
            const data: compressImageParams = {
              oldFilePath: params.oldFilePath!,
              filePath: result.tempFilePath,
              limitSize: params.limitSize,
              quality: params.quality! - params.step!,
              step: params.step,
              callback: params.callback
            };

            compressImage(data);
          }
        });
      } else {
        const newPath = params.filePath ?? params.oldFilePath!;
        // console.log(`压缩成功!size:${res.size / 1024}kb`, `quality:${params.quality}`, `path:${newPath}`);
        // const base64 = Taro.getFileSystemManager().readFileSync(filePath);
        params.callback({
          resultCode: 'SUCCESS',
          filePath: newPath,
          errorDesc: ''
        });
      }
    },
    fail(res) {
      params.callback({
        resultCode: 'FAIL',
        filePath: '',
        errorDesc: res.errMsg
      });
    }
  });
};

  • H5处理图片
  1. 上一章节图片处理,使用的是Taro默认的相关API。但是不支持H5?

image.png

  1. H5对于图片的处理,还是要请出canvas对象。实现的逻辑跟上一章节一样,使用递归的逻辑,使图片满足大小要求。

  2. 如下代码(compressImage.ts):


//压缩
const RESULT_ENUM = {
  ORIGIN: 'ORIGIN', // 无需压缩,文件符合大小
  SUCCESS: 'SUCCESS', // 压缩成功
  FAIL: 'FAIL', // 压缩失败, 无法压缩到指定大小
};
//图片压缩
export async function compressImg(
  file: File | undefined,
  maxSize: number,
  reduceCount = 0,
) {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const fileBase64 = await getBase64WithFile(file);
  // console.log(fileBase64,"fileBase64")
  const img = await base64ToImage(fileBase64);
  // console.log(img,"img")
  // 用canvas绘制
  let scale = 1;
  if (reduceCount > 0) {
    scale = 1 / Math.pow(1.2, reduceCount);
  }
  const originWidth = img.width;
  const originHeight = img.height;
  const targetWidth = originWidth * scale;
  const targetHeight = originHeight * scale;
  canvas.width = targetWidth;
  canvas.height = targetHeight;
  ctx?.clearRect(0, 0, targetWidth, targetHeight);
  ctx?.drawImage(img, 0, 0, targetWidth, targetHeight);
  const originBlob = await canvastoBlob(canvas, 'image/jpeg', 1);
  const originSize = originBlob.size / 1024;
  console.log(originBlob, originSize, maxSize, 12321);
  if (originSize <= maxSize) {
    return {
      msg: RESULT_ENUM.ORIGIN,
      base64: canvas.toDataURL('image/jpeg', 1),
    };
  }
  const maxQualitySize = { quality: 100, size: Number.MAX_SAFE_INTEGER };
  const minQualitySize = { quality: 0, size: 0 };
  let quality = 100;
  let count = 0; // 压缩次数
  let compressFinish = false; // 压缩完成
  let compressFail = false;
  let compressBlob;
  let needReduce = false; // 递归压缩
 
  // 二分法最多尝试8次即可覆盖全部可能
  while (!compressFinish && count < 12) {
    const currentQuality = quality / 100;
    compressBlob = await canvastoBlob(canvas, 'image/jpeg', currentQuality);
    const compressSize = compressBlob.size / 1024;
    count++;
    if (compressSize === maxSize) {
      console.log(`压缩完成,总共压缩了${count}次`);
      compressFinish = true;
      const base64 = canvas.toDataURL('image/jpeg', currentQuality);
      return {
        msg: RESULT_ENUM.SUCCESS,
        base64,
      };
    }
    if (compressSize > maxSize) {
      maxQualitySize.quality = quality;
      maxQualitySize.size = compressSize;
    }
    if (compressSize < maxSize) {
      minQualitySize.quality = quality;
      minQualitySize.size = compressSize;
    }
    console.log(
      `第${count}次压缩,压缩后大小${compressSize},quality参数:${quality}`,
    );
 
    quality = Math.ceil((maxQualitySize.quality + minQualitySize.quality) / 2);
 
    if (maxQualitySize.quality - minQualitySize.quality < 2) {
      console.log({ minQualitySize, quality });
      if (!minQualitySize.size && quality) {
        quality = minQualitySize.quality;
      } else if (!minQualitySize.size && !quality) {
        if (quality === 0) {
          needReduce = true;
        }
 
        compressFinish = true;
        compressFail = true;
        console.log(`压缩完成,总共压缩了${count}次`);
      } else if (minQualitySize.size > maxSize) {
        compressFinish = true;
        compressFail = true;
        needReduce = true;
        console.log(`压缩完成,总共压缩了${count}次`);
      } else {
        console.log(`压缩完成,总共压缩了${count}次`);
        compressFinish = true;
        quality = minQualitySize.quality;
      }
    }
  }
 
  // 递归
  if (needReduce) {
    console.log(1, maxSize, reduceCount);
    const nextReduceBlob = await canvastoBlob(canvas, 'image/jpeg', 1 / 100);
    const reduceFile = generateFileFromBlob(nextReduceBlob);
    return await compressImg(reduceFile, maxSize, reduceCount + 1);
  } else if (compressFail) {
    console.log(2);
    return {
      msg: RESULT_ENUM.FAIL,
      base64: fileBase64,
    };
  }
 
  const currentQuality = quality / 100;
 
  compressBlob = await canvastoBlob(canvas, 'image/jpeg', currentQuality);
  const compressSize = compressBlob.size / 1024;
  console.log(
    `最后一次压缩(即第${
      count + 1
    }次),quality为:${quality},大小:${compressSize}`,
  );
 
  const base64 = canvas.toDataURL('image/jpeg', currentQuality);
  return {
    msg: RESULT_ENUM.SUCCESS,
    base64,
  };
}
 
export function generateFileFromBlob(
  compressBlob,
  fileName = new Date().getTime().toString(),
) {
  return new File([compressBlob], fileName, {
    type: 'image/jpeg',
  });
}
 
// canvas转成blob
export function canvastoBlob(canvas, type, quality): Promise<Blob> {
  return new Promise((resolve) =>
    canvas.toBlob((blob) => resolve(blob as Blob), type, quality),
  );
}
 
// file对象转base64
export function getBase64WithFile(file): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result as string);
    reader.onerror = (error) => reject(error);
  });
}
 
// base64转成image
export function base64ToImage(dataURL): Promise<HTMLImageElement> {
  return new Promise((resolve) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.src = dataURL;
  });
}
export const dataURLtoBlob = (dataurl) => {
  const arr = dataurl.split(','),
    mime = arr[0].match(/:(.*?);/)[1],
    bstr = atob(arr[1]);
  let n = bstr.length;
  const u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  return new Blob([u8arr], { type: mime });
};