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:
其他:

38 則留言:

  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;
      }
      .....

      刪除