1. 程式人生 > >使用AJAX實現檔案拖拽上傳功能詳解

使用AJAX實現檔案拖拽上傳功能詳解

關注微信公眾號“編碼很酷”

概述

對於微雲、百度雲等網盤提供的檔案儲存服務而言,檔案上傳是一個重要功能。檔案上傳的方式主要有兩種:二進位制資料上傳、表單上傳。本文會詳細解析表單上傳的協議規範,前端上傳檔案的兩種方式:對話方塊選擇方式、拖拽選擇方式,服務端接收上傳的檔案以及檔案上傳功能的技巧等。

表單上傳協議詳解

RFC1867(https://www.ietf.org/rfc/rfc1867.txt) 規範了表單上傳的協議格式。下面給出一個例子,用Fiddler抓包工具,抓取同時上傳兩個字串內容和一個文字檔案的HTTP請求,獲取的請求內容如下:

POST http://localhost:8080/Server/uploadfile
HTTP/1.1
Host: localhost:8080 Connection: keep-alive Content-Length: 391 Cache-Control: no-cache Origin: chrome-extension://fdmmgilgnpjigdojojpjoooidkmcomcm User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryuA8AsEvrgV5BUqe5
Accept: */* Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.8 ------WebKitFormBoundaryuA8AsEvrgV5BUqe5 Content-Disposition: form-data; name="file1" value1 ------WebKitFormBoundaryuA8AsEvrgV5BUqe5 Content-Disposition: form-data; name="file2"; filename="test2.txt" Content-Type: text/plain hello world ------WebKitFormBoundaryuA8AsEvrgV5BUqe5
Content-Disposition: form-data; name="file3" value3 ------WebKitFormBoundaryuA8AsEvrgV5BUqe5--

根據HTTP協議規範,每個請求頭後面都需要追加回車和換行符(\r\n)。訊息頭和訊息體之間也需要插入回車和換行符,忽略其它的請求頭部,表單上傳的格式可簡化成如下程式碼,方便描述。

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryuA8AsEvrgV5BUqe5回車換行
回車換行
------WebKitFormBoundaryuA8AsEvrgV5BUqe5回車換行
Content-Disposition: form-data; name="file2"; filename="test2.txt"回車換行
Content-Type: text/plain回車換行
回車換行
hello world回車換行
------WebKitFormBoundaryuA8AsEvrgV5BUqe5回車換行
Content-Disposition: form-data; name="file3"回車換行
回車換行
value3回車換行
------WebKitFormBoundaryuA8AsEvrgV5BUqe5--回車換行

1.新增表單描述頭部

使用表單上傳功能,需要在頭部新增如下程式碼,其中“multipart/form-data”表示請求上傳的內容型別為表單,“boundary”表示分隔符,用於分割表單裡面的每項內容。

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryuA8AsEvrgV5BUqe5回車換行

2.新增表單內容

表單中每項內容的型別無外乎就兩種,一種是文字型別,另外一種是檔案型別。每項內容之間需要用“–+boundary+回車換行”進行分割,緊接著分隔符的程式碼用於描述內容配置。其中文字型別的內容需要新增如下格式的程式碼:

------WebKitFormBoundaryuA8AsEvrgV5BUqe5回車換行
Content-Disposition: form-data; name="file3"回車換行
回車換行
value3回車換行

“name”用於描述表單的欄位名稱,兩個回車換行之後就是這個欄位的值。檔案型別的內容跟文字型別對比多了兩個欄位,“filename”用於描述上傳的檔案的名稱,“Content-Type”用於描述上傳的檔案型別(檔案的MIME),檔案型別的內容需要新增如下格式的程式碼:

------WebKitFormBoundaryuA8AsEvrgV5BUqe5回車換行
Content-Disposition: form-data; name="file2"; filename="test2.txt"回車換行
Content-Type: text/plain回車換行
回車換行
hello world回車換行

新增完表單的每項內容之後,需要在後面追加“–+boundary+–+回車換行”,完成表單內容的拼接。

前端選擇檔案上傳的兩種方式

1.對話方塊選擇方式上傳

實現對話方塊選擇檔案,會用到如下程式碼:

    <form action="http://localhost:8080/Server/uploadfile" method="post" enctype="multipart/form-data">
        <br> 檔案:
        <input type="file" name="image">
        <br>
        <input type="submit" value="上傳">
    </form>

其中action欄位為檔案上傳的介面地址,enctype需要定義為“multipart/form-data”、input標籤的type屬性的值為“file”,對應的name為表單的欄位名稱。

2.拖拽選擇方式上傳

要實現這個功能,可藉助Html5新增的“Drag and drop”功能。W3C官方文件為:https://www.w3.org/TR/2014/CR-html5-20140731/editing.html。 利用它,我們可以知道檔案何時被拖動到目標區域、檔案何時離開目標區域、有哪些檔案被拖到了目標區域。接下來就具體聊聊“Drag and drop”功能。

如何知道檔案何時拖動到目標區域又何時離開目標區域?

HTML中的每個標籤都能夠設定跟拖動相關的事件,拖動事件的回撥函式解釋如下:

事件 描述
ondragstart 拖動操作開始時呼叫(部分瀏覽器不回撥此方法)
ondrag 拖動過程中呼叫(部分瀏覽器不回撥此方法)
ondragenter 剛拖動到目標元素區域時呼叫
ondragover 在目標元素區域內拖動時呼叫,此方法會隔一段時間呼叫一次
ondragleave 拖動離開目標元素區域時呼叫
ondragend 拖動結束時回撥(部分瀏覽器不回撥此方法)
ondrop 在目標元素區域內放開拖動內容時呼叫

註冊事件可以使用如下程式碼:

//element可以為HTML標籤、document
element.ondragstart = function(ev) {
    console.log('ondragstart');
}

注意:

瀏覽器預設在拖放完成時會開啟所拖放的檔案,正確的做法是要呼叫事件物件的preventDefault方法用來阻止事件的預設動作的執行。

//element可以為HTML標籤、document
element.ondragover = function(ev) {
    ev.preventDefault();
    //do something
}

如何獲取拖動的檔案?

上面所列舉的回撥函式,每個回撥函式裡面都有一個引數DragEvent,DragEvent的介面定義語言描述如下:

interface DragEvent : MouseEvent {
  readonly attribute DataTransfer? dataTransfer;
};

可以看到拖動事件介面繼承於滑鼠事件介面,其中有個屬性dataTransfer(資料傳輸者)用於傳輸拖動的內容,DataTransfer的介面定義語言如下:

interface DataTransfer {
           attribute DOMString dropEffect;
           attribute DOMString effectAllowed;

  readonly attribute DataTransferItemList items;

  void setDragImage(Element image, long x, long y);

  /* old interface */
  readonly attribute DOMString[] types;
  DOMString getData(DOMString format);
  void setData(DOMString format, DOMString data);
  void clearData(optional DOMString format);
  readonly attribute FileList files;
};

其中的setData方法用於設定要傳輸的內容,getData方法用於獲取傳輸的內容。當要實現從一個元素中拖動內容到另外一個元素區域時可以使用者兩個方法。拖動檔案時值需要使用files屬性,其值被瀏覽器設定進去了,因此只要獲取即可。那麼獲取files的最佳時機是什麼時候,當然是在ondrop方法回撥時最佳。

dz.ondrop = function(ev) {
    //阻止瀏覽器預設開啟檔案的操作
    ev.preventDefault();
    //表單上傳檔案...

}

如何上傳獲取到的檔案?

使用AJAX即可通過表單方式上傳檔案,附上前端拖拽上傳的完整程式碼。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="zh-CN">

<head>
    <title>HTML5拖拽上傳</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="description" content="" />
    <meta name="keywords" content="" />
    <style type="text/css">
    #dropzone {
        width: 300px;
        height: 300px;
        border: 2px dashed gray;
    }

    #dropzone.over {
        width: 300px;
        height: 300px;
        border: 2px dashed red;
    }
    </style>
</head>

<body>
    <div id="dropzone" dropEffect="link"></div>
</body>
<script type="text/javascript">
function uploadFile(formData) {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', 'http://localhost:8080/Server/uploadfile', true);
    xhr.send(formData);
}
var dz = document.getElementById('dropzone');
dz.ondragover = function(ev) {
    //阻止瀏覽器預設開啟檔案的操作
    ev.preventDefault();
    this.className = 'over';
}

dz.ondragleave = function() {
    this.className = '';
}

dz.ondrop = function(ev) {
    this.className = '';
    //阻止瀏覽器預設開啟檔案的操作
    ev.preventDefault();
    //表單上傳檔案
    var formData = new FormData();
    formData.append('file', ev.dataTransfer.files[0]);
    uploadFile(formData);
}
</script>

</html>

服務端處理上傳的檔案

完整程式碼如下:

package com.servlet;

import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

@WebServlet(name = "uploadfile", urlPatterns = "/uploadfile")
public class UploadFileServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        try {
            ServletContext servletContext = this.getServletConfig()
                    .getServletContext();
            // Create a factory for disk-based file items
            DiskFileItemFactory factory = new DiskFileItemFactory();
            // Set factory constraints
            String path = "D:\\upload";
            File uploadDir = new File(path);
            if (!uploadDir.exists()) {
                uploadDir.mkdirs();
            }
            factory.setRepository(new File(uploadDir.getAbsolutePath()));
            // Create a new file upload handler
            ServletFileUpload upload = new ServletFileUpload(factory);
            // Set overall request size constraint
            upload.setSizeMax(-1);
            // Parse the request
            List<FileItem> items = upload.parseRequest(req);
            // Process the uploaded items
            Iterator<FileItem> iter = items.iterator();
            while (iter.hasNext()) {
                FileItem item = iter.next();
                if (item.isFormField()) {
                    // 普通表單資料
                } else {
                    // 檔案表單資料
                    item.write(new File(uploadDir.getAbsolutePath()
                            + File.separator + item.getName()));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

檔案上傳功能的一些技巧

1.實現檔案秒傳功能

微雲、百度雲就含有檔案秒傳功能,其實現原理其實很簡單,檔案可以用其MD5來區分差異性。上傳檔案時計算檔案的MD5,只要伺服器上存在相同MD5值的檔案,則不會真正的上傳檔案,而是把網盤上檔案的索引儲存到當前使用者資訊中。所以一般網盤上不會出現MD5值相同的檔案。

2.防止可執行檔案注入攻擊

以tomcat伺服器為例,WEB-INF目錄可以被瀏覽器訪問。如果使用者將可執行的檔案如xx.jsp上傳到這個目錄,裡面編寫了刪除檔案目錄的程式碼,則當瀏覽器訪問這個xx.jsp檔案時,這段惡意程式碼就會被執行,這顯然是惡意攻擊。為了阻止這種行為,正確的做法是過濾掉可執行檔案,不讓其上傳,這種判斷前端和後端都需要做,前端做的目的是可以減輕服務端的判斷壓力,後端做是為了阻止模擬的HTTP請求上傳惡意檔案。

更多

長按下圖->識別圖中二維碼或者掃一掃關注我的公眾號。

微信公眾號