写在前面
今年国庆假期终于可以憋在家里了不用出门了,不用出去看后脑了,真的是一种享受。这么好的光阴怎么摧残浪费蹂躏,睡觉、用饭、打豆豆这怎么可能(耍多了也烦),完备不符合我们程序员的作风,赶紧起来把文章写完。
这篇文章比较根本,在国庆期间的业余韶光写的,这几天又完善了下,力求把更多的前端所涉及到的关于文件上传的各种场景和运用都涵盖了,若有疏漏和问题还请留言斧正和补充。
自测读不读以下是本文所涉及到的知识点,break or continue ?
文件上传事理最原始的文件上传利用 koa2 作为做事端写一个文件上传接口单文件上传和上传进度多文件上传和上传进度拖拽上传剪贴板上传大文件上传之分片上传大文件上传之断点续传node 端文件上传事理概述
事理很大略,便是根据 http 协议的规范和定义,完成要求体的封装和体的解析,然后将二进制内容保存到文件。
我们都知道如果要上传一个文件,须要把 form 标签的enctype设置为multipart/form-data,同时method必须为post方法。
那么multipart/form-data表示什么呢?
multipart互联网上的稠浊资源,便是资源由多种元素组成,form-data表示可以利用HTML Forms 和 POST 方法上传文件,详细的定义可以参考RFC 7578。
multipart/form-data 构造
看下 http 要求的体
要求头:Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDCntfiXcSkPhS4PN 表示本次要求要上传文件,个中boundary表示分隔符,如果要上传多个表单项,就要利用boundary分割,每个表单项由———XXX开始,以———XXX结尾。
体- Form Data 部分每一个表单项又由Content-Type和Content-Disposition组成。
Content-Disposition: form-data 为固定值,表示一个表单元素,name 表示表单元素的 名称,回车换行后面便是name的值,如果是上传文件便是文件的二进制内容。
Content-Type:表示当前的内容的 MIME 类型,是图片还是文本还是二进制数据。
解析
客户端发送要求到做事器后,做事器会收到要求的体,然后对体进行解析,解析出哪是普通表单哪些是附件。
可能大家立时能想到通过正则或者字符串处理分割出内容,不过这样是行不通的,二进制buffer转化为string,对字符串进行截取后,其索引和字符串是不一致的,以是结果就不会精确,除非上传的便是字符串。
不过一样平常情形下不须要自行解析,目前已经有很成熟的三方库可以利用。
至于如何解析,这个也会占用很大篇幅,后面的文章在详细说。
最原始的文件上传利用 form 表单上传文件
在 ie时期,如果实现一个无刷新的文件上传那可是费老劲了,大部分都是用 iframe 来实现局部刷新或者利用 flash 插件来搞定,在那个时期 ie 便是最好用的浏览器(别无选择)。
DEMO
这种办法上传文件,不须要 js ,而且没有兼容问题,所有浏览器都支持,便是体验很差,导致页面刷新,页面其他数据丢失。
HTML
<form method="post" action="http://localhost:8100" enctype="multipart/form-data"> 选择文件: <input type="file" name="f1"/> input 必须设置 name 属性,否则数据无法发送<br/><br/> 标题:<input type="text" name="title"/><br/><br/><br/> <button type="submit" id="btn-0">上 传</button></form>复制代码
文件上传接口
做事端文件的保存基于现有的库koa-body结合 koa2实现做事端文件的保存和数据的返回。
在项目开拓中,文件上传本身和业务无关,代码基本上都可通用。
在这里我们利用koa-body库来实现解析和文件的保存。
koa-body 会自动保存文件到系统临时目录下,也可以指定保存的文件路径。
然后在后续中间件内得到已保存的文件的信息,再做二次处理。
ctx.request.files.f1 得到文件信息,f1为input file 标签的 name得到文件的扩展名,重命名文件NODE
/ 做事入口 /var http = require('http');var koaStatic = require('koa-static');var path = require('path');var koaBody = require('koa-body');//文件保存库var fs = require('fs');var Koa = require('koa2');var app = new Koa();var port = process.env.PORT || '8100';var uploadHost= `http://localhost:${port}/uploads/`;app.use(koaBody({ formidable: { //设置文件的默认保存目录,不设置则保存在系统临时目录下 os uploadDir: path.resolve(__dirname, '../static/uploads') }, multipart: true // 开启文件上传,默认是关闭}));//开启静态文件访问app.use(koaStatic( path.resolve(__dirname, '../static') ));//文件二次处理,修正名称app.use((ctx) => { var file = ctx.request.files.f1;//得道文件工具 var path = file.path; var fname = file.name;//原文件名称 var nextPath = path+fname; if(file.size>0 && path){ //得到扩展名 var extArr = fname.split('.'); var ext = extArr[extArr.length-1]; var nextPath = path+'.'+ext; //重命名文件 fs.renameSync(path, nextPath); } //以 json 形式输出上传文件地址 ctx.body = `{ "fileUrl":"${uploadHost}${nextPath.slice(nextPath.lastIndexOf('/')+1)}" }`;});/ http server /var server = http.createServer(app.callback());server.listen(port);console.log('demo1 server start ...... ');复制代码
CODE
https://github.com/Bigerfe/fe-learn-code/tree/master/src/upfiles-demo
多文件上传在 ie 时期的多文件上传是须要创建多个 input file 标签,现在 html5只须要一个标签加个属性就搞定了,file 标签开启multiple。
DEMO
HTML
//设置 multiple属性<input type="file" name="f1" multiple/> 复制代码
NODE
做事端也须要进行大略的调度,由单文件工具变为多文件数组,然后进行遍历处理。
//二次处理文件,修正名称app.use((ctx) => { var files = ctx.request.files.f1;// 多文件, 得到上传文件的数组 var result=[]; //遍历处理 files && files.forEach(item=>{ var path = item.path; var fname = item.name;//原文件名称 var nextPath = path + fname; if (item.size > 0 && path) { //得到扩展名 var extArr = fname.split('.'); var ext = extArr[extArr.length - 1]; var nextPath = path + '.' + ext; //重命名文件 fs.renameSync(path, nextPath); //文件可访问路径放入数组 result.push(uploadHost+ nextPath.slice(nextPath.lastIndexOf('/') + 1)); } }); //输出 json 结果 ctx.body = `{ "fileUrl":${JSON.stringify(result)} }`;})复制代码
CODE
https://github.com/Bigerfe/fe-learn-code/tree/master/src/upfiles-demo
局部刷新 - iframe这里说的是在 ie 时期的上传文件局部刷新,借助 iframe 实现。
DEMO
局部刷新页面内放一个隐蔽的 iframe,或者利用 js 动态创建,指定 form 表单的 target 属性值为iframe标签 的 name 属性值,这样 form 表单的 shubmit 行为的跳转就会在 iframe 内完成,整体页面不会刷新。
拿到接口数据然后为 iframe 添加load事宜,得到 iframe 的页面内容,将结果转换为 JSON 工具,这样就拿到了接口的数据
HTML
<iframe id="temp-iframe" name="temp-iframe" src="" style="display:none;"></iframe> <form method="post" target="temp-iframe" action="http://localhost:8100" enctype="multipart/form-data"> 选择文件(可多选): <input type="file" name="f1" id="f1" multiple/><br/> input 必须设置 name 属性,否则数据无法发送<br/><br/> 标题:<input type="text" name="title"/><br/><br/><br/> <button type="submit" id="btn-0">上 传</button> </form> <script>var iframe = document.getElementById('temp-iframe');iframe.addEventListener('load',function () { var result = iframe.contentWindow.document.body.innerText; //接口数据转换为 JSON 工具 var obj = JSON.parse(result); if(obj && obj.fileUrl.length){ alert('上传成功'); } console.log(obj);});</script>复制代码
NODE
做事端代码不须要改动,略.
CODE
https://github.com/Bigerfe/fe-learn-code/tree/master/src/upfiles-demo
无刷新上传无刷新上传文件肯定要用到XMLHttpRequest,在 ie 时期也有这个工具,单只 支持文本数据的传输,无法用来读取和上传二进制数据。
现在已然升级到了XMLHttpRequest2,较1版本有非常大的升级,首先便是可以读取和上传二进制数据,可以利用·FormData·工具管理表单数据。
当然也可利用 fetch 进行上传。
DEMO
HTML
<div> 选择文件(可多选): <input type="file" id="f1" multiple/><br/><br/> <button type="button" id="btn-submit">上 传</button></div>复制代码
JS xhr
<script> function submitUpload() { //得到文件列表,把稳这里不是数组,而是工具 var fileList = document.getElementById('f1').files; if(!fileList.length){ alert('请选择文件'); return; } var fd = new FormData(); //布局FormData工具 fd.append('title', document.getElementById('title').value); //多文件上传须要遍历添加到 fromdata 工具 for(var i =0;i<fileList.length;i++){ fd.append('f1', fileList[i]);//支持多文件上传 } var xhr = new XMLHttpRequest(); //创建工具 xhr.open('POST', 'http://localhost:8100/', true); xhr.send(fd);//发送时 Content-Type默认便是: multipart/form-data; xhr.onreadystatechange = function () { console.log('state change', xhr.readyState); if (this.readyState == 4 && this.status == 200) { var obj = JSON.parse(xhr.responseText); //返回值 console.log(obj); if(obj.fileUrl.length){ alert('上传成功'); } } } } //绑定提交事宜 document.getElementById('btn-submit').addEventListener('click',submitUpload);</script>复制代码
JS Fetch
fetch('http://localhost:8100/', { method: 'POST', body: fd }) .then(response => response.json()) .then(response =>{ console.log(response); if (response.fileUrl.length) { alert('上传成功'); } } ) .catch(error => console.error('Error:', error));复制代码
CODE
https://github.com/Bigerfe/fe-learn-code/tree/master/src/upfiles-demo
多文件,单进度借助XMLHttpRequest2的能力,实现多个文件或者一个文件的上传进度条的显示。
DEMO
解释
页面内增加一个用于显示进度的标签 div.progressjs 内处理增加进度处理的监听函数xhr.upload.onprogressevent.lengthComputable这是一个状态,表示发送的长度有了变革,可打算event.loaded表示发送了多少字节event.total表示文件总大小根据event.loaded和event.total打算进度,渲染div.progressPS 特殊提醒xhr.upload.onprogress要写在xhr.send方法前面,否则event.lengthComputable状态不会改变,只有在末了一次才能得到,也便是100%的时候.
HTML
<div> 选择文件(可多选): <input type="file" id="f1" multiple/><br/><br/> <div id="progress"> <span class="red"></span> </div> <button type="button" id="btn-submit">上 传</button> </div>复制代码
JS
<script> function submitUpload() { var progressSpan = document.getElementById('progress').firstElementChild; var fileList = document.getElementById('f1').files; progressSpan.style.width='0'; progressSpan.classList.remove('green'); if(!fileList.length){ alert('请选择文件'); return; } var fd = new FormData(); //布局FormData工具 fd.append('title', document.getElementById('title').value); for(var i =0;i<fileList.length;i++){ fd.append('f1', fileList[i]);//支持多文件上传 } var xhr = new XMLHttpRequest(); //创建工具 xhr.open('POST', 'http://10.70.65.235:8100/', true); xhr.onreadystatechange = function () { console.log('state change', xhr.readyState); if (xhr.readyState == 4) { var obj = JSON.parse(xhr.responseText); //返回值 console.log(obj); if(obj.fileUrl.length){ //alert('上传成功'); } } } xhr.onprogress=updateProgress; xhr.upload.onprogress = updateProgress; function updateProgress(event) { console.log(event); if (event.lengthComputable) { var completedPercent = (event.loaded / event.total 100).toFixed(2); progressSpan.style.width= completedPercent+'%'; progressSpan.innerHTML=completedPercent+'%'; if(completedPercent>90){//进度条变色 progressSpan.classList.add('green'); } console.log('已上传',completedPercent); } } //把稳 send 一定要写在最下面,否则 onprogress 只会实行末了一次 也便是100%的时候 xhr.send(fd);//发送时 Content-Type默认便是: multipart/form-data; } //绑定提交事宜 document.getElementById('btn-submit').addEventListener('click',submitUpload);</script>复制代码
CODE
https://github.com/Bigerfe/fe-learn-code/tree/master/src/upfiles-demo
多文件上传+预览+取消上一个栗子的多文件上传只有一个进度条,有些需求可能会不大一样,须要不雅观察到每个文件的上传进度,并且可以终止上传。
DEMO
解释
为了预览的须要,我们这里选择上传图片文件,其他类型的也一样,只是预览未便利页面内增加一个多图预览的容器div.img-box根据选择的文件信息动态创建所属的预览区域和进度条以及取消按钮为取消按钮绑定事宜,调用xhr.abort();终止上传利用window.URL.createObjectURL预览图片,在图片加载成功后须要打消利用的内存window.URL.revokeObjectURL(this.src);HTML
<div> 选择文件(可多选): <div class="addfile">添加文件 <input type="file" id="f1" multiple /> </div> <div class="img-box"></div> <button type="button" id="btn-submit">上 传</button> </div>复制代码
JS
<script> //变动网络 为慢3g,就可以比较明显的看到进度条了 var fileMaxCount=6; var imgBox =document.getElementsByClassName('img-box')[0]; var willUploadFile=[];//保存待上传的文件以及干系附属信息 document.getElementById('f1').addEventListener('change',function (e) { var fileList = document.getElementById('f1').files; if (willUploadFile.length > fileMaxCount || fileList.length>fileMaxCount || (willUploadFile.length+ fileList.length>fileMaxCount)) { alert('最多只能上传' + fileMaxCount + '张图'); return; } for (var i = 0; i < fileList.length; i++) { var f = fileList[i];//先预览图片 var img = document.createElement('img'); var item = document.createElement('div'); var progress = document.createElement('div'); progress.className='progress'; progress.innerHTML = '<span class="red"></span><button type="button">Abort</button>'; item.className='item'; img.src = window.URL.createObjectURL(f); img.onload = function () { //显示假如否这块儿内存 window.URL.revokeObjectURL(this.src); } item.appendChild(img); item.appendChild(progress); imgBox.appendChild(item); willUploadFile.push({ file:f, item, progress }); } }); function xhrSend({file, progress}) { var progressSpan = progress.firstElementChild; var btnCancel = progress.getElementsByTagName('button')[0]; var abortFn=function(){ if(xhr && xhr.readyState!==4){ //取消上传 xhr.abort(); } } btnCancel.removeEventListener('click',abortFn); btnCancel.addEventListener('click',abortFn); progressSpan.style.width='0'; progressSpan.classList.remove('green'); var fd = new FormData(); //布局FormData工具 fd.append('f1',file); var xhr = new XMLHttpRequest(); //创建工具 xhr.open('POST', 'http://localhost:8100/', true); xhr.onreadystatechange = function () { console.log('state change', xhr.readyState); //调用 abort 后,state 立即变成了4,并不会变成0 //增加自定义属性 xhr.uploaded if (xhr.readyState == 4 && xhr.uploaded) { var obj = JSON.parse(xhr.responseText); //返回值 console.log(obj); if(obj.fileUrl.length){ //alert('上传成功'); } } } xhr.onprogress=updateProgress; xhr.upload.onprogress = updateProgress; function updateProgress(event) { if (event.lengthComputable) { var completedPercent = (event.loaded / event.total 100).toFixed(2); progressSpan.style.width= completedPercent+'%'; progressSpan.innerHTML=completedPercent+'%'; if(completedPercent>90){//进度条变色 progressSpan.classList.add('green'); } if(completedPercent>=100){ xhr.uploaded=true; } console.log('已上传',completedPercent); } } //把稳 send 一定要写在最下面,否则 onprogress 只会实行末了一次 也便是100%的时候 xhr.send(fd);//发送时 Content-Type默认便是: multipart/form-data; return xhr; } //文件上传 function submitUpload(willFiles) { if(!willFiles.length){ return; } //遍历文件信息进行上传 willFiles.forEach(function (item) { xhrSend({ file:item.file, progress:item.progress }); }); } //绑定提交事宜 document.getElementById('btn-submit').addEventListener('click',function () { submitUpload(willUploadFile); });</script>复制代码
问题1
这里没有做上传的并发掌握,可以通过掌握同时可上传文件的个数(这里掌握为最多6个)或者上传的时候做好并发处理,也便是同时只能上传 X 个文件。
问题2在测试过程中,取消要求的方法xhr.abort()调用后,xhr.readyState会立即变为4,而不是0,以是这里须要做容错处理。
MDN 上说是0.
如果大家有不同的结果,欢迎留言。
CODE
https://github.com/Bigerfe/fe-learn-code/tree/master/src/upfiles-demo
拖拽上传html5的涌现,让拖拽上传交互成为可能,现在这样的体验也习认为常。
DEMO
解释
定义一个许可拖放文件的区域div.drop-box取消drop 事宜的默认行为e.preventDefault();,不然浏览器会直接打开文件为拖拽区域绑定事宜,鼠标在拖拽区域上 dragover, 鼠标离开拖拽区域dragleave, 在拖拽区域上开释文件dropdrop事宜内得到文件信息e.dataTransfer.filesHTML
<div class="drop-box" id="drop-box"> 拖动文件到这里,开始上传 </div> <button type="button" id="btn-submit">上 传</button>复制代码
JS
<script> var box = document.getElementById('drop-box'); //禁用浏览器的拖放默认行为 document.addEventListener('drop',function (e) { console.log('document drog'); e.preventDefault(); }); //设置拖拽事宜 function openDropEvent() { box.addEventListener("dragover",function (e) { console.log('elemenet dragover'); box.classList.add('over'); e.preventDefault(); }); box.addEventListener("dragleave", function (e) { console.log('elemenet dragleave'); box.classList.remove('over'); e.preventDefault(); }); box.addEventListener("drop", function (e) { e.preventDefault(); //取消浏览器默认拖拽效果 var fileList = e.dataTransfer.files; //获取拖拽中的文件工具 var len=fileList.length;//用来获取文件的长度(实在是得到文件数量) //检测是否是拖拽文件到页面的操作 if (!len) { box.classList.remove('over'); return; } box.classList.add('over'); window.willUploadFileList=fileList; }, false); } openDropEvent(); function submitUpload() { var fileList = window.willUploadFileList||[]; if(!fileList.length){ alert('请选择文件'); return; } var fd = new FormData(); //布局FormData工具 for(var i =0;i<fileList.length;i++){ fd.append('f1', fileList[i]);//支持多文件上传 } var xhr = new XMLHttpRequest(); //创建工具 xhr.open('POST', 'http://localhost:8100/', true); xhr.onreadystatechange = function () { if (xhr.readyState == 4) { var obj = JSON.parse(xhr.responseText); //返回值 if(obj.fileUrl.length){ alert('上传成功'); } } } xhr.send(fd);//发送 } //绑定提交事宜 document.getElementById('btn-submit').addEventListener('click',submitUpload);</script>复制代码
CODE
https://github.com/Bigerfe/fe-learn-code/tree/master/src/upfiles-demo
剪贴板上传掘金的写文编辑器是支持粘贴上传图片的,比如我从磁盘粘贴或者从网页上右键复制图片。
DEMO
解释
页面内增加一个可编辑的编辑区域div.editor-box,开启contenteditable为div.editor-box绑定paste事宜处理paste 事宜,从event.clipboardData || window.clipboardData得到数据将数据转换为文件items[i].getAsFile()实现在编辑区域的光标处插入内容 insertNodeToEditor 方法问题1测试中创造复制多个文件无效,只有末了一个文件上传,在掘金的编辑器里也同样存在,在坐有知道缘故原由的可以留言说下。
问题2mac系统可以支持从磁盘复制文件后上传,windows 系统测试未通过,剪贴板的数据未拿到。
HTML
<div class="editor-box" id="editor-box" contenteditable="true" > 可以直接粘贴图片到这里直接上传 </div>复制代码
JS
//光标处插入 dom 节点 function insertNodeToEditor(editor,ele) { //插入dom 节点 var range;//记录光标位置工具 var node = window.getSelection().anchorNode; // 这里判断是做是否有光标判断,由于弹出框默认是没有的 if (node != null) { range = window.getSelection().getRangeAt(0);// 获取光标起始位置 range.insertNode(ele);// 在光标位置插入该工具 } else { editor.append(ele); } } 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()); } } window.willUploadFileList = fileList; event.preventDefault();//阻挡默认行为 submitUpload(); }); function submitUpload() { var fileList = window.willUploadFileList||[]; var fd = new FormData(); //布局FormData工具 for(var i =0;i<fileList.length;i++){ fd.append('f1', fileList[i]);//支持多文件上传 } var xhr = new XMLHttpRequest(); //创建工具 xhr.open('POST', 'http://localhost:8100/', true); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { var obj = JSON.parse(xhr.responseText); //返回值 console.log(obj); if(obj.fileUrl.length){ var img = document.createElement('img'); img.src= obj.fileUrl[0]; img.style.width='100px'; insertNodeToEditor(box,img); // alert('上传成功'); } } } xhr.send(fd);//发送 } 复制代码
CODE
https://github.com/Bigerfe/fe-learn-code/tree/master/src/upfiles-demo
大文件上传-分片在 ie 时期由于无法利用xhr上传二进制数据,上传大文件须要借助浏览器插件来完成。 现在来看实现大文件上传切实其实soeasy。
如果太大的文件,比如一个视频1g 2g那么大,直接采取上面的栗子中的方法上传可能会出链接现超时的情形,而且也会超过做事端许可上传文件的大小限定,以是办理这个问题我们可以将文件进行分片上传,每次只上传很小的一部分 比如2M。
DEMO
解释
相信大家都对Blob 工具有所理解,它表示原始数据,也便是二进制数据,同时供应了对数据截取的方法slice,而 File 继续了Blob的功能,以是可以直策应用此方法对数据进行分段截图。
把大文件进行分段 比如2M,发送到做事器携带一个标志,暂时用当前的韶光戳,用于标识一个完全的文件做事端保存各段文件浏览器端所有分片上传完成,发送给做事端一个合并文件的要求做事端根据文件标识、类型、各分片顺序进行文件合并删除分片文件HTML
代码略,只须要一个 input file 标签。
JS
//分片逻辑 像操作字符串一样 var start=0,end=0; while (true) { end+=chunkSize; var blob = file.slice(start,end); start+=chunkSize; if(!blob.size){//截取的数据为空 则结束 //拆分结束 break; } chunks.push(blob);//保存分段数据 }<script> function submitUpload() { var chunkSize=210241024;//分片大小 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);//发送 } //绑定提交事宜 document.getElementById('btn-submit').addEventListener('click',submitUpload);</script>复制代码
NODE
做事端须要做一些改动,保存分片文件、合并分段文件、删除分段文件。
PS
合并文件这里利用 stream pipe 实现,这样更节省内存,边读边写入,占用内存更小,效率更高,代码见fnMergeFile方法。
//二次处理文件,修正名称app.use((ctx) => { var body = ctx.request.body; var files = ctx.request.files ? ctx.request.files.f1:[];//得到上传文件的数组 var result=[]; var fileToken = ctx.request.body.token;// 文件标识 var fileIndex=ctx.request.body.index;//文件顺序 if(files && !Array.isArray(files)){//单文件上传容错 files=[files]; } files && files.forEach(item=>{ var path = item.path; var fname = item.name;//原文件名称 var nextPath = path.slice(0, path.lastIndexOf('/') + 1) + fileIndex + '-' + fileToken; if (item.size > 0 && path) { //得到扩展名 var extArr = fname.split('.'); var ext = extArr[extArr.length - 1]; //var nextPath = path + '.' + ext; //重命名文件 fs.renameSync(path, nextPath); result.push(uploadHost+nextPath.slice(nextPath.lastIndexOf('/') + 1)); } }); if(body.type==='merge'){//合并分片文件 var filename = body.filename, chunkCount = body.chunkCount, folder = path.resolve(__dirname, '../static/uploads')+'/'; var writeStream = fs.createWriteStream(`${folder}${filename}`); var cindex=0; //合并文件 function fnMergeFile(){ var fname = `${folder}${cindex}-${fileToken}`; var readStream = fs.createReadStream(fname); readStream.pipe(writeStream, { end: false }); readStream.on("end", function () { fs.unlink(fname, function (err) { if (err) { throw err; } }); if (cindex+1 < chunkCount){ cindex += 1; fnMergeFile(); } }); } fnMergeFile(); ctx.body='merge ok 200'; } });复制代码
CODE
https://github.com/Bigerfe/fe-learn-code/tree/master/src/upfiles-demo
大文件上传-断点续传在上面我们实现了大文件的分片上传,办理了大文件上传超时和做事器的限定。
但是仍旧不足完美,大文件上传并不是短韶光内就上传完成,如果期间断网,页面刷新了仍旧须要重头上传,这种韶光的摧残浪费蹂躏怎么能忍?
以是我们实现断点续传,已上传的部分跳过,只传未上传的部分。
方法1在上面我们实现了文件分片上传和终极的合并,现在要做的便是如何检测这些分片,不再重新上传即可。 这里我们可以在本地进行保存已上传成功的分片,重新上传的时候利用spark-md5来天生文件 hash,区分此文件是否已上传。
为每个分段天生 hash 值,利用 spark-md5 库将上传成功的分段信息保存到本地重新上传时,进行和本地分段 hash 值的比拟,如果相同的话则跳过,连续下一个分段的上传PS
天生 hash 过程肯定也会耗费资源,但是和重新上传比较可以忽略不计了。
DEMO
HTML
代码略 复制代码
JS
仿照分段保存,本地保存到localStorage
//得到本地缓存的数据 function getUploadedFromStorage(){ return JSON.parse( localStorage.getItem(saveChunkKey) || "{}"); } //写入缓存 function setUploadedToStorage(index) { var obj = getUploadedFromStorage(); obj[index]=true; localStorage.setItem(saveChunkKey, JSON.stringify(obj) ); } //分段比拟 var uploadedInfo = getUploadedFromStorage();//得到已上传的分段信息 for(var i=0;i< chunkCount;i++){ console.log('index',i, uploadedInfo[i]?'已上传过':'未上传'); if(uploadedInfo[i]){//比拟分段 sendChunkCount=i+1;//记录已上传的索引 continue;//如果已上传则跳过 } var fd = new FormData(); //布局FormData工具 fd.append('token', token); fd.append('f1', chunks[i]); fd.append('index', i); (function (index) { xhrSend(fd, function () { sendChunkCount += 1; //将成功信息保存到本地 setUploadedToStorage(index); 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); } }); })(i); }复制代码
方法2
为什么还有方法2呢,正常情形下方法1没问题,但是须要将分片信息保存在客户端,保存在客户端是最不保险的,说不定涌现各种神奇的幺蛾子。
以是这里有一个更完善的实现,只供应思路,代码就不写了,也是基于上面的实现,只是做事端须要增加一个接口。
基于上面一个栗子进行改进,做事端已保存了部分片段,客户端上传前须要从做事端获取已上传的分片信息(上面是保存在了本地浏览器),本地比拟每个分片的 hash 值,跳过已上传的部分,只传未上传的分片。
方法1是从本地获取分片信息,这里只须要将此方法的能力改为从做事端获取分片信息就行了。
-getUploadedFromStorage+getUploadedFromServer(fileHash)复制代码
其余做事端增加一个获取分片的接口供客户端调用,思路最主要,代码就不贴了。
node 端上传图片不但会从客户端上传文件到做事器,做事器也会上传文件到其他做事器。
读取文件buffer fs构建 form-data form-data上传文件 node-fetchNODE
/ filepath = 相对根目录的路径即可 / async function getFileBufer(filePath) => { return new Promise((resolve) => { fs.readFile(filePath, function (err, data) { var bufer = null; if (!err) { resolve({ err: err, data: data }); } }); }); } / 上传文件 / let fetch = require('node-fetch'); let formData = require('form-data'); module.exports = async (options) => { let { imgPath } = options; let data = await getFileBufer(imgPath); if (data.err) { return null; } let form = new formData(); form.append('xxx', xxx); form.append('pic', data.data); return fetch('http://xx.com/upload', { body: form, method: 'POST', headers: form.getHeaders()//要活的 form-data的头,否则无法上传 }).then(res => { return res.json(); }).then(data => { return data; }) }复制代码
其他在浏览器端对文件的类型、大小、尺寸进行判断file.type判断类型file.size判断大小通过动态创建 img 标签,图片加载后得到尺寸,naturalWidth naturalHeightor width height
JS
var file = document.getElementById('f1').files[0]; //判断类型 if(f.type!=='image/jpeg' && f.type !== 'image/jpg' ){ alert('只能上传 jpg 图片'); flag=false; break; } //判断大小 if(file.size>1001024){ alert('不能大于100kb'); } //判断图片尺寸 var img =new Image(); img.onload=function(){ console.log('图片原始大小 widthheight', this.width, this.height); if(this.naturalWidth){ console.log('图片原始大小 naturalWidthnaturalHeight', this.naturalWidth, this.naturalHeight); }else{ console.log('oImg.widthheight', this.width, this.height); } }复制代码
input file 外不雅观变动
由于input file 的外不雅观比较传统,很多地方都须要进行美化。
定义好一个外不雅观,然后将 file input 定位到该元素上,让他的透明度为0。利用 label 标签 <label for="file">Choose file to upload</label> <input type="file" id="file" name="file" multiple>复制代码
隐蔽 input file 标签,然后调用 input 元素的 click 方法
PS
file 标签隐蔽后在 ie 下无法得到文件内容,建议还是方法1 兼容性强。
源码在这里以上代码均已上传 github
https://github.com/Bigerfe/fe-learn-code/