文件上传攻略
(1)原理概述
就是根據 http 協議的規范和定義,完成請求消息體的封裝和消息體的解析,然后將二進制內容保存到文件。
我們都知道如果要上傳一個文件,需要把 form 標簽的enctype設置為multipart/form-data,同時method必須為post方法。
那么multipart/form-data表示什么呢?
multipart互聯網上的混合資源,就是資源由多種元素組成,form-data表示可以使用HTML Forms 和 POST 方法上傳文件,具體的定義可以參考RFC 7578。
multipart/form-data 結構
看下 http 請求的消息體
解析
(2)最原始的文件上傳
使用 form 表單上傳文件
在 ie時代,如果實現一個無刷新的文件上傳那可是費老勁了,大部分都是用 iframe 來實現局部刷新或者使用 flash 插件來搞定,在那個時代 ie 就是最好用的瀏覽器(別無選擇)。
這種方式上傳文件,不需要 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>(3)文件上傳接口
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: {//設置文件的默認保存目錄,不設置則保存在系統臨時目錄下 osuploadDir: 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 ...... ');(4)多文件上傳
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)}}`; })(5)局部刷新 - iframe
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>(6)無刷新上傳
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));(7)多文件,單進度
特別提醒
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>(8)多文件上傳+預覽+取消
上一個栗子的多文件上傳只有一個進度條,有些需求可能會不大一樣,需要觀察到每個文件的上傳進度,并且可以終止上傳。
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];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.uploadedif (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>(9)拖拽上傳
html5的出現,讓拖拽上傳交互成為可能,現在這樣的體驗也屢見不鮮。
HTML
<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>(11)剪貼板上傳
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) {// 檢索剪切板itemsfor (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);//發送}(12)大文件上傳-分片
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=2*1024*1024;//分片大小 2Mvar 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>注意點
(13)大文件上傳-斷點續傳
生成 hash 過程肯定也會耗費資源,但是和重新上傳相比可以忽略不計了
JS
模擬分段保存,本地保存到localStorage
(14) node 端上傳圖片
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;})}(15)其他
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);}}(16)input file 外觀更改
總結
- 上一篇: actions相互调用并且存在顺序
- 下一篇: 2022年东数西算概念股龙头,十大龙头股