文件上传,搞懂这8种场景就够了

本文转载自微信公众号「全栈修仙之路」,作者阿宝哥 。转载本文请联系全栈修仙之路公众号。

专注于为中小企业提供网站设计制作、成都网站建设服务,电脑端+手机端+微信端的三站合一,更高效的管理,为中小企业汪清免费做网站提供优质的服务。我们立足成都,凝聚了一批互联网行业人才,有力地推动了成百上千家企业的稳健成长,帮助中小企业通过网站建设实现规模扩充和转变。

在日常工作中,文件上传是一个很常见的功能。在项目开发过程中,我们通常都会使用一些成熟的上传组件来实现对应的功能。一般来说,成熟的上传组件不仅会提供漂亮 UI 或好的交互体验,而且还会提供多种不同的上传方式,以满足不同的场景需求。

一般在我们工作中,主要会涉及到 8 种文件上传的场景,每一种场景背后都使用不同的技术,其中也有很多细节需要我们额外注意。今天阿宝哥就来带大家总结一下这 8 种场景,让大家能更好地理解成熟上传组件所提供的功能。阅读本文后,你将会了解以下的内容:

  • 单文件上传:利用 input 元素的 accept 属性限制上传文件的类型、利用 JS 检测文件的类型及使用 Koa 实现单文件上传的功能;
  • 多文件上传:利用 input 元素的 multiple 属性支持选择多文件及使用 Koa 实现多文件上传的功能;
  • 目录上传:利用 input 元素上的 webkitdirectory 属性支持目录上传的功能及使用 Koa 实现目录上传并按文件目录结构存放的功能;
  • 压缩目录上传:在目录上传的基础上,利用 JSZip 实现压缩目录上传的功能;
  • 拖拽上传:利用拖拽事件和 DataTransfer 对象实现拖拽上传的功能;
  • 剪贴板上传:利用剪贴板事件和 Clipboard API 实现剪贴板上传的功能;
  • 大文件分块上传:利用 Blob.slice、SparkMD5 和第三方库 async-pool 实现大文件并发上传的功能;
  • 服务端上传:利用第三方库 form-data 实现服务端文件流式上传的功能。

一、单文件上传

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

1.1 前端代码

html

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

 
 
 
 
  1.  
  2. 上传文件 

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

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

那么在前端能否不借助工具,读取文件的二进制数据呢?答案是可以的,这里阿宝哥就不展开介绍了。感兴趣的话,你可以阅读 JavaScript 如何检测文件的类型? 这篇文章。另外,需要注意的是 input 元素 accept 属性有存在兼容性问题。比如 IE 9 以下不支持,具体如下图所示:

(图片来源 —— https://caniuse.com/input-file-accept)

js

 
 
 
 
  1. const uploadFileEle = document.querySelector("#uploadFile"); 
  2.  
  3. const request = axios.create({ 
  4.   baseURL: "http://localhost:3000/upload", 
  5.   timeout: 60000,  
  6. }); 
  7.  
  8. async function uploadFile() { 
  9.   if (!uploadFileEle.files.length) return; 
  10.   const file = uploadFileEle.files[0]; // 获取单个文件 
  11.   // 省略文件的校验过程,比如文件类型、大小校验 
  12.   upload({ 
  13.     url: "/single", 
  14.     file, 
  15.   }); 
  16.  
  17. function upload({ url, file, fieldName = "file" }) { 
  18.   let formData = new FormData(); 
  19.   formData.set(fieldName, file); 
  20.   request.post(url, formData, { 
  21.     // 监听上传进度 
  22.     onUploadProgress: function (progressEvent) { 
  23.       const percentCompleted = Math.round( 
  24.         (progressEvent.loaded * 100) / progressEvent.total 
  25.       ); 
  26.       console.log(percentCompleted); 
  27.      }, 
  28.   }); 

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

1.2 服务端代码

Koa 是一个简单易用的 Web 框架,它的特点是优雅、简洁、轻量、自由度高。所以我们选择它来搭建文件服务,并使用以下中间件来实现相应的功能:

  • koa-static:处理静态资源的中间件;
  • @koa/cors:处理跨域请求的中间件;
  • @koa/multer:处理 multipart/form-data 的中间件;
  • @koa/router:处理路由的中间件。
 
 
 
 
  1. const path = require("path"); 
  2. const Koa = require("koa"); 
  3. const serve = require("koa-static"); 
  4. const cors = require("@koa/cors"); 
  5. const multer = require("@koa/multer"); 
  6. const Router = require("@koa/router"); 
  7.  
  8. const app = new Koa(); 
  9. const router = new Router(); 
  10. const PORT = 3000; 
  11. // 上传后资源的URL地址 
  12. const RESOURCE_URL = `http://localhost:${PORT}`; 
  13. // 存储上传文件的目录 
  14. const UPLOAD_DIR = path.join(__dirname, "/public/upload"); 
  15.  
  16. const storage = multer.diskStorage({ 
  17.   destination: async function (req, file, cb) { 
  18.     // 设置文件的存储目录 
  19.     cb(null, UPLOAD_DIR); 
  20.   }, 
  21.   filename: function (req, file, cb) { 
  22.     // 设置文件名 
  23.     cb(null, `${file.originalname}`); 
  24.   }, 
  25. }); 
  26.  
  27. const multerUpload = multer({ storage }); 
  28.  
  29. router.get("/", async (ctx) => { 
  30.   ctx.body = "欢迎使用文件服务(by 阿宝哥)"; 
  31. }); 
  32.  
  33. router.post( 
  34.   "/upload/single", 
  35.   async (ctx, next) => { 
  36.     try { 
  37.       await next(); 
  38.       ctx.body = { 
  39.         code: 1, 
  40.         msg: "文件上传成功", 
  41.         url: `${RESOURCE_URL}/${ctx.file.originalname}`, 
  42.       }; 
  43.     } catch (error) { 
  44.       ctx.body = { 
  45.         code: 0, 
  46.         msg: "文件上传失败" 
  47.       }; 
  48.     } 
  49.   }, 
  50.   multerUpload.single("file") 
  51. ); 
  52.  
  53. // 注册中间件 
  54. app.use(cors()); 
  55. app.use(serve(UPLOAD_DIR)); 
  56. app.use(router.routes()).use(router.allowedMethods()); 
  57.  
  58. app.listen(PORT, () => { 
  59.   console.log(`app starting at port ${PORT}`); 
  60. }); 

以上代码相对比较简单,我们就不展开介绍了。Koa 内核很简洁,扩展功能都是通过中间件来实现。比如示例中使用到的路由、CORS、静态资源处理等功能都是通过中间件实现。因此要想掌握 Koa 这个框架,核心是掌握它的中间件机制。如果你想深入了解的话,可以阅读 如何更好地理解中间件和洋葱模型 这篇文章。其实除了单文件上传外,在文件上传的场景中,我们也可以同时上传多个文件。

单文件上传示例:single-file-upload

https://github.com/semlinker/file-upload-demos/tree/master/single-file-upload

二、多文件上传

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

(图片来源 —— https://caniuse.com/mdn-api_htmlinputelement_multiple)

2.1 前端代码

html

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

 
 
 
 
  1.  
  2. 上传文件 

js

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

 
 
 
 
  1. async function uploadFile() { 
  2.   if (!uploadFileEle.files.length) return; 
  3.   const files = Array.from(uploadFileEle.files); 
  4.   upload({ 
  5.     url: "/multiple", 
  6.     files, 
  7.   }); 

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

 
 
 
 
  1. function upload({ url, files, fieldName = "file" }) { 
  2.   let formData = new FormData(); 
  3.   files.forEach((file) => { 
  4.     formData.append(fieldName, file); 
  5.   }); 
  6.   request.post(url, formData, { 
  7.     // 监听上传进度 
  8.     onUploadProgress: function (progressEvent) { 
  9.       const percentCompleted = Math.round( 
  10.         (progressEvent.loaded * 100) / progressEvent.total 
  11.       ); 
  12.       console.log(percentCompleted); 
  13.     }, 
  14.   }); 

2.2 服务端代码

在以下代码中,我们定义了一个新的路由 —— /upload/multiple 来处理多文件上传的功能。当所有文件都成功上传后,就会返回一个已上传文件的 url 地址列表:

 
 
 
 
  1. router.post( 
  2.   "/upload/multiple", 
  3.   async (ctx, next) => { 
  4.     try { 
  5.       await next(); 
  6.       urls = ctx.files.file.map(file => `${RESOURCE_URL}/${file.originalname}`); 
  7.       ctx.body = { 
  8.         code: 1, 
  9.         msg: "文件上传成功", 
  10.         urls 
  11.       }; 
  12.     } catch (error) { 
  13.       ctx.body = { 
  14.         code: 0, 
  15.         msg: "文件上传失败", 
  16.       }; 
  17.     } 
  18.   }, 
  19.   multerUpload.fields([ 
  20.     { 
  21.       name: "file", // 与FormData表单项的fieldName相对应 
  22.     }, 
  23.   ]) 
  24. ); 

介绍完单文件和多文件上传的功能,接下来我们来介绍目录上传的功能。

多文件上传示例:multiple-file-upload

https://github.com/semlinker/file-upload-demos/tree/master/multiple-file-upload

三、目录上传

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

 
 
 
 
  1.  

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

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

虽然通过 webkitdirectory 属性可以很容易地实现选择目录的功能,但在实际项目中我们还需要考虑它的兼容性。比如在 IE 11 以下的版本就不支持该属性,其它浏览器的兼容性如下图所示:

(图片来源 —— https://caniuse.com/?search=webkitdirectory)

了解完 webkitdirectory 属性的兼容性,我们先来介绍前端的实现代码。

3.1 前端代码

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

 
 
 
 
  1. function upload({ url, files, fieldName = "file" }) { 
  2.   let formData = new FormData(); 
  3.   files.forEach((file, i) => { 
  4.     formData.append( 
  5.       fieldName,  
  6.       files[i], 
  7.       files[i].webkitRelativePath.replace(/\//g, "@"); 
  8.     ); 
  9.   }); 
  10.   request.post(url, formData); // 省略上传进度处理 

3.2 服务端代码

目录上传与多文件上传,服务端代码的主要区别就是 @koa/multer 中间件的配置对象不一样。在 destination 属性对应的函数中,我们需要把文件名中 @ 还原成 /,然后根据文件的实际路径来生成目录。

 
 
 
 
  1. const fse = require("fs-extra"); 
  2. const storage = multer.diskStorage({ 
  3.   destination: async function (req, file, cb) { 
  4.     // images@image-1.jpeg => images/image-1.jpeg 
  5.     let relativePath = file.originalname.replace(/@/g, path.sep); 
  6.     let index = relativePath.lastIndexOf(path.sep); 
  7.     let fileDir = path.join(UPLOAD_DIR, relativePath.substr(0, index)); 
  8.     // 确保文件目录存在,若不存在的话,会自动创建 
  9.     await fse.ensureDir(fileDir);  
  10.     cb(null, fileDir); 
  11.   }, 
  12.   filename: function (req, file, cb) { 
  13.     let parts = file.originalname.split("@"); 
  14.     cb(null, `${parts[parts.length - 1]}`);  
  15.   }, 
  16. }); 

现在我们已经实现了目录上传的功能,那么能否把目录下的文件压缩成一个压缩包后再上传呢?答案是可以的,接下来我们来介绍如何实现压缩目录上传的功能。

目录上传示例:directory-upload

https://github.com/semlinker/file-upload-demos/tree/master/directory-upload

四、压缩目录上传

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

4.1 前端代码

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

 
 
 
 
  1. function generateZipFile( 
  2.   zipName, files, 
  3.   options = { type: "blob", compression: "DEFLATE" } 
  4. ) { 
  5.   return new Promise((resolve, reject) => { 
  6.     const zip = new JSZip(); 
  7.     for (let i = 0; i < files.length; i++) { 
  8.       zip.file(files[i].webkitRelativePath, files[i]); 
  9.     } 
  10.     zip.generateAsync(options).then(function (blob) { 
  11.       zipName = zipName || Date.now() + ".zip"; 
  12.       const zipFile = new File([blob], zipName, { 
  13.         type: "application/zip", 
  14.       }); 
  15.       resolve(zipFile); 
  16.     }); 
  17.   }); 

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

 
 
 
 
  1. async function uploadFile() { 
  2.   let fileList = uploadFileEle.files; 
  3.   if (!fileList.length) return; 
  4.   let webkitRelativePath = fileList[0].webkitRelativePath; 
  5.   let zipFileName = webkitRelativePath.split("/")[0] + ".zip"; 
  6.   let zipFile = await generateZipFile(zipFileName, fileList); 
  7.   upload({ 
  8.     url: "/single", 
  9.     file: zipFile, 
  10.     fileName: zipFileName 
  11.   }); 

在以上的 uploadFile 函数中,我们会对返回的 FileList 对象进行处理,即调用 generateZipFile 函数来生成 ZIP 文件。此外,为了在服务端接收压缩文件时,能获取到文件名,我们为 upload 函数增加了一个 fileName 参数,该参数用于调用 formData.append 方法时,设置上传文件的文件名:

 
 
 
 
  1. function upload({ url, file, fileName, fieldName = "file" }) { 
  2.   if (!url || !file) return; 
  3.   let formData = new FormData(); 
  4.   formData.append( 
  5.     fieldName, file, fileName 
  6.   ); 
  7.   request.post(url, formData); // 省略上传进度跟踪 

以上就是压缩目录上传,前端部分的 JS 代码,服务端的代码可以参考前面单文件上传的相关代码。

压缩目录上传示例:directory-compress-upload

https://github.com/semlinker/file-upload-demos/tree/master/directory-compress-upload

五、拖拽上传

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

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

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

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

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

5.1 前端代码

html

  
 
 
 
  1.  
  2.    

    拖拽上传文件

     
  3.    
 
  •  

    css

     
     
     
     
    1. #dropArea { 
    2.   width: 300px; 
    3.   height: 300px; 
    4.   border: 1px dashed gray; 
    5.   margin-bottom: 20px; 
    6. #dropArea p { 
    7.   text-align: center; 
    8.   color: #999; 
    9. #dropArea.highlighted { 
    10.   background-color: #ddd; 
    11. #imagePreview { 
    12.   max-height: 250px; 
    13.   overflow-y: scroll; 
    14. #imagePreview img { 
    15.   width: 100%; 
    16.   display: block; 
    17.   margin: auto; 

    js

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

    1、阻止默认拖拽行为

     
     
     
     
    1. const dropAreaEle = document.querySelector("#dropArea"); 
    2. const imgPreviewEle = document.querySelector("#imagePreview"); 
    3. const IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png)$/i; 
    4.  
    5. ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => { 
    6.    dropAreaEle.addEventListener(eventName, preventDefaults, false); 
    7.    document.body.addEventListener(eventName, preventDefaults, false); 
    8. }); 
    9.  
    10. function preventDefaults(e) { 
    11.   e.preventDefault(); 
    12.   e.stopPropagation(); 

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

     
     
     
     
    1. ["dragenter", "dragover"].forEach((eventName) => { 
    2.     dropAreaEle.addEventListener(eventName, highlight, false); 
    3. }); 
    4. ["dragleave", "drop"].forEach((eventName) => { 
    5.     dropAreaEle.addEventListener(eventName, unhighlight, false); 
    6. }); 
    7.  
    8. // 添加高亮样式 
    9. function highlight(e) { 
    10.   dropAreaEle.classList.add("highlighted"); 
    11.  
    12. // 移除高亮样式 
    13. function unhighlight(e) { 
    14.   dropAreaEle.classList.remove("highlighted"); 

    3、处理图片预览

     
     
     
     
    1. dropAreaEle.addEventListener("drop", handleDrop, false); 
    2.  
    3. function handleDrop(e) { 
    4.   const dt = e.dataTransfer; 
    5.   const files = [...dt.files]; 
    6.   files.forEach((file) => { 
    7.     previewImage(file, imgPreviewEle); 
    8.   }); 
    9.   // 省略文件上传代码 
    10.  
    11. function previewImage(file, container) { 
    12.   if (IMAGE_MIME_REGEX.test(file.type)) { 
    13.     const reader = new FileReader(); 
    14.     reader.onload = function (e) { 
    15.       let img = document.createElement("img"); 
    16.       img.src = e.target.result; 
    17.       container.append(img); 
    18.     }; 
    19.     reader.readAsDataURL(file); 
    20.   } 

    4、文件上传

     
     
     
     
    1. function handleDrop(e) { 
    2.   const dt = e.dataTransfer; 
    3.   const files = [...dt.files]; 
    4.   // 省略图片预览代码 
    5.   files.forEach((file) => { 
    6.     upload({ 
    7.       url: "/single", 
    8.       file, 
    9.     }); 
    10.   }); 
    11.  
    12. const request = axios.create({ 
    13.   baseURL: "http://localhost:3000/upload", 
    14.   timeout: 60000, 
    15. }); 
    16.  
    17. function upload({ url, file, fieldName = "file" }) { 
    18.   let formData = new FormData(); 
    19.   formData.set(fieldName, file); 
    20.   request.post(url, formData, { 
    21.     // 监听上传进度 
    22.     onUploadProgress: function (progressEvent) { 
    23.       const percentCompleted = Math.round( 
    24.         (progressEvent.loaded * 100) / progressEvent.total 
    25.       ); 
    26.       console.log(percentCompleted); 
    27.     }, 
    28.   }); 

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

    拖拽上传示例:drag-drop-upload

    https://github.com/semlinker/file-upload-demos/tree/master/drag-drop-upload

    六、剪贴板上传

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

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

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

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

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

    利用 Clipboard API 我们可以很方便地操作剪贴板,但实际项目使用过程中也得考虑它的兼容性:

    (图片来源 —— https://caniuse.com/async-clipboard)

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

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

    6.1 前端代码

    html

      
     
     
     
    1.  
    2.    

      请先复制图片后再执行粘贴操作

       
     

    css

     
     
     
     
    1. #uploadArea { 
    2.    width: 400px; 
    3.    height: 400px; 
    4.    border: 1px dashed gray; 
    5.    display: table-cell; 
    6.    vertical-align: middle; 
    7. #uploadArea p { 
    8.    text-align: center; 
    9.    color: #999; 
    10. #uploadArea img { 
    11.    max-width: 100%; 
    12.    max-height: 100%; 
    13.    display: block; 
    14.    margin: auto; 

    js

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

     
     
     
     
    1. const IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png)$/i; 
    2. const uploadAreaEle = document.querySelector("#uploadArea"); 
    3.  
    4. uploadAreaEle.addEventListener("paste", async (e) => { 
    5.   e.preventDefault(); 
    6.   const files = []; 
    7.   if (navigator.clipboard) { 
    8.     let clipboardItems = await navigator.clipboard.read(); 
    9.     for (const clipboardItem of clipboardItems) { 
    10.       for (const type of clipboardItem.types) { 
    11.         if (IMAGE_MIME_REGEX.test(type)) { 
    12.            const blob = await clipboardItem.getType(type); 
    13.            insertImage(blob, uploadAreaEle); 
    14.            files.push(blob); 
    15.          } 
    16.        } 
    17.      } 
    18.   } else { 
    19.       const items = e.clipboardData.items; 
    20.       for (let i = 0; i < items.length; i++) { 
    21.         if (IMAGE_MIME_REGEX.test(items[i].type)) { 
    22.           let file = items[i].getAsFile(); 
    23.           insertImage(file, uploadAreaEle); 
    24.           files.push(file); 
    25.         } 
    26.       } 
    27.   } 
    28.   if (files.length > 0) { 
    29.     confirm("剪贴板检测到图片文件,是否执行上传操作?")  
    30.       && upload({ 
    31.            url: "/multiple", 
    32.            files, 
    33.       }); 
    34.    } 
    35. }); 

    若当前浏览器不支持异步 Clipboard API,则我们会尝试通过 e.clipboardData.items 来访问剪贴板中的内容。需要注意的是,在遍历剪贴板内容项的时候,我们是通过 getAsFile 方法来获取剪贴板的内容。当然该方法也存在兼容性问题,具体如下图所示:

    (图片来源 —— https://caniuse.com/mdn-api_datatransferitem_getasfile)

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

     
     
     
     
    1. function previewImage(file, container) { 
    2.   const reader = new FileReader(); 
    3.   reader.onload = function (e) { 
    4.     let img = document.createElement("img"); 
    5.     img.src = e.target.result; 
    6.     container.append(img); 
    7.   }; 
    8.   reader.readAsDataURL(file); 

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

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

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

    剪贴板上传示例:clipboard-upload

    https://github.com/semlinker/file-upload-demos/tree/master/clipboard-upload

    七、大文件分块上传

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

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

    前面我们都是介绍客户端文件上传的场景,其实也有服务端文件上传的场景。比如在服务端动态生成海报后,上传到另外一台服务器或云厂商的 OSS(Object Storage Service)。下面我们就以 Node.js 为例来介绍在服务端如何上传文件。

    大文件分块上传示例:big-file-upload

    https://github.com/semlinker/file-upload-demos/tree/master/big-file-upload

    八、服务端上传

    服务器上传就是把文件从一台服务器上传到另外一台服务器。借助 Github 上 form-data 这个库提供的功能,我们可以很容易地实现服务器上传的功能。下面我们来简单介绍一下单文件和多文件上传的功能:

    8.1 单文件上传

     
     
     
     
    1. const fs = require("fs"); 
    2. const path = require("path"); 
    3. const FormData = require("form-data"); 
    4.  
    5. const form1 = new FormData(); 
    6. form1.append("file", fs.createReadStream(path.join(__dirname, "images/image-1.jpeg"))); 
    7. form1.submit("http://localhost:3000/upload/single", (error, response) => { 
    8.   if(error) { 
    9.     console.log("单图上传失败"); 
    10.     return; 
    11.   } 
    12.   console.log("单图上传成功"); 
    13. }); 

    8.2 多文件上传

     
     
     
     
    1. const form2 = new FormData(); 
    2. form2.append("file", fs.createReadStream(path.join(__dirname, "images/image-2.jpeg"))); 
    3. form2.append("file", fs.createReadStream(path.join(__dirname, "images/image-3.jpeg"))); 
    4. form2.submit("http://localhost:3000/upload/multiple", (error, response) => { 
    5.   if(error) { 
    6.     console.log("多图上传失败"); 
    7.     return; 
    8.   } 
    9.   console.log("多图上传成功"); 
    10. }); 

    观察以上代码可知,创建完 FormData 对象之后,我们只需要通过 fs.createReadStream API 创建可读流,然后调用 FormData 对象的 append 方法添加表单项,最后再调用 submit 方法执行提交操作即可。

    其实除了 ReadableStream 之外,FormData 对象的 append 方法还支持以下类型:

     
     
     
     
    1. const FormData = require('form-data'); 
    2. const http = require('http'); 
    3.  
    4. const form = new FormData(); 
    5. http.request('http://nodejs.org/images/logo.png', function(response) { 
    6.   form.append('my_field', 'my value'); 
    7.   form.append('my_buffer', new Buffer(10)); 
    8.   form.append('my_logo', response); 
    9. }); 

    服务端文件上传的内容就介绍到这里,关于 form-data 这个库的其他用法,感兴趣的话,可以阅读对应的使用文档。其实除了以上介绍的八种场景外,在日常工作中,你也可能会使用一些同步工具,比如 Syncthing 文件同步工具实现文件传输。好的,本文的所有内容都已经介绍完了,最后我们来做一个总结。

    服务端上传示例:server-upload

    https://github.com/semlinker/file-upload-demos/tree/master/server-upload

    九、总结

    本文阿宝哥详细介绍了文件上传的八种场景,希望阅读完本文后,你对八种场景背后使用的技术有一定的了解。由于篇幅有限,阿宝哥就没有展开介绍与 multipart/form-data 类型相关的内容,感兴趣的小伙伴可以自行了解一下。

    此外,在实际项目中,你可以考虑直接使用成熟的第三方组件,比如 Github 上的 Star 数 11K+ 的 filepond。该组件采用插件化的架构,以插件的方式,提供了非常多的功能,比如 File encode、File rename、File poster、Image preview 和 Image crop 等。总之,它是一个很不错的组件,以后有机会的话,大家可以尝试一下。

    十、参考资源

    MDN- Clipboard

    MDN - DataTransfer

    JSZip- API

    JavaScript 如何检测文件的类型?

    JavaScript 中如何实现大文件并发上传?

    网站栏目:文件上传,搞懂这8种场景就够了
    文章出自:http://www.shufengxianlan.com/qtweb/news42/47842.html

    网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

    广告

    声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联

    猜你还喜欢下面的内容

    网站维护知识

    同城分类信息