文件下載
傳統的文件下載有兩種方法:
- 使用<a/>標簽,href屬性直接連接到服務器的文件路徑
- window.location.href="url"
這兩種方法效果一樣。但有個很大的問題,如果下載出現異常(連接路徑失效、文件不存在、網絡問題等),會導致原本的頁面被覆蓋掉,顯示404等錯誤信息。
大致的優化思路如下:
- 使用<a/>標簽HTML5新的屬性download。
- 使用<iframe><iframe/>元素進行下載。
- 使用ajax、axios、fetch等方法異步下載。
- 使用websocket下載。
我們來逐一分析:
- <a/>標簽的download屬性,需要和href一起用,download的作用是為下載的文件賦文件名。
- 如果服務端沒有指定文件名,就以此屬性規定的名稱命名。
- 如果下載出現異常,該屬性的存在能夠保證頁面不會出問題。
- 如果服務端返回的不是文件、而是字符,如果download=‘’error.txt”,能夠通過打開此文件查看到返回的文本信息。
- <iframe>標簽可以做到在現有的頁面下,內嵌一個子頁面。當用戶點擊文件下載時,將隱藏的iframe元素的src屬性指向文件下載路徑。
- 如果沒有異常,文件將會直接下載。
- 如果出現異常,iframe子頁面會報錯,父頁面不會受任何影響。
- 使用異步請求進行下載。
- 在網上看了看,大致的流程是:發送異步請求時設置responseType為blob,即接收流數據為blob對象保存在內存中。接收完成后,生成鏈接地址(1.通過FileReader對象將blob對象生成base64編碼 2.通過URL.createObjectURL生成指向文件內存的鏈接),寫入<a/>標簽的href屬性,然后模擬點擊<a/>按標簽實現下載。
- 此方法最大的問題是,因無法直接操作磁盤,故接收的文件必須先存放在內存中(且只有傳輸完成后才能構建blob對象),才能轉化成文件。因此,大文件的下載可能會把你的瀏覽器擠爆。
- 使用websocket下載。
- 需要額外開啟websocket服務,此方法未做實踐。
總結以上方法,最推薦前兩種,方便簡單。
附上后端Django代碼(適用於前兩種方法):
def syncDownLoad(request): "文件下載" print("同步下載文件") startTime = time.time() def file_iterator(file, chunk_size=1024): with open(file, "rb") as f: while True: c = f.read(chunk_size) if c: yield c else: endTime = time.time() print("傳輸時間", endTime - startTime) break fileRoute = "/static/files/2018/12/18/第四章(1)學習動機概述.mp4" fileName = "第四章(1)學習動機概述.mp4" route = os.path.dirname(os.path.dirname(__file__)) + fileRoute if os.path.exists(route): # 如果存在文件 response = StreamingHttpResponse(file_iterator(route)) # response['Content-Type'] = 'application/octet-stream' response['Content-Type'] = 'text/html' response['Content-Disposition'] = 'attachment;filename="{0}"'.format(fileName).encode("utf-8") return response else: return HttpResponse("cannot find file")
參考鏈接:
https://scarletsky.github.io/2016/07/03/download-file-using-javascript/
https://my.oschina.net/watcher/blog/1525962
文件上傳
概述
文件上傳需要處理的問題有:
1.多文件上傳 2.異步上傳 3.拖拽上傳 4.上傳限制(限制大小、類型) 5.顯示上傳進度、上傳速度、中途取消上傳 6.預覽文件
HTML DEMO
<input type="file" id="file" name="myfile" onchange="onchanges()" multiple="multiple"/> <input type="button" onclick="SerialUploadFile()" value="上傳"/>
一、多文件上傳
<input type="file" id="file" name="myfile" multiple="multiple"/> <!-- multiple屬性 -->
二、異步上傳
通過ajax等方式異步上傳,FormData對象支持傳輸文件。
function UploadFile() { var fileObj = document.getElementById("file").files; // js 獲取文件對象(FileList對象) // FormData 對象 var form = new FormData(); form.append("author", "xueba"); // 可以增加表單數據 for (let i = 0; i < fileObj.length; i++) { form.append("file", fileObj[i]); // 文件對象 } $.ajax({ url: "/file_upload/", type: "POST", async: true, // 異步上傳 data: form, contentType: false, // 必須false才會自動加上正確的Content-Type processData: false, // 必須false才會避開jQuery對 formdata 的默認處理。XMLHttpRequest會對 formdata 進行正確的處理 success: function (data) { data = JSON.parse(data); data.forEach((i)=>{ console.log(i.code,i.file_url); }); }, error: function () { alert("aaa上傳失敗!"); }, }); }
三、拖拽上傳
默認文本、圖像和鏈接可以被拖動。其它的元素想要被拖動,只需為標簽加一個draggable="true"屬性
<div draggable="true"><div/>
HTML5 API drag 和 drop
被拖動元素發生的事件 dragstart 被拖動元素開始拖動時 drag 正在被拖動時 dragend 取消拖拽時 目標元素發生的事件(當某元素被綁定以下事件就變成了目標元素) dragenter 拖動元素進入目標上觸發 dragover 拖動元素在目標元素上移動觸發 dragleave 拖動元素離開目標時觸發 drop 拖動元素在目標上釋放觸發,這時不會觸發dragleave 注意: 1.目標元素默認不能夠被拖放drop,要在dragover事件中取消默認事件(e.preventDefault()) 2.有些元素(img)被拖放后,默認以鏈接形式打開,要在drop事件中取消默認事件(e.preventDefault()) 【火狐瀏覽器可能不頂用,需要再加event.stopPropagation()】 dataTransfer(事件對象屬性(對象)) 數據交換:只是簡單的拖拽沒有意義,我們還需要數據交換,即被拖動元素和目標元素之間的數據交換。 方法: setData(key,value) 設置數據(key和value都必須是string類型) getData(key) 獲取數據 clearData() 清除數據(不傳參清空所有數據) setDragImage(imgElement,x,y) 設置元素移動過程中的圖像(參數:圖像元素,xy表示圖像內的偏移量) 屬性: dropEffect 表示被拖動元素可以執行哪一種放置行為(一般在dragover事件內設置) none禁止放置(默認值) move移動到新的位置 copy復制到新的位置 link effectAllowed 用來指定拖動時被允許的行為(一般無需設置) copy,move,link,copyLink,copyMove,linkMove,all,none,uninitialized默認值,相當於all. files FileList對象。如果拖動的不是文件,此為空列表 items 返回DataTransferItems對象,該對象代表了拖動數據。 types 返回一個DOMStringList對象,該對象包括了存入dataTransfer中數據的所有類型。 注意: 1.如果拖拽了文本,瀏覽器會自動調用setData(),設置對應文本數據
該功能沒有Demo
參考鏈接:
https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API
https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer
https://www.zhangxinxu.com/wordpress/2018/09/drag-drop-datatransfer-js/
http://www.sohu.com/a/198973397_291052
四、上傳限制
<input type="file" accept="image/*" /> 接收全部格式的圖片
此外,獲取到的File對象中有type屬性可以得知文件類型,size屬性的得知文件大小
五、上傳進度、上傳速度、中途取消上傳
原生API
xhr.onload = function(e){};//上傳請求完成 xhr.onerror = function(e){};//上傳異常 xhr.upload.onloadstart = function(e){};//開始上傳 xhr.upload.onprogress =function(e){};//上傳進度 這個方法會在文件每上傳一定字節時調用 e.loaded//表示已經上傳了多少byte的文件大小 e.total//表示文件總大小為多少byte 通過這兩個關鍵的屬性就可以去計算 上傳進度與速度 xhr.onreadystatechange = function(){}//當xhr的狀態(上傳開始,結束,失敗)變化時會調用 該方法可以用來接收服務器返回的數據 中途取消上傳 xhr.abort();
單文件上傳 或 多文件串行上傳 Demo:(該Demo只會有一個進度條,顯示上傳總進度。對應“異步上傳”的代碼)
xhr.upload.addEventListener("progess",progessSFunction,false); // 上傳過程中顯示進度和速度 function progressSFunction(e) { var progressBar = document.getElementById(`pro`); var percentageDiv = document.getElementById(`per`); if (e.lengthComputable) // lengthComputable表示進度信息是否可用 { progressBar.max = e.total; progressBar.value = e.loaded; let speed = (e.loaded - progress[0].last_laoded) / (e.timeStamp - progress[0].last_time) + " bytes/s"; let percent = Math.round(e.loaded / e.total * 100) + "%"; progress[0].last_laoded = e.loaded, progress[0].last_time = e.timeStamp; percentageDiv.innerHTML = percent + " " + speed; } }
多文件並行上傳進度顯示:(多個進度條,分別上傳)
// 多文件並行上傳 function ParallelUploadFile() { last_laoded = 0; last_time = (new Date()).getTime(); var fileObj = document.getElementById("file").files; // js 獲取文件對象 for (let k = 0; k < fileObj.length; k++) { let domStr = `<div> ${fileObj[k].name},大小${fileObj[k].size}字節 <progress class='progressBar' id='pro${k}' value='' max=''></progress> <span class='percentage' id='per${k}'></span> </div>`; $("body").append(domStr); // FormData 對象 var form = new FormData(); form.append("author", "xueba"); // 可以增加表單數據 form.append("csrfmiddlewaretoken", $("[name = 'csrfmiddlewaretoken']").val()); form.append("file", fileObj[k]); // XMLHttpRequest 對象 {#var xhr = new XMLHttpRequest();#} {#xhr.open("post", "/file_upload/", true);#} {#xhr.onload = function () {#} {# alert("上傳完成!");#} {# };#} {#xhr.upload.addEventListener("progress", progressFunction, false);#} {#xhr.send(form);#} // jQuery ajax $.ajax({ url: "/file_upload/", type: "POST", async: true, // 異步上傳 data: form, contentType: false, // 必須false才會自動加上正確的Content-Type processData: false, // 必須false才會避開jQuery對 formdata 的默認處理。XMLHttpRequest會對 formdata 進行正確的處理 xhr: function () { let xhr = $.ajaxSettings.xhr(); xhr.upload.addEventListener("progress", (e) => {progressPFunction(e, k)}, false); xhr.upload.onloadstart = (e) => { progress[k] = { last_laoded: 0, last_time: e.timeStamp, }; }; xhr.upload.onloadend = () => { delete progress[k]; }; return xhr; }, success: function (data) { data = JSON.parse(data); data.forEach((i) => { console.log(i.code, i.file_url); }); }, error: function () { alert("aaa上傳失敗!"); }, }); } }
六、預覽文件
預覽圖片
function onchanges() { // input file綁定onchange事件 let files = document.getElementById("file").files; if(files[0].type.indexOf("image")>-1) { let read = new FileReader(); read.onload = function(e) { // 讀取操作完成時觸發 let img = new Image(); img.src = e.target.result; // 將base64編碼賦給src屬性 $("body")[0].appendChild(img); }; read.readAsDataURL(files[0]); // 讀取文件轉化成base64編碼 } }
七、前后端匯總Demo
前端
HTML
<input type="file" id="file" name="myfile" onchange="onchanges()" multiple="multiple"/> <input type="button" onclick="SerialUploadFile()" value="上傳"/>
JavaScript
let progress = {}; let last_laoded; let last_time; function onchanges() { let files = document.getElementById("file").files; console.log(`共${files.length}個文件`); let countSize = 0; for (let i = 0; i < files.length; i++) { console.log(`${files[i].name} 大小${files[i].size}`); countSize += files[i].size; } console.log(`共計占用${countSize}字節`); if (files[0].type.indexOf("image") > -1) { let read = new FileReader(); read.onload = function (e) { // 讀取操作完成時觸發 let img = new Image(); img.src = e.target.result; // 將base64編碼賦給src屬性 $("body")[0].appendChild(img); }; read.readAsDataURL(files[0]); // 讀取文件轉化成base64編碼 } } // 多文件並行上傳 function ParallelUploadFile() { last_laoded = 0; last_time = (new Date()).getTime(); var fileObj = document.getElementById("file").files; // js 獲取文件對象 for (let k = 0; k < fileObj.length; k++) { let domStr = `<div> ${fileObj[k].name},大小${fileObj[k].size}字節 <progress class='progressBar' id='pro${k}' value='' max=''></progress> <span class='percentage' id='per${k}'></span> </div>`; $("body").append(domStr); // FormData 對象 var form = new FormData(); form.append("author", "xueba"); // 可以增加表單數據 form.append("csrfmiddlewaretoken", $("[name = 'csrfmiddlewaretoken']").val()); form.append("file", fileObj[k]); // XMLHttpRequest 對象 {#var xhr = new XMLHttpRequest();#} {#xhr.open("post", "/file_upload/", true);#} {#xhr.onload = function () {#} {# alert("上傳完成!");#} {# };#} {#xhr.upload.addEventListener("progress", progressFunction, false);#} {#xhr.send(form);#} // jQuery ajax $.ajax({ url: "/file_upload/", type: "POST", async: true, // 異步上傳 data: form, contentType: false, // 必須false才會自動加上正確的Content-Type processData: false, // 必須false才會避開jQuery對 formdata 的默認處理。XMLHttpRequest會對 formdata 進行正確的處理 xhr: function () { let xhr = $.ajaxSettings.xhr(); xhr.upload.addEventListener("progress", (e) => {progressPFunction(e, k)}, false); xhr.upload.onloadstart = (e) => { progress[k] = { last_laoded: 0, last_time: e.timeStamp, }; }; xhr.upload.onloadend = () => { delete progress[k]; }; return xhr; }, success: function (data) { data = JSON.parse(data); data.forEach((i) => { console.log(i.code, i.file_url); }); }, error: function () { alert("aaa上傳失敗!"); }, }); } } // 多文件串行上傳 function SerialUploadFile() { var fileObj = document.getElementById("file").files; // js 獲取文件對象 let domStr = `<div> <progress class='progressBar' id='pro' value='' max=''></progress> <span class='percentage' id='per'></span> </div>`; $("body").append(domStr); // FormData 對象 var form = new FormData(); form.append("author", "xueba"); // 可以增加表單數據 for (let i = 0; i < fileObj.length; i++) { form.append("file", fileObj[i]); // 文件對象 } // jQuery ajax $.ajax({ url: "/file_upload/", type: "POST", async: true, // 異步上傳 data: form, contentType: false, // 必須false才會自動加上正確的Content-Type processData: false, // 必須false才會避開jQuery對 formdata 的默認處理。XMLHttpRequest會對 formdata 進行正確的處理 xhr: function () { let xhr = $.ajaxSettings.xhr(); xhr.upload.addEventListener("progress", progressSFunction, false); xhr.upload.onloadstart = (e) => { progress[0] = { last_laoded: 0, last_time: e.timeStamp, }; console.log("開始上傳",progress); }; xhr.upload.onloadend = () => { delete progress[0]; console.log("結束上傳",progress); }; return xhr; }, success: function (data) { data = JSON.parse(data); data.forEach((i) => { console.log(i.code, i.file_url); }); }, error: function () { alert("aaa上傳失敗!"); }, }); } // jQuery版本進度條 function Progressbar(e) { var bar = $("#progressBar"); // 進度條 var num = $("#percentage"); // 百分比 if (e.lengthComputable) { bar.attr("max", e.total); bar.attr("value", e.loaded); num.text(Math.round(e.loaded / e.total * 100) + "%"); } } // 原生js版 並行進度條 function progressPFunction(e, k) { var progressBar = document.getElementById(`pro${k}`); var percentageDiv = document.getElementById(`per${k}`); if (e.lengthComputable) { progressBar.max = e.total; progressBar.value = e.loaded; let speed = (e.loaded - progress[k].last_laoded) / (e.timeStamp - progress[k].last_time) + " bytes/s"; let percent = Math.round(e.loaded / e.total * 100) + "%"; progress[k].last_laoded = e.loaded, progress[k].last_time = e.timeStamp; percentageDiv.innerHTML = percent + " " + speed; console.log(speed); } } // 原生js 串行進度條 function progressSFunction(e) { var progressBar = document.getElementById(`pro`); var percentageDiv = document.getElementById(`per`); if (e.lengthComputable) // lengthComputable表示進度信息是否可用 { progressBar.max = e.total; progressBar.value = e.loaded; let speed = (e.loaded - progress[0].last_laoded) / (e.timeStamp - progress[0].last_time) + " bytes/s"; let percent = Math.round(e.loaded / e.total * 100) + "%"; progress[0].last_laoded = e.loaded, progress[0].last_time = e.timeStamp; percentageDiv.innerHTML = percent + " " + speed; } }
Django后端
def file_upload(request): "ajax文件上傳功能" resList, fileList = [], request.FILES.getlist("file") dir_path = 'static/files/{0}/{1}/{2}'.format(time.strftime("%Y"),time.strftime("%m"),time.strftime("%d")) if os.path.exists(dir_path) is False: os.makedirs(dir_path) for file in fileList: file_path = '%s/%s' % (dir_path, file.name) file_url = '/%s/%s' % (dir_path, file.name) res = {"code": 0, "file_url": ""} with open(file_path, 'wb') as f: if f == False: res['code'] = 1 for chunk in file.chunks(): # chunks()代替read(),如果文件很大,可以保證不會拖慢系統內存 f.write(chunk) res['file_url'] = file_url resList.append(res) return HttpResponse(json.dumps(resList))
參考:
https://www.cnblogs.com/potatog/p/9342448.html
https://www.w3cmm.com/ajax/progress-events.html