天天看點

HTML檔案上傳與下載下傳

檔案下載下傳

傳統的檔案下載下傳有兩種方法:

  1. 使用<a/>标簽,href屬性直接連接配接到伺服器的檔案路徑
  2. window.location.href="url"

這兩種方法效果一樣。但有個很大的問題,如果下載下傳出現異常(連接配接路徑失效、檔案不存在、網絡問題等),會導緻原本的頁面被覆寫掉,顯示404等錯誤資訊。

大緻的優化思路如下:

  1. 使用<a/>标簽HTML5新的屬性download。
  2. 使用<iframe><iframe/>元素進行下載下傳。
  3. 使用ajax、axios、fetch等方法異步下載下傳。
  4. 使用websocket下載下傳。

我們來逐一分析:

  1.  <a/>标簽的download屬性,需要和href一起用,download的作用是為下載下傳的檔案賦檔案名。
    • 如果服務端沒有指定檔案名,就以此屬性規定的名稱命名。
    • 如果下載下傳出現異常,該屬性的存在能夠保證頁面不會出問題。
    • 如果服務端傳回的不是檔案、而是字元,如果download=‘’error.txt”,能夠通過打開此檔案檢視到傳回的文本資訊。
  2. <iframe>标簽可以做到在現有的頁面下,内嵌一個子頁面。當使用者點選檔案下載下傳時,将隐藏的iframe元素的src屬性指向檔案下載下傳路徑。
    • 如果沒有異常,檔案将會直接下載下傳。
    • 如果出現異常,iframe子頁面會報錯,父頁面不會受任何影響。
  3. 使用異步請求進行下載下傳。
    • 在網上看了看,大緻的流程是:發送異步請求時設定responseType為blob,即接收流資料為blob對象儲存在記憶體中。接收完成後,生成連結位址(1.通過FileReader對象将blob對象生成base64編碼 2.通過URL.createObjectURL生成指向檔案記憶體的連結),寫入<a/>标簽的href屬性,然後模拟點選<a/>按标簽實作下載下傳。
    • 此方法最大的問題是,因無法直接操作磁盤,故接收的檔案必須先存放在記憶體中(且隻有傳輸完成後才能建構blob對象),才能轉化成檔案。是以,大檔案的下載下傳可能會把你的浏覽器擠爆。
  4. 使用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