文件下载
传统的文件下载有两种方法:
- 使用<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