2013年3月20日 星期三

HTML5 拖曳多檔案上傳

目的:拖曳電腦檔案到網頁某區塊後,直接上傳到網站伺服器上。
方法:
  1. 使用 HTML5 新增的拖曳功能(Drag and Drop),將電腦檔案拖曳到網頁上。 
  2. 拖曳時會產生幾種事件,這些事件會產生 DragEvent 物件
  3. DragEvent 物件有一個 dataTransfer 屬性,由dataTransfer 屬性,可取得 DataTransfer 物件
  4. 由 DataTransfer 物件的 files 屬性,即可取得檔案物件(FileList)。 
  5. 將取得的檔案物件丟給 FormData 物件。
    FormData:http://www.w3.org/TR/XMLHttpRequest2/#interface-formdata
  6. 最後將 FormData 的資料,丟給 XMLHttpRequest 使用 AJAX 方式上傳到網站伺服器。
範例:
以下範例可拖曳多個 JPG 圖檔,一次上傳,並顯示上傳進度。
(於Firefox、Chrome、IE10 測試可正常運作)
HTML 與 JavaScript 的部份如下
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>HTML5拖曳多檔案上傳</title>
    <style>
    #dropDIV{
        text-align: center; 
        width: 300px;
        height: 200px;        
        margin: auto;
        border: dashed 2px gray;
        
    }
    img{
        max-height:200px; 
        max-width:300px;
    }
    </style>
    <script>

        function dragoverHandler(evt) {
            evt.preventDefault();
        }
        function dropHandler(evt) {//evt 為 DragEvent 物件
            evt.preventDefault();
            var files = evt.dataTransfer.files;//由DataTransfer物件的files屬性取得檔案物件
            var fd = new FormData();
            var xhr = new XMLHttpRequest();
            var up_progress = document.getElementById('up_progress');
            xhr.open('POST', 'upload.php');//上傳到upload.php
            xhr.onload = function() {
                //上傳完成
                up_progress.innerHTML = '100 %, 上傳完成';
            };
            xhr.upload.onprogress = function (evt) {
              //上傳進度
              if (evt.lengthComputable) {
                var complete = (evt.loaded / evt.total * 100 | 0);
                if(100==complete){
                    complete=99.9;
                }
                up_progress.innerHTML = complete + ' %';
              }
            }

        
            for (var i in files) {
                if (files[i].type == 'image/jpeg') {
                    //將圖片在頁面預覽
                    var fr = new FileReader();
                    fr.onload = openfile;
                    fr.readAsDataURL(files[i]);
                    
                    //新增上傳檔案,上傳後名稱為 ff 的陣列
                    fd.append('ff[]', files[i]);
                }
            }
            xhr.send(fd);//開始上傳
        }
        function openfile(evt) {
            var img = evt.target.result;
            var imgx = document.createElement('img');
            imgx.style.margin = "10px";
            imgx.src = img;
            document.getElementById('imgDIV').appendChild(imgx);
        }    
    </script>
</head>
<body>
    <div id="dropDIV" ondragover="dragoverHandler(event)" ondrop="dropHandler(event)">
    拖曳圖片到此處上傳
    <div id="up_progress"></div>
    </div>
    <div id="imgDIV"></div>
</body>
</html>
伺服器端處理上傳圖檔的 PHP 程式(upload.php)
$uploads_dir = 'mydir';//存放上傳檔案資料夾
foreach ($_FILES["ff"]["error"] as $key => $error) {
    if ($error == UPLOAD_ERR_OK) {
        $tmp_name = $_FILES["ff"]["tmp_name"][$key];
        $name = $_FILES["ff"]["name"][$key];
        move_uploaded_file($tmp_name, "$uploads_dir/$name");
    }
}

DEMO:
其他:

46 則留言:

  1. 不能用阿...
    伺服器收不到檔案

    回覆刪除
    回覆
    1. 您好,是指上傳進度有到 100%,但伺服器端沒看到檔案嗎?
      如果是,請檢查伺服器的回應訊息,看是否有發生錯誤。
      上面的範例沒有將回應訊息顯示出來,
      可以用各瀏覽器的開發者工具,查看 AJAX 的回應訊息。

      或是在上面的 JS 範例,加上顯示回應訊息的程式碼,例如
      xhr.onreadystatechange = function() {
      if (xhr.readyState == 4) {
      alert("伺服器回應:\n" + xhr.responseText);
      }
      }

      也可以先用傳統的上傳方式,測看看伺服器端是否正常
      <form action="upload.php" method="post" enctype="multipart/form-data">
      <input type="file" name="ff[]" />
      <input type="submit" value="上傳" />
      </form>

      還有伺服器端的 mydir 資料夾要自己建立

      刪除
    2. form 我測試過了可以上傳 但拖曳就是不能上傳
      不知哪裡出了問題

      刪除
    3. 您好,可以用瀏覽器的開發人員工具,看裡面的
      1.主控台(console)是否有 JS 的錯誤訊息
      2.網路(network)是否有產生上傳的request

      或是將您的測試程式寄給我看看
      xyz@cinc.biz

      另外請問您使用的瀏覽器與版本是?

      刪除
    4. 補充一下,叫出瀏覽器內建的 開發人員工具 快速鍵如下
      Firefox:「shift + F2」
      chrome:「ctrl + shift + i」
      IE:「F12」

      刪除
    5. 非常感謝 回應 我webserver 是架設在 fedora 20 apache2 開發人員工具 也看不出問題 測試程式跟網頁的一樣只改 body 內div的排列 您說的網路是否有產生上傳的request 要如何看

      刪除
    6. 利用開發人員工具 確認 upload.php 有被啟動 另外圖片有14MB左右 然後就無法上傳 php.ini post_max_size upload_max_filesize 我都加大到 32MB 了 目前找不到問題下手解決

      刪除
    7. 非常感謝我已經找到問題 是 selinux 的問題 目前還未找到設定解決 但知道是確認是selinux設定問題 感謝您寶貴的範例

      刪除
    8. 晚了一步 XD,我剛弄了個demo
      http://demo.cinc.biz/html5-drag-drop-upload/
      demo就留著囉,給之後有緣的人看 XD

      假設上傳檔名為 abc.jpg
      上傳後,圖片網址為
      http://demo.cinc.biz/html5-drag-drop-upload/mydir/abc.jpg
      可以查看是否上傳成功

      如果是 selinux 的問題
      似乎可以在 upload.php 回應訊息看到執行 move_uploaded_file() 時,產生PHP Warning 的訊息
      假設PHP 錯誤訊息,顯示的層級,有開到很高的話,開發時我是都開到最高,notice 都顯示 XD

      解決的方式可以參考
      http://albertech.net/2011/03/fix-fedora-selinux-permissions-for-php-file-upload/


      刪除
    9. 感謝分享!! 我剛好也看到這個網站 分享一下可能版本與系統環境的關係還有幾個項目需要測試
      sudo chcon -R -t httpd_sys_rw_content_t 目錄
      sudo setsebool -P httpd_usified 1
      sudo setsebool -P httpd_read_user_content 1

      分享可能線索

      刪除
    10. 感謝分享,剛好我對 SELinux 的設定也不熟 XD

      刪除
  2. 您好
    我在第59行發生error
    POST http://localhost/upload_test/upload.php 405 (Method Not Allowed)
    請問要怎麼解決QQ

    回覆刪除
    回覆
    1. 您好
      看起來應該是網頁伺服器設定成不能接收 POST 的資料
      可能要修改網頁伺服器的設定。
      IIS 可以參考:jQuery POST, Error 405 Method not allowed
      Nginx 可以參考:Nginx HTTP Post Method: 405 Method not allowed解决方法

      刪除
    2. 最後決定改用asp寫了
      謝謝您

      刪除
    3. 您好,可以請問您原本是用什麼寫呢?
      我自己是用 Apache+PHP,
      之前測試限制不能使用 POST ,出現的訊息不是 405,
      所以還蠻好奇的您是不是用 Apache,謝謝~

      刪除
    4. 您好,我是用IIS沒有用apache耶
      不好意思沒有幫上你的忙
      您的文章真的很棒 很有幫助
      謝謝您

      刪除
    5. 您好,有幫到我喔,因為我之前在猜您是不是用 IIS,但不確定。
      我剛剛試了一下 IIS 7.5 + PHP(用FastCgiModule)
      將"處理常式對應"->"編輯模組對應"->"要求限制"->"指令動詞" 改成只接受 GET
      真的出現 "POST upload.php 405 Method Not Allowed" 了
      解決我心中的疑惑,感謝~

      刪除
  3. 您好:想請教一下~
    該怎樣實現此範例拖曳圖片後..能夠在我的html 指定 div 裏面出現呢??
    我對於後端怎樣串資料給前端沒有研究...
    但我在想..html5 是不是會有更人性化的功能出現?
    讓管理者在指定上傳的區域放上圖片後.點選傳送 .,可以在前台的首頁看到變更.....
    有想過用xml去串...但是還不夠人性化@@....

    本來想放棄的..但看到你的教學.感覺還是有希望

    回覆刪除
    回覆
    1. 您好
      我修改原範例,加了顯示上傳後的圖片,供您參考
      http://demo.cinc.biz/html5-drag-drop-upload/index2.html
      主要修改是在 PHP 傳回了上傳後的檔名,JavaScript 增加了 xhr.onreadystatechange 處理 PHP 回傳的檔名資料

      關於"後台上傳後,前台可以看到變更"
      我習慣將上傳後的檔案名稱,儲存在資料庫,前台再去撈資料庫儲存的檔案名稱。

      另外,您提到使用 HTML5 的功能達到此效果,
      我在猜,您是想做正式儲存前的預覽效果嗎?
      如果在同一個瀏覽器,我目前是想到,可以使用HTML5的 postMessage 的功能,
      假設一開始,已從後台開啟前台,此時瀏覽器有兩個頁面,
      這時後台可以用 postMessage,傳遞資料給被開啟的前台
      就可以讓前台更換圖片,達到預覽的效果。

      刪除
  4. 你好!請問要上傳照片去 網上directory 可以怎樣做?
    我嘗試把$uploads_dir = 改做想存放的位置-->>'http://maki1824.byethost11.com/Wedding/photos/'
    但是行不通,,請指教

    回覆刪除
    回覆
    1. 您好,
      假設範例中的 upload.php 放在 Wedding 資料夾底下
      則 $uploads_dir = 'photos';
      photos 資料夾要先建立好,並確認有寫入的權限。

      刪除
    2. 謝謝已解決!

      然後想問一下若要限制上傳圖片的大小, 可以怎樣做?

      刪除
    3. server 端可以用 $_FILES['ff']['size'] 取得上傳檔案大小
      如果是多個檔案,可如範例中,跑迴圈處理。
      PHP上傳後的檔案陣列 $_FILES 可用的資訊,可參考
      http://www.php.net/manual/en/features.file-upload.post-method.php

      client 端可以在 for (var i in files) {...} 這個地方,
      使用 files[i].size 取得檔案大小,再進行判斷。
      可參考 https://developer.mozilla.org/en-US/docs/Web/API/Blob.size

      只在 server 端限制,有一個缺點=>要上傳完才知道有無超過大小。
      只在 client 端限制,缺點則是=>可能會被繞過限制。

      刪除
  5. 請問要如何可以在上傳前改變上傳檔名,因為不想在server端php改,client端給隨機碼讓server有覆蓋同一批圖片的可能
    files[i].name="????";
    alert(files[i].name)
    出來後發現檔名沒有被我改變,還是原始檔名
    新手求解 plz

    回覆刪除
    回覆
    1. 您好
      因為 File.name 屬性是唯獨的,所以沒辦法修改
      https://developer.mozilla.org/en-US/docs/Web/API/File

      一般我都是在server端產生唯一的檔名。
      如果您想在client端設定上傳後的檔名,
      可以用一個隱藏欄位來存放設定的檔名,再一併送到server端
      server端儲存檔案時,再用接收到的檔名來儲存檔案

      以文中範例來說,server端PHP大概類似這樣
      move_uploaded_file($tmp_name, "$uploads_dir/接收到的檔名")

      刪除
    2. 我剛剛弄了個範例,您參考看看
      http://demo.cinc.biz/html5-drag-drop-upload/index3.html
      不過我直接在 JS 裡面設定要一起傳送的檔名資料,加了
      fd.append('fn[]', 'newfilename' + i);//設定上傳後的檔案名稱,上傳後名稱為 fn 的陣列

      設定的檔名依序為 newfilename0、newfilename1、newfilename2、.....

      刪除
    3. 感謝了!!!原來是唯讀屬性,只好乖乖帶值傳過去了

      刪除
  6. 您好:
    感謝您的提供的範例 但是我有碰到問題想要請教一下

    我要從前面頁面的form傳遞一個參數來放置我要的路徑

    可是檔案卻無法放置在我所要放置的路徑上 這個是哪方面的問題呢?

    $error) {
    if ($error == UPLOAD_ERR_OK) {
    $tmp_name = $_FILES["ff"]["tmp_name"][$key];
    $name = $_FILES["ff"]["name"][$key];
    move_uploaded_file($tmp_name, $uploads_dir.$name);
    }
    }
    ?>

    回覆刪除
    回覆
    1. $v_id = $_POST['f_id'];
      $uploads_dir = "/home/Taian/paper/".$v_id."/";
      mkdir($uploads_dir, 0755, true);
      foreach ($_FILES['ff']["error"] as $key => $error) {
      if ($error == UPLOAD_ERR_OK) {
      $tmp_name = $_FILES["ff"]["tmp_name"][$key];
      $name = $_FILES["ff"]["name"][$key];
      move_uploaded_file($tmp_name, $uploads_dir.$name);
      }
      }
      ?>

      刪除
    2. 您好,是指檔案上傳成功,但路徑不是您設的嗎?
      如果是,$_POST['f_id'] 接收到值是?
      另外,因您沒貼 JS 的部份,請問 JS 有使用 fd.append 將 f_id 增加到要傳送的資料裡嗎?

      刪除
    3. 您好
      JS 我只有修改上傳的upload.php 而已 其他的還不清楚要如何調整
      我想要透過丟過去upload.php的參數來指定我要放置檔案的路徑
      這部分不清楚該如何修改
      所以是我有少增加語法嗎?
      感謝您的回覆

      刪除
    4. 我前端只是希望透過一個 input name='f_id' type='text' value="A1234567890" 傳遞
      使其將檔案放置在 A1234567890 這個路徑下
      這些我寫在一個< form > 裡面

      < form enctype='multipart/form-data' action="test.php" method="post" name="form_member_login" class="form_block" >
      < p class="form_text" >
      < input name="but_paper_upload" type="submit" value="登入" / >
      < /p >
      < p >
      < div id="dropDIV" ondragover="dragoverHandler(event)" ondrop="dropHandler(event)" >
      拖曳圖片到此處上傳
      < div id="up_progress" >< /div >
      < /div >
      < div id="imgDIV" >< /div >
      < /p >

      刪除
    5. 1.HTML 部份
      <form id="myForm" ....>
      <input name='f_id' type='text' value="A1234567890">
      ...略...
      </form>

      2.JS 部份,FormData 綁定 HTML 的 form,POST 時會連表單資料一起傳送
      var fd = new FormData();
      改成
      var fd = new FormData(document.getElementById("myForm"));

      這樣PHP應該就可以接收到 $_POST["f_id"] 的值了。
      P.S.若不想綁定整個 form,也可以使用 fd.append('f_id', 'A1234567890') 將要傳送的資料個別加進去

      刪除
  7. 請問如果我要加入按鈕 也是透過JAVASCRIPT嗎

    回覆刪除
    回覆
    1. 預覽完圖片後 在點擊按鈕上傳

      刪除
    2. 按鈕直接放在HTML即可
      簡易的修改如下
      1. JavaScript 中 fd、xhr 改成全域變數
      var fd = new FormData();
      var xhr = new XMLHttpRequest();
      改成
      fd = new FormData();
      xhr = new XMLHttpRequest();

      2.拿掉 JavaScript 開始上傳的程式碼
      xhr.send(fd);//開始上傳 => 這行拿掉

      3.HTML按鈕的onclick加上開始上傳的程式碼
      <input type="button" value="開始上傳" onclick="xhr.send(fd);">

      刪除
  8. 請教一下~
    如果我要限制拖曳的檔案數量要怎麼做呢?
    例如只能接受1個檔案拖曳~
    謝謝

    回覆刪除
    回覆
    1. 您好,可以在「var files = evt.dataTransfer.files;//由DataTransfer物件的files屬性取得檔案物件」這一行後面,使用 files.length 屬性,加上拖曳數量判斷。
      例如:
      .....
      var files = evt.dataTransfer.files;//由DataTransfer物件的files屬性取得檔案物件
      if(files.length >1){
      alert("一次只能上傳一張圖片");
      return;
      }
      .....

      刪除
  9. 你好,我有問題想請教版主
    假設我一次拖曳8張圖片做預覽之後,然後刪除了第3張和第5張
    之後在選其他圖片拖曳進去或者不選,確認之後,才將上述圖片一次性上傳

    這樣的話,我就沒辦法在一開始拖曳的時候做
    var fd = new FormData();
    var xhr = new XMLHttpRequest();
    fd.append('ff[]', files[i]);
    xhr.send(fd);
    這些動作,

    原本想說自己建 input file再將檔案塞進去 結果發現不行

    想請問版主,我應該怎處理...

    或者再刪除的時候,可否將ff[]指定的圖檔刪除?

    回覆刪除
    回覆
    1. 抱歉 我是原PO 想請問一下
      我在form裡,進行拖曳圖片或是點選圖檔都可以在瀏覽器預覽(這一段沒問題)

      form裡,也包含一些其他input訊息,再透過submit方式,在 publish_act.php 儲存資料

      form裡面 我設定 action="publish_act.php" method="post" id="view_form" enctype="multipart/form-data"

      接著 我在submit的時候
      加上這兩行,再submit
      xhr.open('POST', 'publish_act.php');
      xhr.send(fd);//開始上傳

      但是我在 publish_act.php 這支
      print_r($_FILES["ff"]);
      卻沒有任何資料!!

      想請問 我是哪一段出問題
      是ff[]要先設定input嗎?
      還是 xhr.open('POST', 'publish_act.php'); 這一段有問題..


      我用另一隻js去做拖曳和預覽的動作,內容如下:
      ===========================================================================================
      // 要使用全域變數
      fd = new FormData($('view_form')[0]);
      xhr = new XMLHttpRequest();

      //xhr.open('POST', 'publish_detail_act.php'); //上傳到publish_detail_act

      function dragoverHandler(evt)
      {
      evt.preventDefault();
      }

      function dropHandler(evt)
      {
      evt.preventDefault();
      var files = evt.dataTransfer.files; //由DataTransfer物件的files屬性取得檔案物件

      var pic_count = $('#pic_area').children('li').length;

      for(var i=0;i<files.length;i++) {

      var pic_file = files[i];

      //將圖片在頁面預覽
      var fr = new FileReader();
      fr.onload = openfile;
      fr.readAsDataURL(files[i]);

      //新增上傳檔案,上傳後名稱為 ff 的陣列
      fd.append('ff[]', files[i]);
      }
      //xhr.send(fd);//開始上傳
      }

      function openfile(evt) // 預覽顯示
      {
      ......
      }

      function getMultiPic(input)
      {
      var files = input.files;

      if(typeof(FileReader) != 'undefined')
      {
      var pic_count = $('#pic_area').children('li').length;

      for(var i=0;i<files.length;i++) {

      var pic_file = files[i];

      //將圖片在頁面預覽
      var fr = new FileReader();
      fr.onload = openfile;
      fr.readAsDataURL(files[i]);

      //新增上傳檔案,上傳後名稱為 ff 的陣列
      fd.append('ff[]', files[i]);
      }

      //xhr.send(fd);//開始上傳
      }
      }
      ==========================================================================================

      刪除
    2. 您好,我做了個範例供您參考(目前範例server有問題,無法上傳,請自行參考程式碼在您的環境測試)
      http://demo.cinc.biz/html5-drag-drop-upload/preview-del.html
      範例是讀取預覽圖片的data URI進行上傳,
      另外我想如果記錄、刪除拖曳產生的file物件,應該也可以,但要想如何紀錄、刪除。

      刪除
    3. 謝謝你 我之前以為
      xhr.open('POST', 'publish_act.php');
      xhr.send(fd);//開始上傳
      然後在做 form.submit();
      兩邊的資訊 就會一起post到另一個頁面
      結果我錯了
      原來 xhr.send(fd) 是背景去執行...

      我改變做法了
      在拖曳或是選檔案時,先做xhr.send(fd) 存到暫存的資料夾
      然後本頁也存圖片的檔名
      submit之後
      再將檔名和暫存的資料夾裡面的檔案做比對
      在將圖片移到我要的目錄

      刪除
    4. 糗大了 在手機上 不能拖曳 囧rz...

      刪除
  10. 作者已經移除這則留言。

    回覆刪除
  11. 如何用成所有檔案都能上傳,不只有jpg這格式

    回覆刪除
    回覆
    1. 您好,JS 的部分是在這裡過濾掉了
      if (files[i].type == 'image/jpeg') {
      ...
      }


      至於 PHP 的部分,本文中的範例沒有過濾,
      但後面其他 DEMO 的程式,部分有過濾,也是修改有出現 jpg 或 jpeg 的地方。

      刪除