笔者在业务中碰到了需要下载示例和拖拽上传并实现进度条的功能,针对过程中遇到的问题,笔者进行了相应的总结。
需求
页面中增加下载示例按钮
实现一块区域能够拖拽上传word文件,限制文件大小2MB和文件类型,能显示进度条,同时支持取消上传。
文件下载
业务中要求的是示例放在静态文件夹中,并不需要请求后台。针对这种场景,笔者将介绍三种方法,分别是window.open,form表单提交以及a标签下载。笔者将通过下载img和word文档的例子,对这三种方法进行对比。
现构建dom结构如下:
1 2 3 | <button onClick={this.windowOpen}>window.open</button><button onClick={this.formSubmit}>formSubmit</button><button onClick={this.aDownload}>aDownload</button> |
方法一:使用window.open:
1 2 3 4 5 6 | import gakkiURL from './gakki.jpg';import wordURL from './wordURL.doc';windowOpen = () => { window.open(gakkiURL); //window.open(wordURL);} |
该方法在请求两种文件时,具体表现为:
img:新开网页,然后显示对应的img图片。
word:下载该文件。
方法2:使用form表单的submit:
01 02 03 04 05 06 07 08 09 10 | formSubmit = () => { let form = document.createElement('form'); form.method = 'get'; form.action = gakkiURL; //form.action = wordURL; //form.target = '_blank'; // form新开页面 document.body.appendChild(form); // form表单做出提交操作要先加入到dom树中 form.submit(); document.body.removeChild(form);} |
该方法在请求两种文件时,具体表现为:
img:form在不设置target时,会在当前页面打开url,显示图片。
word:下载该文件。
从上述两种方法可以看出,在请求对应的url时,浏览器针对不同的MIME类型会选择不同的处理方式。在请求img、txt等格式时,浏览器会打开对应的文件,而不是下载。如果想要img这些格式也下载呢?此时就需要方法三。
方法3:使用a标签:
// 使用a标签
1 2 3 4 5 6 7 | aDownload = () => { const a = document.createElement('a'); a.href = gakkiURL; //a.href = wordURL; //a.download = 'gakki.jpg'; a.click();} |
a标签在不加download属性时表现同上两种方法,而在加了download属性后,可成功触发img等格式的下载。
download:
该属性可以设置一个值来规定下载文件的名称。所允许的值没有限制,浏览器将自动检测正确的文件扩展名并添加到文件 (.img, .pdf, .txt, .html, 等等)。
最终对比效果:
文件拖拽上传
文件上传
常用方法是使用type="file"的input标签触发下载,然后使用formData传输数据,代码如下:
// 点击上传文档
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | handleClick = () => { const input = document.createElement('input'); input.type = 'file'; input.accept = 'application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document'; // word文件对应的MIME类型 input.onchange = (e) => { const file = e.target.files.items(0);// files[0]也行 console.table(file); // 检查文档格式 if (!this.checkDocument()) { e.target.value = ''; return; } // 上传文档 this.uploadDocument(file); }; input.click();}; |
accept:表明input接受的文件的MIME类型。.doc和.docx相应的MIME类型在源码中已标明。
fileList对象可通过items或数组索引的形式获得对应的file对象。file对象常用的属性有:lastModified、type、name和size。可通过这些属性自定义检查文档格式。
检查文档的代码checkDocument如下:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | // 文档检查checkDocument = file => { const accept = ['.doc', '.docx']; const index = file.name.lastIndexOf('.'); if (index < 0 || accept.indexOf(file.name.substr(index).toLowerCase()) < 0) { // 检查文件类型 Message.error('暂不支持该文件格式'); return false; } if (file.size > 2 * 1024 * 1024) { // 检查文件大小 Message.error('文档大于2MB,上传失败'); return false; } return true;}; |
之后是上传文档uploadDocument:
1 2 3 4 5 6 7 8 9 | // 上传文档uploadDocument = file => { const index = file.name.lastIndexOf('.'); const fileName = file.name.slice(0, index); const formData = new FormData(); formData.append('file', file); // ajax、fetch或axios等方式上传 ...}; |
上传文档后需要去获取上传进度显示进度条,下面将对ajax、fetch和axios对progress事件的支持情况分别予以介绍。
Ajax
原生支持progress事件,可用于获取上传进度和下载进度,分别为xhr.upload.onprogress和xhr.onprogress事件。代码如下:
1 | xhr.upload.onprogress = ev => console.log((ev.loaded / ev.total) * 100) |
另外可以使用xhr.abort()取消文件上传。
Fetch
不支持progress事件,所以无法获取上传进度。但是笔者在查阅资料时发现由于res.body是可读字节流对象,所以可以使用res.body对象支持的getReader()属性获得下载进度,具体文献请参考jakearchibald.com/2016/stream…。此处代码与上传的需求无关,仅作为fetch的相关拓展,可直接跳过这一段。
res.body.getReader()方法用于读取响应的原始字节流,该字节流是可以循环读取的,直至body内容传输完成;
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | fetch(url, options).then(res => { let reader = res.body.getReader(); let loaded = 0; // read()方法返回一个promise,接受值时resolve。 reader.read().then(function processResult(result) { // result对象有两个属性: // done:完成时为true // value 数据 if (result.done) { // 完成时退出循环 console.log("Fetch complete"); return; } loaded += result.value.length;// 长度,单位:字节 console.log('Received', loaded, 'bytes of data so far'); // 循环读取 return reader.read().then(processResult); });}); |
Axios
通过onUploadProgress和onDownloadProgress实现上传和下载。
1 2 3 4 | onUploadProgress(ev) => { length = Math.round((ev.loaded / ev.total) * 100); console.log(length);} |
axios使用cancel token取消请求
1 2 3 4 5 6 7 8 | var CancelToken = axios.CancelToken;var source = CancelToken.source();axios.get(url, { cancelToken: source.token})source.cancel();//取消请求 |
总结
ajax和axios对progress事件都进行了很好地支持,而fetch由于缺少对progress事件的支持在这里无法使用。
拖拽事件
实现了点击上传、获得上传进度以及取消上传等功能后,接下来要完成的是实现拖拽上传。实现前,我将对有关事件进行介绍。
首先是拖拽物体时发生的事件:onDragStart,onDrag和onDragEnd,事件与被拖拽的物体有关。
onDragStart:拖拽开始
onDrag:拖拽中持续触发
onDragEnd:拖拽结束,无论是否可以放置均触发事件
然后是放置文件时要触发的事件:onDragEnter,onDragOver,onDragLeave和onDrop,事件与要拖放进的区域有关。
onDragEnter:拖拽的物体进入时触发
onDragOver:拖拽的物体在区域中拖动时持续触发
onDragLeave:拖拽的物体离开区域时触发
onDrop:拖拽的物体放置在区域中时触发
项目中为了能够拖拽word文档,需要在容器上取消该容器默认的onDragEnter和onDragOver事件,这是因为:
事件的侦听器 dragenter 或 dragover 事件被用来表示有效的 drop 目标,也就是拖放项目可能被 dropped 的地方。web页面或应用程序的大多数区域都不是 drop 数据的有效位置。因此,这些事件的默认处理是不允许出现 drop。
如果您想要允许 drop,您必须通过取消事件来防止默认的处理。您可以通过从attribute-defined 事件监听器返回 false,或者通过调用事件的 preventDefault方法来实现这一点。后者在一个单独的脚本中定义的函数中可能更可行。
dom结构如下:
1 2 3 4 5 6 7 8 9 | <div styleName="dropbox" onDragOver={this.preventDefault} onDragEnter={this.preventDefault} onDrop={this.handleDrop} > <div styleName="word-img" /> {this.renderBtnByUpload(this.state.uploadStatus)} // 根据上传状态决定是"上传文件"还是"取消上传"</div> |
在将文件拖拽到内容区放置后,可以通过dataTransfer对象获得file信息。最终的handleDrop事件如下:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | // 拖拽上传handleDrop = (e) => { const file = e.dataTransfer.files[0]; if (e.dataTransfer.files.length > 1) { Message.error('仅支持上传一个word文件'); return; } if (!this.checkDocument(file)) { // 上传失败直接退出 e.target.value = ''; return; } this.uploadDocument(file); // 上传文件} |
总结
最终,实现的总体思路就是,首先构建放置文件的容器,然后给该容器取消默认的onDragOver和onDragEnter事件,当拖拽文件到容器中时通过dataTransfer.files拿到文件并上传,使用ajax或axios等方式提供的progress事件拿到长度,将该长度传到progressBar组件中,最后展示出来。