• 文件上传,8种场景


    ------------恢复内容开始------------

    • 单文件上传:利用 input 元素的 accept 属性限制上传文件的类型、利用 JS 检测文件的类型及使用 Koa 实现单文件上传的功能;

    • 多文件上传:利用 input 元素的 multiple 属性支持选择多文件及使用 Koa 实现多文件上传的功能;

    • 目录上传:利用 input 元素上的 webkitdirectory 属性支持目录上传的功能及使用 Koa 实现目录上传并按文件目录结构存放的功能;

    • 压缩目录上传:在目录上传的基础上,利用 JSZip 实现压缩目录上传的功能;

    • 拖拽上传:利用拖拽事件和 DataTransfer 对象实现拖拽上传的功能;

    • 剪贴板上传:利用剪贴板事件和 Clipboard API 实现剪贴板上传的功能;

    • 大文件分块上传:利用 Blob.sliceSparkMD5 和第三方库 async-pool 实现大文件并发上传的功能;

    • 服务端上传:利用第三方库 form-data 实现服务端文件流式上传的功能。


    一、单文件上传

    对于单文件上传的场景来说,最常见的是图片上传的场景,所以我们就以图片上传为例,先来介绍单文件上传的基本流程。

    1.1 前端代码

    html

    在以下代码中,我们通过 input 元素的 accept 属性限制了上传文件的类型。这里使用 image/* 限制只能选择图片文件,当然你也可以设置特定的类型,比如 image/pngimage/png,image/jpeg

    <input id="uploadFile" type="file" accept="image/*" />
    <button id="submit" onclick="uploadFile()">上传文件</button>

    需要注意的是,虽然我们把 input 元素的 accept 属性设置为 image/png。但如果用户把 jpg/jpeg 格式的图片后缀名改为 .png,就可以成功绕过这个限制。要解决这个问题,我们可以通过读取文件中的二进制数据来识别正确的文件类型。

    要查看图片对应的二进制数据,我们可以借助一些现成的编辑器,比如 Windows 平台下的 WinHex 或 macOS 平台下的 Synalyze It! Pro 十六进制编辑器。这里我们使用 Synalyze It! Pro 这个编辑器,来查看阿宝哥头像对应的二进制数据。

    const uploadFileEle = document.querySelector("#uploadFile");
    
    const request = axios.create({
      baseURL: "http://localhost:3000/upload",
      timeout: 60000, 
    });
    
    async function uploadFile() {
      if (!uploadFileEle.files.length) return;
      const file = uploadFileEle.files[0]; // 获取单个文件
      // 省略文件的校验过程,比如文件类型、大小校验
      upload({
        url: "/single",
        file,
      });
    }
    
    function upload({ url, file, fieldName = "file" }) {
      let formData = new FormData();
      formData.set(fieldName, file);
      request.post(url, formData, {
        // 监听上传进度
        onUploadProgress: function (progressEvent) {
          const percentCompleted = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
          );
          console.log(percentCompleted);
         },
      });
    }

    在以上代码中,我们先把读取的 File 对象封装成 FormData 对象,然后利用 Axios 实例的 post 方法实现文件上传的功能。 在上传前,通过设置请求配置对象的 onUploadProgress 属性,就可以获取文件的上传进度。

    二、多文件上传

    要上传多个文件,首先我们需要允许用户同时选择多个文件。要实现这个功能,我们可以利用 input 元素的 multiple 属性。跟前面介绍的 accept 属性一样,该属性也存在兼容性问题,具体如下图所示:

    2.1 前端代码

    html

    相比单文件上传的代码,多文件上传场景下的 input 元素多了一个 multiple 属性:

    <input id="uploadFile" type="file" accept="image/*" multiple />
    <button id="submit" onclick="uploadFile()">上传文件</button>

    js

    在单文件上传的代码中,我们通过 uploadFileEle.files[0] 获取单个文件,而对于多文件上传来说,我们需要获取已选择的文件列表,即通过 uploadFileEle.files 来获取,它返回的是一个 FileList 对象。

    async function uploadFile() {
      if (!uploadFileEle.files.length) return;
      const files = Array.from(uploadFileEle.files);
      upload({
        url: "/multiple",
        files,
      });
    }

    因为要支持上传多个文件,所以我们需要同步更新一下 upload 函数。对应的处理逻辑就是遍历文件列表,然后使用 FormData 对象的 append 方法来添加多个文件,具体代码如下所示:

    function upload({ url, files, fieldName = "file" }) {
      let formData = new FormData();
      files.forEach((file) => {
        formData.append(fieldName, file);
      });
      request.post(url, formData, {
        // 监听上传进度
        onUploadProgress: function (progressEvent) {
          const percentCompleted = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
          );
          console.log(percentCompleted);
        },
      });
    }

    三、目录上传

    可能你还不知道,input 元素上还有一个的 webkitdirectory 属性。当设置了 webkitdirectory 属性之后,我们就可以选择目录了。

    <input id="uploadFile" type="file" accept="image/*" webkitdirectory />

    当我们选择了指定目录之后,比如阿宝哥桌面上的 images 目录,就会显示以下确认框:

    点击上传按钮之后,我们就可以获取文件列表。列表中的文件对象上含有一个 webkitRelativePath 属性,用于表示当前文件的相对路径。

    3.1 前端代码

    为了让服务端能按照实际的目录结构来存放对应的文件,在添加表单项时我们需要把当前文件的路径提交到服务端。此外,为了确保@koa/multer 能正确处理文件的路径,我们需要对路径进行特殊处理。即把 / 斜杠替换为 @ 符号。对应的处理方式如下所示:

    function upload({ url, files, fieldName = "file" }) {
      let formData = new FormData();
      files.forEach((file, i) => {
        formData.append(
          fieldName, 
          files[i],
          files[i].webkitRelativePath.replace(///g, "@");
        );
      });
      request.post(url, formData); // 省略上传进度处理
    }

    四、压缩目录上传

    JavaScript 如何在线解压 ZIP 文件? 这篇文章中,介绍了在浏览器端如何使用 JSZip 这个库实现在线解压 ZIP 文件的功能。 JSZip 这个库除了可以解析 ZIP 文件之外,它还可以用来 创建和编辑 ZIP 文件。利用 JSZip 这个库提供的 API,我们就可以把目录下的所有文件压缩成 ZIP 文件,然后再把生成的 ZIP 文件上传到服务器。

    4.1 前端代码

    JSZip 实例上的 file(name, data [,options]) 方法,可以把文件添加到 ZIP 文件中。基于该方法我们可以封装了一个 generateZipFile 函数,用于把目录下的文件列表压缩成一个 ZIP 文件。以下是 generateZipFile 函数的具体实现:

    function generateZipFile(
      zipName, files,
      options = { type: "blob", compression: "DEFLATE" }
    ) {
      return new Promise((resolve, reject) => {
        const zip = new JSZip();
        for (let i = 0; i < files.length; i++) {
          zip.file(files[i].webkitRelativePath, files[i]);
        }
        zip.generateAsync(options).then(function (blob) {
          zipName = zipName || Date.now() + ".zip";
          const zipFile = new File([blob], zipName, {
            type: "application/zip",
          });
          resolve(zipFile);
        });
      });
    }

    在创建完 generateZipFile 函数之后,我们需要更新一下前面已经介绍过的 uploadFile 函数:

    async function uploadFile() {
      let fileList = uploadFileEle.files;
      if (!fileList.length) return;
      let webkitRelativePath = fileList[0].webkitRelativePath;
      let zipFileName = webkitRelativePath.split("/")[0] + ".zip";
      let zipFile = await generateZipFile(zipFileName, fileList);
      upload({
        url: "/single",
        file: zipFile,
        fileName: zipFileName
      });
    }
    在以上的 uploadFile 函数中,我们会对返回的 FileList 对象进行处理,即调用 generateZipFile 函数来生成 ZIP 文件。此外,为了在服务端接收压缩文件时,能获取到文件名,我们为 upload 函数增加了一个 fileName 参数,该参数用于调用 formData.append 方法时,设置上传文件的文件名:
    function upload({ url, file, fileName, fieldName = "file" }) {
      if (!url || !file) return;
      let formData = new FormData();
      formData.append(
        fieldName, file, fileName
      );
      request.post(url, formData); // 省略上传进度跟踪
    }

    五、拖拽上传

    要实现拖拽上传的功能,我们需要先了解与拖拽相关的事件。比如 dragdragenddragenterdragoverdrop 事件等。这里我们只介绍接下来要用到的拖拽事件:

    • dragenter:当拖拽元素或选中的文本到一个可释放目标时触发;
    • dragover:当元素或选中的文本被拖到一个可释放目标上时触发(每100毫秒触发一次);
    • dragleave:当拖拽元素或选中的文本离开一个可释放目标时触发;
    • drop:当元素或选中的文本在可释放目标上被释放时触发。

    基于上面的这些事件,我们就可以提高用户拖拽的体验。比如当用户拖拽的元素进入目标区域时,对目标区域进行高亮显示。当用户拖拽的元素离开目标区域时,移除高亮显示。很明显当 drop 事件触发后,拖拽的元素已经放入目标区域了,这时我们就需要获取对应的数据。

    那么如何获取拖拽对应的数据呢?这时我们需要使用 DataTransfer 对象,该对象用于保存拖动并放下过程中的数据。它可以保存一项或多项数据,这些数据项可以是一种或者多种数据类型。若拖动操作涉及拖动文件,则我们可以通过 DataTransfer 对象的 files 属性来获取文件列表。

    介绍完拖拽上传相关的知识后,我们来看一下具体如何实现拖拽上传的功能。

    5.1 前端代码

    html

    <div id="dropArea">
       <p>拖拽上传文件</p>
       <div id="imagePreview"></div>
    </div>

    css

    #dropArea {
      width: 300px;
      height: 300px;
      border: 1px dashed gray;
      margin-bottom: 20px;
    }
    #dropArea p {
      text-align: center;
      color: #999;
    }
    #dropArea.highlighted {
      background-color: #ddd;
    }
    #imagePreview {
      max-height: 250px;
      overflow-y: scroll;
    }
    #imagePreview img {
      width: 100%;
      display: block;
      margin: auto;
    }

    js

    为了让大家能够更好地阅读拖拽上传的相关代码,我们把代码拆成 4 部分来讲解:

    1、阻止默认拖拽行为

    const dropAreaEle = document.querySelector("#dropArea");
    const imgPreviewEle = document.querySelector("#imagePreview");
    const IMAGE_MIME_REGEX = /^image/(jpe?g|gif|png)$/i;
    
    ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
       dropAreaEle.addEventListener(eventName, preventDefaults, false);
       document.body.addEventListener(eventName, preventDefaults, false);
    });
    
    function preventDefaults(e) {
      e.preventDefault();
      e.stopPropagation();
    }

    2、切换目标区域的高亮状态

    ["dragenter", "dragover"].forEach((eventName) => {
        dropAreaEle.addEventListener(eventName, highlight, false);
    });
    ["dragleave", "drop"].forEach((eventName) => {
        dropAreaEle.addEventListener(eventName, unhighlight, false);
    });
    
    // 添加高亮样式
    function highlight(e) {
      dropAreaEle.classList.add("highlighted");
    }
    
    // 移除高亮样式
    function unhighlight(e) {
      dropAreaEle.classList.remove("highlighted");
    }

    3、处理图片预览

    dropAreaEle.addEventListener("drop", handleDrop, false);
    
    function handleDrop(e) {
      const dt = e.dataTransfer;
      const files = [...dt.files];
      files.forEach((file) => {
        previewImage(file, imgPreviewEle);
      });
      // 省略文件上传代码
    }
    
    function previewImage(file, container) {
      if (IMAGE_MIME_REGEX.test(file.type)) {
        const reader = new FileReader();
        reader.onload = function (e) {
          let img = document.createElement("img");
          img.src = e.target.result;
          container.append(img);
        };
        reader.readAsDataURL(file);
      }
    }

    4、文件上传

    function handleDrop(e) {
      const dt = e.dataTransfer;
      const files = [...dt.files];
      // 省略图片预览代码
      files.forEach((file) => {
        upload({
          url: "/single",
          file,
        });
      });
    }
    
    const request = axios.create({
      baseURL: "http://localhost:3000/upload",
      timeout: 60000,
    });
    
    function upload({ url, file, fieldName = "file" }) {
      let formData = new FormData();
      formData.set(fieldName, file);
      request.post(url, formData, {
        // 监听上传进度
        onUploadProgress: function (progressEvent) {
          const percentCompleted = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
          );
          console.log(percentCompleted);
        },
      });
    }

    拖拽上传算是一个比较常见的场景,很多成熟的上传组件都支持该功能。其实除了拖拽上传外,还可以利用剪贴板实现复制上传的功能。

    六、剪贴板上传

    在介绍如何实现剪贴板上传的功能前,我们需要了解一下 Clipboard API。Clipboard 接口实现了 Clipboard API,如果用户授予了相应的权限,就能提供系统剪贴板的读写访问。在 Web 应用程序中,Clipboard API 可用于实现剪切、复制和粘贴功能。该 API 用于取代通过 document.execCommand API 来实现剪贴板的操作。

    在实际项目中,我们不需要手动创建 Clipboard 对象,而是通过 navigator.clipboard 来获取 Clipboard 对象:

    在获取 Clipboard 对象之后,我们就可以利用该对象提供的 API 来访问剪贴板,比如:

    navigator.clipboard.readText().then(
      clipText => document.querySelector(".editor").innerText = clipText
    );

    以上代码将 HTML 中含有 .editor 类的第一个元素的内容替换为剪贴板的内容。如果剪贴板为空,或者不包含任何文本,则元素的内容将被清空。这是因为在剪贴板为空或者不包含文本时,readText 方法会返回一个空字符串。

    要实现剪贴板上传的功能,可以分为以下 3 个步骤:

    • 监听容器的粘贴事件;
    • 读取并解析剪贴板中的内容;
    • 动态构建 FormData 对象并上传。

    了解完上述步骤,接下来我们来分析一下具体实现的代码。

    6.1 前端代码

    html

    <div id="uploadArea">
       <p>请先复制图片后再执行粘贴操作</p>
    </div>

    css

    #uploadArea {
       width: 400px;
       height: 400px;
       border: 1px dashed gray;
       display: table-cell;
       vertical-align: middle;
    }
    #uploadArea p {
       text-align: center;
       color: #999;
    }
    #uploadArea img {
       max-width: 100%;
       max-height: 100%;
       display: block;
       margin: auto;
    }

    js

    在以下代码中,我们使用 addEventListener 方法为 uploadArea 容器添加 paste 事件。在对应的事件处理函数中,我们会优先判断当前浏览器是否支持异步 Clipboard API。如果支持的话,就会通过 navigator.clipboard.read 方法来读取剪贴板中的内容。在读取内容之后,我们会通过正则判断剪贴板项中是否包含图片资源,如果有的话会调用 previewImage 方法执行图片预览操作并把返回的 blob 对象保存起来,用于后续的上传操作。

    const IMAGE_MIME_REGEX = /^image/(jpe?g|gif|png)$/i;
    const uploadAreaEle = document.querySelector("#uploadArea");
    
    uploadAreaEle.addEventListener("paste", async (e) => {
      e.preventDefault();
      const files = [];
      if (navigator.clipboard) {
        let clipboardItems = await navigator.clipboard.read();
        for (const clipboardItem of clipboardItems) {
          for (const type of clipboardItem.types) {
            if (IMAGE_MIME_REGEX.test(type)) {
               const blob = await clipboardItem.getType(type);
               insertImage(blob, uploadAreaEle);
               files.push(blob);
             }
           }
         }
      } else {
          const items = e.clipboardData.items;
          for (let i = 0; i < items.length; i++) {
            if (IMAGE_MIME_REGEX.test(items[i].type)) {
              let file = items[i].getAsFile();
              insertImage(file, uploadAreaEle);
              files.push(file);
            }
          }
      }
      if (files.length > 0) {
        confirm("剪贴板检测到图片文件,是否执行上传操作?") 
          && upload({
               url: "/multiple",
               files,
             });
       }
    });

    若当前浏览器不支持异步 Clipboard API,则我们会尝试通过 e.clipboardData.items 来访问剪贴板中的内容。需要注意的是,在遍历剪贴板内容项的时候,我们是通过 getAsFile 方法来获取剪贴板的内容。

    前面已经提到,当从剪贴板解析到图片资源时,会让用户进行预览,该功能是基于 FileReader API 来实现的,对应的代码如下所示:

    function previewImage(file, container) {
      const reader = new FileReader();
      reader.onload = function (e) {
        let img = document.createElement("img");
        img.src = e.target.result;
        container.append(img);
      };
      reader.readAsDataURL(file);
    }

    当用户预览完成后,如果确认上传我们就会执行文件的上传操作。因为文件是从剪贴板中读取的,所以在上传前我们会根据文件的类型,自动为它生成一个文件名,具体是采用时间戳加文件后缀的形式:

    function upload({ url, files, fieldName = "file" }) {
      let formData = new FormData();
      files.forEach((file) => {
        let fileName = +new Date() + "." + IMAGE_MIME_REGEX.exec(file.type)[1];
        formData.append(fieldName, file, fileName);
      });
      request.post(url, formData);
    }

    前面我们已经介绍了文件上传的多种不同场景,接下来我们来介绍一个 “特殊” 的场景 —— 大文件上传。

    七、大文件分块上传

    相信你可能已经了解大文件上传的解决方案,在上传大文件时,为了提高上传的效率,我们一般会使用 Blob.slice 方法对大文件按照指定的大小进行切割,然后通过多线程进行分块上传,等所有分块都成功上传后,再通知服务端进行分块合并。具体处理方案如下图所示:

    因为在 JavaScript 中如何实现大文件并发上传? 这篇文章中,阿宝哥已经详细介绍了大文件并发上传的方案,所以这里就不展开介绍了。我们只回顾一下大文件并发上传的完整流程:

  • 相关阅读:
    Spring Boot任务管理之定时任务
    Spring Boot任务管理之有返回值异步任务调用
    Spring Boot任务管理之无返回值异步任务调用
    Spring Boot整合Thymeleaf
    IDEA配置maven详细教程
    虚拟机运行centos设置固定IP
    django.db.utils.IntegrityError: (1048, "Column 'spu_id' cannot be null")关于RESTframework使用序列化器报错问题
    RuntimeWarning: DateTimeField User.date_joined received a naive datetime (2020-08-01 00:00:00) while time zone support is active. warnings.warn("DateTimeField %s.%s received a naive datetime "问题
    ModuleNotFoundError: No module named 'PIL'问题
    ImportError: cannot import name 'Feature' from 'setuptools' (D:python_learnmeiduo_projectenvlibsite-packagessetuptools\__init__.py)问题
  • 原文地址:https://www.cnblogs.com/magicg/p/15457342.html
Copyright © 2020-2023  润新知