写在前面

今年国庆假期终于可以憋在家里了不用出门了,不用出去看后脑了,真的是一种享受。这么好的光阴怎么浪费,睡觉、吃饭、打豆豆这怎么可能(耍多了也烦),完全不符合我们程序员的作风,赶紧起来把文章写完。

这篇文章比较基础,在国庆期间的业余时间写的,这几天又完善了下,力求把更多的前端所涉及到的关于文件上传的各种场景和应用都涵盖了,若有疏漏和问题还请留言斧正和补充。

自测读不读

以下是本文所涉及到的知识点,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-TypeContent-Disposition组成。

Content-Disposition: form-data 为固定值,表示一个表单元素,name 表示表单元素的 名称,回车换行后面就是name的值,如果是上传文件就是文件的二进制内容。

Content-Type:表示当前的内容的 MIME 类型,是图片还是文本还是二进制数据。

解析

客户端发送请求到服务器后,服务器会收到请求的消息体,然后对消息体进行解析,解析出哪是普通表单哪些是附件。

可能大家马上能想到通过正则或者字符串处理分割出内容,不过这样是行不通的,二进制buffer转化为string,对字符串进行截取后,其索引和字符串是不一致的,所以结果就不会正确,除非上传的就是字符串。

不过一般情况下不需要自行解析,目前已经有很成熟的三方库可以使用。

至于如何解析,这个也会占用很大篇幅,后面的文章在详细说。

最原始的文件上传

使用 form 表单上传文件

ie时代,如果实现一个无刷新的文件上传那可是费老劲了,大部分都是用 iframe 来实现局部刷新或者使用 flash 插件来搞定,在那个时代 ie 就是最好用的浏览器(别无选择)。

DEMO


这种方式上传文件,不需要 js ,而且没有兼容问题,所有浏览器都支持,就是体验很差,导致页面刷新,页面其他数据丢失。

HTML

         选择文件:             input 必须设置 name 属性,否则数据无法发送            标题:        上 传

文件上传接口

服务端文件的保存基于现有的库koa-body结合 koa2实现服务端文件的保存和数据的返回。

在项目开发中,文件上传本身和业务无关,代码基本上都可通用。

在这里我们使用koa-body库来实现解析和文件的保存。

koa-body 会自动保存文件到系统临时目录下,也可以指定保存的文件路径。


然后在后续中间件内得到已保存的文件的信息,再做二次处理。

  • ctx.request.files.f1   得到文件信息,f1input 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/demo1

多文件上传

ie 时代的多文件上传是需要创建多个 input file 标签,现在 html5只需要一个标签加个属性就搞定了,file 标签开启multiple

DEMO


HTML

//设置 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/demo2

局部刷新 - iframe

这里说的是在 ie 时代的上传文件局部刷新,借助 iframe 实现。

DEMO


  • 局部刷新

页面内放一个隐藏的 iframe,或者使用 js 动态创建,指定 form 表单的 target 属性值为iframe标签 的 name 属性值,这样 form 表单的 shubmit 行为的跳转就会在 iframe 内完成,整体页面不会刷新。

  • 拿到接口数据

然后为 iframe 添加load事件,得到 iframe 的页面内容,将结果转换为 JSON 对象,这样就拿到了接口的数据

HTML

                   选择文件(可多选):             input 必须设置 name 属性,否则数据无法发送            标题:        上 传                        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);});

NODE

服务端代码不需要改动,略.

CODE

https://github.com/Bigerfe/fe-learn-code/tree/master/src/upfiles-demo/demo3

无刷新上传

无刷新上传文件肯定要用到XMLHttpRequest,在 ie 时代也有这个对象,单只 支持文本数据的传输,无法用来读取和上传二进制数据。

现在已然升级到了XMLHttpRequest2,较1版本有非常大的升级,首先就是可以读取和上传二进制数据,可以使用·FormData·对象管理表单数据。

当然也可使用 fetch 进行上传。

DEMO


HTML

 
选择文件(可多选): 上 传

JS xhr

    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            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);

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/demo4

多文件,单进度

借助XMLHttpRequest2的能力,实现多个文件或者一个文件的上传进度条的显示。

DEMO


说明

  • 页面内增加一个用于显示进度的标签 div.progress

  • js 内处理增加进度处理的监听函数xhr.upload.onprogress

  • event.lengthComputable这是一个状态,表示发送的长度有了变化,可计算

  • event.loaded表示发送了多少字节

  • event.total表示文件总大小

  • 根据event.loadedevent.total计算进度,渲染div.progress

PS 特别提醒

xhr.upload.onprogress要写在xhr.send方法前面,否则event.lengthComputable状态不会改变,只有在最后一次才能获得,也就是100%的时候.

HTML

 
选择文件(可多选): 上 传

JS

    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            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);

CODE

https://github.com/Bigerfe/fe-learn-code/tree/master/src/upfiles-demo/demo5

多文件上传+预览+取消

上一个栗子的多文件上传只有一个进度条,有些需求可能会不大一样,需要观察到每个文件的上传进度,并且可以终止上传。

DEMO


说明

  • 为了预览的需要,我们这里选择上传图片文件,其他类型的也一样,只是预览不方便

  • 页面内增加一个多图预览的容器div.img-box

  • 根据选择的文件信息动态创建所属的预览区域和进度条以及取消按钮

  • 为取消按钮绑定事件,调用xhr.abort();终止上传

  • 使用window.URL.createObjectURL预览图片,在图片加载成功后需要清除使用的内存window.URL.revokeObjectURL(this.src);

HTML

 
选择文件(可多选):

添加文件

上 传

JS

    //更改网络 为慢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 = 'Abort';            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];                btnCancel.removeEventListener('click',function(e) {                    });        btnCancel.addEventListener('click',function(e) {           if(xhr && xhr.readyState!==4){               //取消上传               xhr.abort();           }         });                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);    });

问题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/demo11

拖拽上传

html5的出现,让拖拽上传交互成为可能,现在这样的体验也屡见不鲜。

DEMO


说明

  • 定义一个允许拖放文件的区域div.drop-box

  • 取消drop 事件的默认行为e.preventDefault();,不然浏览器会直接打开文件

  • 为拖拽区域绑定事件,鼠标在拖拽区域上 dragover, 鼠标离开拖拽区域dragleave, 在拖拽区域上释放文件drop

  • drop事件内获得文件信息e.dataTransfer.files

HTML

 
拖动文件到这里,开始上传

上 传

JS

    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            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);

CODE

https://github.com/Bigerfe/fe-learn-code/tree/master/src/upfiles-demo/demo7

剪贴板上传

掘金的写文编辑器是支持粘贴上传图片的,比如我从磁盘粘贴或者从网页上右键复制图片。

DEMO


说明

  • 页面内增加一个可编辑的编辑区域div.editor-box,开启contenteditable

  • div.editor-box绑定paste事件

  • 处理paste 事件,从event.clipboardData || window.clipboardData获得数据

  • 将数据转换为文件items[i].getAsFile()

  • 实现在编辑区域的光标处插入内容 insertNodeToEditor 方法

问题1

测试中发现复制多个文件无效,只有最后一个文件上传,在掘金的编辑器里也同样存在,在坐有知道原因的可以留言说下。

问题2

mac系统可以支持从磁盘复制文件后上传,windows 系统测试未通过,剪贴板的数据未拿到。

HTML

 
可以直接粘贴图片到这里直接上传

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            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/blob/master/src/upfiles-demo/demo8/

大文件上传-分片

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);//保存分段数据    }    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);//发送    }    //绑定提交事件    document.getElementById('btn-submit').addEventListener('click',submitUpload);

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/demo12

大文件上传-断点续传

在上面我们实现了大文件的分片上传,解决了大文件上传超时和服务器的限制。

但是仍然不够完美,大文件上传并不是短时间内就上传完成,如果期间断网,页面刷新了仍然需要重头上传。

方法1

基于上面一个栗子进行改进,服务端已保存了部分片段,重新上传的时候,服务端对当前的分段进行对比,只接收本地没有的分段,前提是分段大小一致。

在上面为了方便,使用了时间戳作为这个文件的标志,其实可以使用spark-md5来生成文件的 hash 值,这样服务器就可以进行文件的对比了。

但是不好的地方是每个分段都要重新发送请求。

方法2 - 断点续传

方法1中,重新上传时请求和数据还会发到服务器,其实已上传的分段就不应该再发送到服务器了,所以我们可以使用断点续传来进行改进。

  • 为每个分段生成 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);    }

node 端上传图片

不只会从客户端上传文件到服务器,服务器也会上传文件到其他服务器。

  • 读取文件buffer fs

  • 构建 form-data form-data

  • 上传文件 node-fetch

NODE

 /**     * 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>100*1024){        alert('不能大于100kb');    }        //判断图片尺寸    var img =new Image();    img.onload=function(){         console.log('图片原始大小 width*height', this.width, this.height);        if(this.naturalWidth){        console.log('图片原始大小 naturalWidth*naturalHeight', this.naturalWidth, this.naturalHeight);        }else{          console.log('oImg.width*height', this.width, this.height);        }    }

input file 外观更改

由于input file 的外观比较传统,很多地方都需要进行美化。

  1. 定义好一个外观,然后将 file input 定位到该元素上,让他的透明度为0。

  2. 使用 label 标签

  Choose file to upload    
  1. 隐藏 input file 标签,然后调用 input 元素的 click 方法

PS

file 标签隐藏后在 ie 下无法获得文件内容,建议还是方法1 兼容性强。

所有代码

以上代码均已上传 github

https://github.com/Bigerfe/fe-learn-code/

最后

本文参考资料

https://developer.mozilla.org/zh-CN/docs/Web/API/Blob

https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest

https://cloud.tencent.com/developer/news/323657

原创不易,多多鼓励

node 生产的env文件怎么注入_前端各种文件上传攻略,从小图片到大文件断点续传...相关推荐

  1. 写给新手前端的各种文件上传攻略,从小图片到大文件断点续传

    写在前面 今年国庆假期终于可以憋在家里了不用出门了,不用出去看后脑了,真的是一种享受.这么好的光阴怎么浪费,睡觉.吃饭.打豆豆这怎么可能(耍多了也烦),完全不符合我们程序员的作风,赶紧起来把文章写完. ...

  2. 前端各种文件上传攻略,从小图片到大文件断点续传

    写在前面 今年国庆假期终于可以憋在家里了不用出门了,不用出去看后脑了,真的是一种享受.这么好的光阴怎么浪费,睡觉.吃饭.打豆豆这怎么可能(耍多了也烦),完全不符合我们程序员的作风,赶紧起来把文章写完. ...

  3. MOSS2007 无法上传超过30M或者50M的大文件解决办法 (转)

    转自:http://www.cnblogs.com/codingart/articles/1718032.html 1 如果MOSS2007 无法上传超过30M的文件 解决办法:可能是iis7上传大文 ...

  4. python 上传文件到网络设备_基于python实现上传文件到OSS代码实例

    基础环境 # +++++ 阿里云oss开发指南里都有详细的步骤,在这里整理了一下自己需要的东西 # 确定开发环境,centos默认安装了python2.7 # python -v # 安装python ...

  5. SpringBoot2整合富文本编辑器wangEditor(含文件上传)攻略

    背景 最近要用到一个富文本编辑器,记得遥远的年代,调过Kingeditor.Ueditor...但是那些都很重,,,于是最近经常再留意这件事,直到最近看到一个wangEditor,体验了一下,又轻又好 ...

  6. ftp上传乱码_ftp同步图片到本地文件夹,ftp同步图片到本地文件夹的实现步骤

    建设网站经常需要使用FTP上传文件到远程服务器的空间上.如果使用一般的FTP软件,需要把上传的文件拖动到对应的目录上面实现上传,如果要修改文件,只能够使用FTP默认的方式打开文件,一般都是记事本之类很 ...

  7. dva是什么游戏_守望先锋DVA上分攻略 DVA使用技巧详解

    本期小编给大家带来守望先锋DVA上分攻略,希望能给喜欢用DVA上分的玩家一些帮助,下面让我们一起来看看文章吧. 英雄定位 首先DVA这个英雄我认为是守望里面最全能容错率最高的英雄,有着右键的保护能力, ...

  8. 检查文件上传完成_“我的数据上传NCBI又报错了...” “攻略拿去!”

    在上一期的内容中,我们分享了NCBI测序数据上传的主要步骤和资料填写的注意事项.今天跟大家分享最后一步:原始测序数据的上传以及上传后项目编号的相关类型和含义. 图1 NCBI测序数据上传步骤 | 原始 ...

  9. python第三方库文件传输_本地 Python 代码上传到 Python 第三方库(Pypi)

    程序员对于编程都有自己的"套路",好的套路都会得到复用和 IT 界的传播.这时有一个疑问,怎样来实现呢?小编这里就准备介绍如何将自己写的 Python 包上传到 Python 官网 ...

最新文章

  1. 德富莱智能抹墙机器人_深圳智能制造应急生产联盟成立,大咖共探机器人行业新机遇...
  2. 分享一个让 Ping 的输出更简单易读方法
  3. 利用栈的特性,将十进制数转换成八进制数
  4. 2020年人工智能领域突破性工作
  5. Linux设备驱动中的并发控制总结
  6. 记录使用websocket时因为Sec-Websocket-Protocol遇到的一个问题
  7. java 8时间操作_Java8 时间日期类操作
  8. 快报:Java跌惨!Python背后或有推手?网友:心态已崩!
  9. 写在使用 Linux 工作一年后
  10. SharePoint 2013必备组件离线包安装:AppFabric无法安装问题解决
  11. iOS底层探索之Runtime(二): objc_msgSend汇编快速查找分析
  12. matlab中abs函数,Abs函数
  13. QT 字体家族中的 字体名称中英文名称对应
  14. Java连接db2数据库(常用数据库连接五)
  15. 移动硬盘坏点测试软件,移动硬盘坏道检测工具
  16. google 安装去广告插件
  17. java计算机毕业设计网上拍卖系统源码+系统+数据库+lw文档+mybatis+运行部署
  18. 金融数据挖掘(一):A股上市公司2021年年报
  19. The requested resource (Servlet action is not available) is not available.这个问题让我通宵了一个晚上
  20. 一元线性回归与多元线性回归

热门文章

  1. ChatGPT在数据智能分析与诊断预测中的应用
  2. 成语词典 API数据接口
  3. python 匹配段落_python中用xpath匹配文本段落内容的技巧
  4. 计算机程序c语言教科书,清华大学出版社-图书前言
  5. uni-app px rpx 互相转换
  6. 学好SQF, 快活Arma3
  7. amd 服务器 虚拟化技术,关于“虚拟化”(Intel VT和AMD SVM)的一些认识
  8. 【备忘】导出Excel,使用NativeExcel控件
  9. 恒烁M0+系列CX32L003单片机定时器控制LED亮灭
  10. ORACLE之rman备份恢复及故障处理