node 生产的env文件怎么注入_前端各种文件上传攻略,从小图片到大文件断点续传...
写在前面
今年国庆假期终于可以憋在家里了不用出门了,不用出去看后脑了,真的是一种享受。这么好的光阴怎么浪费,睡觉、吃饭、打豆豆这怎么可能(耍多了也烦),完全不符合我们程序员的作风,赶紧起来把文章写完。
这篇文章比较基础,在国庆期间的业余时间写的,这几天又完善了下,力求把更多的前端所涉及到的关于文件上传的各种场景和应用都涵盖了,若有疏漏和问题还请留言斧正和补充。
自测读不读
以下是本文所涉及到的知识点,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
选择文件: input 必须设置 name 属性,否则数据无法发送 标题: 上 传
文件上传接口
服务端文件的保存基于现有的库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/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.loaded
和event.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 naturalHeight
orwidth 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 的外观比较传统,很多地方都需要进行美化。
定义好一个外观,然后将 file input 定位到该元素上,让他的透明度为0。
使用 label 标签
Choose file to upload
隐藏 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文件怎么注入_前端各种文件上传攻略,从小图片到大文件断点续传...相关推荐
- 写给新手前端的各种文件上传攻略,从小图片到大文件断点续传
写在前面 今年国庆假期终于可以憋在家里了不用出门了,不用出去看后脑了,真的是一种享受.这么好的光阴怎么浪费,睡觉.吃饭.打豆豆这怎么可能(耍多了也烦),完全不符合我们程序员的作风,赶紧起来把文章写完. ...
- 前端各种文件上传攻略,从小图片到大文件断点续传
写在前面 今年国庆假期终于可以憋在家里了不用出门了,不用出去看后脑了,真的是一种享受.这么好的光阴怎么浪费,睡觉.吃饭.打豆豆这怎么可能(耍多了也烦),完全不符合我们程序员的作风,赶紧起来把文章写完. ...
- MOSS2007 无法上传超过30M或者50M的大文件解决办法 (转)
转自:http://www.cnblogs.com/codingart/articles/1718032.html 1 如果MOSS2007 无法上传超过30M的文件 解决办法:可能是iis7上传大文 ...
- python 上传文件到网络设备_基于python实现上传文件到OSS代码实例
基础环境 # +++++ 阿里云oss开发指南里都有详细的步骤,在这里整理了一下自己需要的东西 # 确定开发环境,centos默认安装了python2.7 # python -v # 安装python ...
- SpringBoot2整合富文本编辑器wangEditor(含文件上传)攻略
背景 最近要用到一个富文本编辑器,记得遥远的年代,调过Kingeditor.Ueditor...但是那些都很重,,,于是最近经常再留意这件事,直到最近看到一个wangEditor,体验了一下,又轻又好 ...
- ftp上传乱码_ftp同步图片到本地文件夹,ftp同步图片到本地文件夹的实现步骤
建设网站经常需要使用FTP上传文件到远程服务器的空间上.如果使用一般的FTP软件,需要把上传的文件拖动到对应的目录上面实现上传,如果要修改文件,只能够使用FTP默认的方式打开文件,一般都是记事本之类很 ...
- dva是什么游戏_守望先锋DVA上分攻略 DVA使用技巧详解
本期小编给大家带来守望先锋DVA上分攻略,希望能给喜欢用DVA上分的玩家一些帮助,下面让我们一起来看看文章吧. 英雄定位 首先DVA这个英雄我认为是守望里面最全能容错率最高的英雄,有着右键的保护能力, ...
- 检查文件上传完成_“我的数据上传NCBI又报错了...” “攻略拿去!”
在上一期的内容中,我们分享了NCBI测序数据上传的主要步骤和资料填写的注意事项.今天跟大家分享最后一步:原始测序数据的上传以及上传后项目编号的相关类型和含义. 图1 NCBI测序数据上传步骤 | 原始 ...
- python第三方库文件传输_本地 Python 代码上传到 Python 第三方库(Pypi)
程序员对于编程都有自己的"套路",好的套路都会得到复用和 IT 界的传播.这时有一个疑问,怎样来实现呢?小编这里就准备介绍如何将自己写的 Python 包上传到 Python 官网 ...
最新文章
- 德富莱智能抹墙机器人_深圳智能制造应急生产联盟成立,大咖共探机器人行业新机遇...
- 分享一个让 Ping 的输出更简单易读方法
- 利用栈的特性,将十进制数转换成八进制数
- 2020年人工智能领域突破性工作
- Linux设备驱动中的并发控制总结
- 记录使用websocket时因为Sec-Websocket-Protocol遇到的一个问题
- java 8时间操作_Java8 时间日期类操作
- 快报:Java跌惨!Python背后或有推手?网友:心态已崩!
- 写在使用 Linux 工作一年后
- SharePoint 2013必备组件离线包安装:AppFabric无法安装问题解决
- iOS底层探索之Runtime(二): objc_msgSend汇编快速查找分析
- matlab中abs函数,Abs函数
- QT 字体家族中的 字体名称中英文名称对应
- Java连接db2数据库(常用数据库连接五)
- 移动硬盘坏点测试软件,移动硬盘坏道检测工具
- google 安装去广告插件
- java计算机毕业设计网上拍卖系统源码+系统+数据库+lw文档+mybatis+运行部署
- 金融数据挖掘(一):A股上市公司2021年年报
- The requested resource (Servlet action is not available) is not available.这个问题让我通宵了一个晚上
- 一元线性回归与多元线性回归
热门文章
- ChatGPT在数据智能分析与诊断预测中的应用
- 成语词典 API数据接口
- python 匹配段落_python中用xpath匹配文本段落内容的技巧
- 计算机程序c语言教科书,清华大学出版社-图书前言
- uni-app px rpx 互相转换
- 学好SQF, 快活Arma3
- amd 服务器 虚拟化技术,关于“虚拟化”(Intel VT和AMD SVM)的一些认识
- 【备忘】导出Excel,使用NativeExcel控件
- 恒烁M0+系列CX32L003单片机定时器控制LED亮灭
- ORACLE之rman备份恢复及故障处理