技术咨询、项目合作、广告投放、简历咨询、技术文档下载 点击这里 联系博主

# 设计一个图片上传系统的大致思路

文章参考自图片上传功能设计的时候需要考虑些什么? (opens new window)

# 先分析

# 上传前

  • 文件格式、大小有限制吗?

  • 一次可以上传多少文件?单张上传?批量上传?是否可以压缩包上传?

  • 异常的文件怎么处理?已经传过的呢(覆盖还是追加)?重名怎么处理?

  • 点击上传吗?是不是可以支持拖拽?复制粘贴上传可以吗?

  • 上传文件需要本地预览吗

# 上传中

  • 上传的文件要压缩吗?压缩规则是什么?(什么时候压缩?怎么压缩?)如果压缩需要存原图吗?

  • 上传进度要显示吗?

  • 上传中可以暂停吗?可以断点续传吗?

  • 文件较大是否支持分片上传?

  • 是否需要进行上传限频?

  • 上传成功或者失败的提示?有失败最大重试次数吗?还是用户无感上传几次之后默认失败?

  • 触发的时候,手动上传还是自动上传?

  • 最大同时支持多少个任务同时上传?服务器支持多少个?

  • 各任务状态如何排序与展示?

# 上传后

  • 可以预览吗?需要缩略图吗?预览的规则是什么?直接按照比例缩放?显示中间部分?

  • 图片是否需要懒加载?

  • 文件存储到哪里?cdn还是自己的服务器?

  • 上传后,支持删除吗?多图情况是否可以一键清除?是否可以重新上传?

  • 是否可以逐个编辑?可以重命名吗?

# 图片点击上传

<input
  type="file"
  id="choose"
  accept="image/gif,image/jpeg,image/jpg,image/png"
/>;

var filechooser = document.getElementById("choose");
filechooser.onchange = function(e) {
  const files = this.files;
  console.log(files);
};

# 图片拖拽上传及进度展示

关于拖放事件有些是在被拖动元素上触发的,而有些则是在放置目标上触发的

当我们拖动某个元素时,会依次触发:

  • dragstart
  • drag
  • dragend

这三个事件都是在被拖动元素上触发的。当拖动开始时会先触发 dragstart 事件,然后在拖动的过程中会持续触发 drag 事件,当拖动停止时(无论被拖动元素是否放到了有效的放置目标)都会触发 dragend 事件

当某个元素被拖动到放置目标上,会依次触发:

  • dragenter
  • dragover
  • dragleave 或 drop

这三个事件都是在放置目标上触发的。当元素进入放置目标时会触发 dragenter 事件,当元素在放置目标上移动时会持续触发 dragover 事件,当元素移出放置目标时会触发 dragleave 事件,当元素被放到了放置目标中会触发 drop 事件而不是 dragleave 事件

在一些浏览器中,当我们移动图片到放置目标上,松开的时候会打开这张图片,如果移动的是超链接,则会打开这个页面。我们有时候需要阻止这种默认的行为;

// 拖动的源事件对象--document

// 监听drop事件,防止浏览器中打开客户端图片
document.ondragover = function(e) {
  // 阻止ondragover的默认行为(触发ondragleave)
  e.preventDefault();
};
document.ondrop = function(e) {
  // 阻止ondrop的默认行为(触发在当前窗口打开客户端图片)
  e.preventDefault();
};
const container = document.getElementById("container");
// 若图片释放此元素上方,则需要在其中显示此图片
container.ondragover = function(e) {
  // 阻止ondragover的默认行为(触发ondragleave)
  e.preventDefault();
};

container.ondrop = function(e) {
  // 读取拖放进来的客户端图片内容
  const files = e.dataTransfer.files;

  // 文件上传: 构造form数据
  const form = new FormData();
  Array.prototype.forEach.call(files, (ele) => {
    // ele包含了如下的信息
    // lastModified: 1623221527820
    // lastModifiedDate: Wed Jun 09 2021 14:52:07 GMT+0800 (中国标准时间) {}
    // name: "1.png"
    // size: 177771
    // type: "image/png"
    // webkitRelativePath: ""
    form.append(ele.name + "_" + Date.now(), ele);
  });

  const xhr = new XMLHttpRequest();
  // 设置超时时间
  xhr.timeout = 3000;
  xhr.open("POST", "http://127.0.0.1/");
  // 文件上传:进度的监听,xhr.upload.onprogress要写在xhr.send方法前面,否则event.lengthComputable状态不会改变,只有在最后一次才能获得,也就是100%的时候.
  xhr.upload.onprogress = function(event) {
    // 这是一个状态,表示发送的长度有了变化,可计算
    if (event.lengthComputable) {
      // 发送了多少字节
      const percent = event.loaded / event.total;
      console.log(percent);
    }
  };
  xhr.onload = () => {
    if (xhr.status === 200 && xhr.readyState === 4) {
      console.log("文件上传成功");
    } else {
      console.log("文件上传失败");
    }
  };
  xhr.send(form);
};

# 图片剪切板上传

var box = document.getElementById("editor-box");
//绑定paste事件
box.addEventListener("paste", function(event) {
  var data = event.clipboardData || window.clipboardData;

  var items = data.items;
  var fileList = []; //存储文件数据
  if (items && items.length) {
    // 检索剪切板items
    for (var i = 0; i < items.length; i++) {
      console.log(items[i].getAsFile());
      fileList.push(items[i].getAsFile());
    }
  }

  event.preventDefault(); //阻止默认行为

  submitUpload(fileList);
});

# 文件取消上传

  • 为取消按钮绑定事件,调用 xhr.abort();终止上传
  • 使用window.URL.createObjectURL 预览图片,在图片加载成功后需要清除使用的内存window.URL.revokeObjectURL(this.src);

# 如何进行图片预览

很多业务场景需要搞定图片/视频上传,同时又要求能够快速预览效果,避免图片或视频稍大或请求响应较慢,使用createObjectURL()本地预览实用性更加。

文章参考自js 图片 视频预览 URL createObjectURL() (opens new window)

读取了本地文件,实现本地预览可以使用createObjectURL或者FileReader.readAsDataURL(file)

他们两者是有区别的:

  • 返回值

    • FileReader.readAsDataURL(file)可以得到一段base64的字符串。
    • URL.createObjectURL(file)可以得到当前文件的一个内存URL。 比如blob:http://localhost:52330/a0c0a1e3-2395-4807-8870-a32589c01f79
  • 内存使用

    • FileReader.readAsDataURL(file)的返回值是转化后的超长base64字符串(长度与要解析的文件大小正相关)。
    • URL.createObjectURL(file)的返回值虽然是字符串,但是是一个url地址。
  • 内存清理

    • FileReader.readAsDataURL(file)依照 JS 垃圾回收机制自动从内存中清理。
    • URL.createObjectURL(file)存在于当前doucment内,清除方式只有unload()事件或revokeObjectURL()手动清除 。
  • 执行机制

    • FileReader.readAsDataURL(file)通过回调的形式返回,异步执行。
    var reader = new FileReader();
    reader.onload = function() {
      callback(reader.result);
    };
    reader.readAsDataURL(file);
    
    • URL.createObjectURL(file)直接返回,同步执行。
  • 兼容性

    • 兼容性兼容IE10以上,其他浏览器均支持。
  • 其他

    • FileReader.readAsDataURL(file)当多个文件同时处理时,需要每一个文件对应一个新的FileReader对象。

      FileReader_error.png

    • URL.createObjectURL(file)依次返回无影响

# 大图片上传之如何进行图片压缩

涉及到 JS 的图片压缩,我的想法是需要用到 Canvas 的绘图能力,通过调整图片的分辨率或者绘图质量来达到图片压缩的效果,实现思路如下:

  • 获取上传 Input 中的图片对象 File
  • 将图片转换成 base64 格式
  • base64 编码的图片通过 Canvas 转换压缩,这里会用到的 Canvas 的 drawImage 以及 toDataURL 这两个 Api,一个调节图片的分辨率的,一个是调节图片压缩质量并且输出的,后续会有详细介绍
  • 转换后的图片生成对应的新图片,然后输出

# drawImage 图像压缩

context.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

简单来讲,此方法将从图像中截取一个矩形区域来画到画板中的一个矩形区域,如果两个矩形区域的数值不一样,将对图像进行缩放,甚至拉伸

就是图片对象,可以是页面上获取的 DOM 对象,也可以是虚拟 DOM 中的图片对象。

sx , sy , swidth , sheight

这 4 个参数是用来裁剪源图片的,表示图片在 canvas 画布上显示的大小和位置。sx,sy 表示在源图片上裁剪位置的 X 轴、Y 轴坐标,然后以 swidth,sheight 尺寸来选择一个区域范围,裁剪出来的图片作为最终在 Canvas 上显示的图片内容( swidth,sheight 不说明的情况下,整个矩形(裁剪)从坐标的 sxsy 开始,到图片的右下角结束)。

dx , dy , dWidth , dHeight

表示在 canvas 画布上规划处一片区域用来放置图片,dx, dy 为绘图位置在 Canvas 元素的 X 轴、Y 轴坐标,dWidth, dHeight 指在 Canvas 元素上绘制图像的宽度和高度(如果不说明, 在绘制时图片的宽度和高度不会缩放)。

以下为图片绘制的实例:

context.drawImage(image, 0, 0, 100, 100);
context.drawImage(image, 300, 300, 200, 200);
context.drawImage(image, 0, 100, 150, 150, 300, 0, 150, 150);

# 图片输出

canvas 提供了两个转图片的方法:

toDataURL

调用 canvas 的 toDataURL 方法可以输出 base64 格式的图片。

canvas.toDataURL(type, encoderOptions);

  • type 可选

图片格式,默认为 image/png。

  • encoderOptions 可选

在指定图片格式为 image/jpeg 或 image/webp 的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。encoderOptions 值越小,所绘制出的图像越模糊

toBlob

void canvas.toBlob(callback, type, encoderOptions);

属于异步方法,所以有个callback 参数。

type 参数指定图片格式;

encoderOptions参数指定图片质量,用于压缩图片

值在 0 与 1 之间,当请求图片格式为image/jpeg或者image/webp时用来指定图片展示质量。

关于 Blob:

Blob (opens new window)对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是 JavaScript 原生格式的数据。File (opens new window) 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。

JavaScript 中 Blob 对象 (opens new window): 这篇文章介绍了 Blob,里面也提到了大文件分割上传的实现。

一般来说,对比 Blob 文件和 base64 ,有下面几点优点:

  • 二进制文件,对后端更友好
  • base64 字符串一般都非常长,会有性能等问题

所以选择转化成 Blob 文件进行上传更好。把 base64 或者 Blob 文件加入 FormData 里就可以实现上传了。

# 遇到的一些坑

  1. PNG 转 JPEG 时 PNG 格式的透明区域会变黑色,需要先手动铺底色
  2. toDataURL 参数为 PNG 时不支持传图片质量,所以需要写死 image/jpeg 或 image/webp,具体可以参考 toDataURL 的 api
  3. formData.append 第三个参数 filename 是有浏览器兼容性问题的,如果不传就是 filename=blob,后端校验文件名可能过不去
  4. ajax 的 contentType 和 processData 需要传 false,这和本文关系不大直接带过

# 一个例子

// 图片压缩
function imageCompress(image, callback) {
  const canvas = document.createElement("canvas");
  const context = canvas.getContext("2d");
  // 定义 canvas 大小,也就是压缩后下载的图片大小
  let imageWidth = image.width; //压缩后图片的大小
  let imageHeight = image.height;
  canvas.width = imageWidth;
  canvas.height = imageHeight;

  // 图片不压缩,全部加载展示
  context.drawImage(image, 0, 0);
  // 图片按压缩尺寸载入
  // let imageWidth = 500; //压缩后图片的大小
  // let imageHeight = 200;
  // context.drawImage(image, 0, 0, 500, 200);
  // 图片去截取指定位置载入
  // context.drawImage(image,100, 100, 100, 100, 0, 0, imageWidth, imageHeight);

  // 如果要求直接上传blob则使用toBlob,如果需要展示则使用toDataURL
  canvas.toBlob(
    function(blob) {
      callback && callback(blob);
    },
    "image/jpeg",
    0.5 // 自己调整质量
  );
}
// 文件转换成图片
function filesToImage(file) {
  return new Promise((resolve, reject) => {
    const image = new Image();
    image.src = URL.createObjectURL(file);
    image.onload = function() {
      imageCompress(this, (blob) => {
        resolve(blob);
      });
    };
    image.onerror = () => {
      reject();
    };
  });
}

var filechooser = document.getElementById("choose");
filechooser.onchange = function(e) {
  const files = this.files;
  const images = [];
  Array.prototype.forEach.call(files, (ele) => {
    console.log(ele.size, "原始文件大小");
    // ele包含了如下的信息
    // lastModified: 1623221527820
    // lastModifiedDate: Wed Jun 09 2021 14:52:07 GMT+0800 (中国标准时间) {}
    // name: "1.png"
    // size: 177771
    // type: "image/png"
    // webkitRelativePath: ""
    images.push(filesToImage(ele));
  });
  Promise.all(images).then((res) => {
    // res为所有的图片的blob,如果上传使用File方式则需要将blob转换成file
    //     let files = new window.File([this.blob], file.name, {type: file.type})
    console.log(res);
    console.log(res[0].size, "压缩后文件大小");
  });
};

# 大图片之分片

相信大家都对Blob 对象有所了解,它表示原始数据,也就是二进制数据,同时提供了对数据截取的方法slice,而 File 继承了Blob的功能,所以可以直接使用此方法对数据进行分段截图。

  • 把大文件进行分段 比如 2M,发送到服务器携带一个标志,暂时用当前的时间戳,用于标识一个完整的文件
  • 服务端保存各段文件
  • 浏览器端所有分片上传完成,发送给服务端一个合并文件的请求
  • 服务端根据文件标识、类型、各分片顺序进行文件合并
  • 删除分片文件

代码参考自掘金 (opens new window)

function submitUpload() {
  var chunkSize = 2 * 1024 * 1024; //分片大小 2M
  var file = document.getElementById("f1").files[0];
  var chunks = [], //保存分片数据
    token = +new Date(), //时间戳
    name = file.name,
    chunkCount = 0,
    sendChunkCount = 0;

  //拆分文件 像操作字符串一样
  if (file.size > chunkSize) {
    //拆分文件
    var start = 0,
      end = 0;
    while (true) {
      end += chunkSize;
      var blob = file.slice(start, end);
      start += chunkSize;

      if (!blob.size) {
        //截取的数据为空 则结束
        //拆分结束
        break;
      }

      chunks.push(blob); //保存分段数据
    }
  } else {
    chunks.push(file.slice(0));
  }

  chunkCount = chunks.length; //分片的个数

  //没有做并发限制,较大文件导致并发过多,tcp 链接被占光 ,需要做下并发控制,比如只有4个在请求在发送

  for (var i = 0; i < chunkCount; i++) {
    var fd = new FormData(); //构造FormData对象
    fd.append("token", token);
    fd.append("f1", chunks[i]);
    fd.append("index", i);
    xhrSend(fd, function() {
      sendChunkCount += 1;
      if (sendChunkCount === chunkCount) {
        //上传完成,发送合并请求
        console.log("上传完成,发送合并请求");
        var formD = new FormData();
        formD.append("type", "merge");
        formD.append("token", token);
        formD.append("chunkCount", chunkCount);
        formD.append("filename", name);
        xhrSend(formD);
      }
    });
  }
}

function xhrSend(fd, cb) {
  var xhr = new XMLHttpRequest(); //创建对象
  xhr.open("POST", "http://localhost:8100/", true);
  xhr.onreadystatechange = function() {
    console.log("state change", xhr.readyState);
    if (xhr.readyState == 4) {
      console.log(xhr.responseText);
      cb && cb();
    }
  };
  xhr.send(fd); //发送
}

# NodeJS生成预览图

尝试着使用gm (opens new window)

【未经作者允许禁止转载】 Last Updated: 9/10/2024, 6:03:53 AM