利用File,Drop&Drag,XHR2實現圖片拖拽上傳

劉源泉發表於2018-07-25

一直想做下圖片上傳的功能,今天終於把這個心願了了,在製作的過程中也順便把HTML5的File,Drop,Drag,URL,FileReader複習了下,多贏

image

檢視demo

檢視原始碼,歡迎star

我們先來回顧下檔案上傳有幾種方式

form表單

這個是HTML5出來之前普遍的檔案上傳方式,它是通過頁面的form表單進行上傳的,程式碼如下

<form action='upload.php' enctype='multipart/form-data' method='POST' name='form' id='form'>
    <input type='file' name='image' multiple accept='image/png'/>
    <button id='upload' type='submit'>上傳</button>
</form>
複製程式碼

注意下當中的幾個屬性:

  • action:接受請求的URL
  • enctype:請求的編碼型別,預設是application/x-www-form-urlencoded,檔案上傳時設定為multipart/form-data
  • method:請求的方法,檔案上傳時設定為POST
  • multiple:可以讓我們一次選擇多個檔案
  • accept:設定上傳的檔案型別

另外將我們的input標籤的type設定為file,點選之後就可以開啟系統的檔案管理器,單擊上傳按鈕就可以把我們選擇的檔案傳送到伺服器了

FormData & XHR2

除了使用form表單來提交資料外,我們還可以自己構建表單資料進行提交,其中FormData用來建立表單資料,是屬於HTML5的東西,XHR2用來傳送請求到伺服器

FormData物件API:

  • append
  • delete
  • set
  • get
  • getAll
  • has
  • keys
  • entries
  • values
  • forEach

我在demo中是這樣用的

const formData = new FormData()
files.forEach((file, index) => {
  formData.append(`img${index+1}`, file)
})
複製程式碼

XHR2是用來傳送請求的,ajax的實現就是靠它,正是因為XHR2的出現才使得通過ajax上傳檔案變成可能,XHR2相對於XHR有以下特點:

  • 可以設定timeout
  • 可以使用FormData物件管理資料
  • 可以上傳二進位制檔案
  • 可以跨域
  • 可以獲取資料傳輸的進度資訊

關於XHR2的使用,下面給出一個demo,更詳細的用法大家可以去MDN,傳送門

const formData = new FormData()
const xhr = new XMLHttpRequest()
xhr.timeout = 3000
xhr.open('POST', 'upload')
xhr.upload.onprogress = event => {
  if (event.lengthComputable) {
    const percent = event.loaded / event.total
    console.log(percent)
  }
}
xhr.onload = () => {
  if (xhr.status === 200 && xhr.readyState === 4) {
    alert('檔案上傳成功')
  } else {
    alert('檔案上傳失敗')
  }
}
xhr.send(formData)
複製程式碼

Fetch

終於說到Fetch了,Fetch是一種新的HTTP請求方式,替代了之前的XHR2,也是我個人比較喜歡的一種,因為它配合Promise,Async/Await寫起程式碼來簡直不要太爽了,關於它的使用大家可MDN

var form = new FormData(),
    url = 'http://.......', //伺服器上傳地址
    file = files[0];
form.append('file', file);
 
fetch(url, {
    method: 'POST',
    body: form
}).then(function(response) {
    if (response.status >= 200 && response.status < 300) {
        return response;
    } 
    else {
        var error = new Error(response.statusText);
        error.response = response;
        throw error;
    }
}).then(function(resp) {
    return resp.json();
}).then(function(respData) {
    console.log('檔案上傳成功', respData);
}).catch(function(e) {
    console.log('檔案上傳失敗', e);
});

複製程式碼

回到正題,我們先來分析下我所做的DEMO有幾個核心功能,然後針對每個功能去具體講解如何實現的,整個demo是基於react寫的

我把功能劃分了一下:

  • 選擇檔案上傳
  • 圖片縮圖
  • 圖片刪除
  • 拖拽檔案上傳
  • 拖拽檔案刪除
  • 圖片預覽

選擇檔案上傳

我的思路是這樣子的:點選一個input,彈出一個檔案選擇器,我們可以選取多張圖片,選擇完成後,會觸發input標籤的change事件,我們可以從input的元素files屬性裡拿到我們選擇的圖片資料,然後把它新增到一個全域性的陣列裡面

這裡注意一點就是:選擇的圖片資料儲存在input的files屬性裡面,files屬性的值是一個類似陣列的FileList物件,因此我們不能直接使用Array的例項方法

核心程式碼如下:

handleChange = event => {
    event.preventDefault()
    const { files } = this.state
    Array.prototype.forEach.call(this.fileInput.files, file => {
        const src = URL.createObjectURL(file)
        file.src = src
        this.setState({
            files: [...files, file]
        })
    this.fileInput.value = ''
    })
}
複製程式碼

其中我通過Array.prototype.forEach.call來呼叫陣列的forEach方法,程式碼最後有一行this.fileInput.value = '' 是為了解決不能上傳同一張圖片,因為input內部會去檢查我們上傳的檔案是否和上一次一樣,如果一樣是不會觸發onchange事件的

圖片縮圖

如何實現上傳一張圖片之後把它顯示出來呢?這裡我查閱了相關資料,一種是通過FileReader,另外一種是通過URL,我們們分別來講解下

FileReader

什麼是FileReader,我這裡引用MDN中的一段話

The FileReader object lets web applications asynchronously read the contents of files (or raw data buffers) stored on the user's computer, using File or Blob objects to specify the file or data to read.

File objects may be obtained from a FileList object returned as a result of a user selecting files using the element, from a drag and drop operation's DataTransfer object, or from the mozGetAsFile() API on an HTMLCanvasElement.

大致意思就是FileReader可以非同步讀取電腦上的檔案內容,我們可以使用File或者Blob物件來指定讀取的檔案

File物件可以來自input元素選擇檔案後返回的FileList物件,也可以來自使用Drag和Drop操作後的DataTransfer物件,或者是使用HTMLCanvasElement呼叫mozGetAsFile()返回的結果

關於FileReader的屬性,函式,事件這裡列舉下

  • 屬性
    • error: 表示讀取檔案時發生的錯誤,只讀
    • readState: 0-表示還沒有載入資料,1-正在載入資料,2-已完成全部的讀取請求,只讀
    • result: 檔案的內容,只有在檔案讀取完成之後才有值,資料的格式取決於呼叫的方法
  • 函式
    • abort: 中斷讀取操作,返回時readyState變為2
    • readAsArrayBuffer: 讀取檔案內容,返回格式ArrayBuffer
    • readAsBinaryString: 讀取檔案內容,返回格式二進位制
    • readAsDataURL: 讀取檔案內容,返回格式data:URL
    • readAsText: 讀取檔案內容,返回格式字串
  • 事件
    • onabort: 讀取被中斷時觸發
    • onerror: 讀取發生錯誤時觸發
    • onload: 讀取完成時觸發
    • onloadstart: 讀取開始時觸發
    • onloadend: 讀取結束時觸發,成功或者失敗
    • onprogress: 讀取進度改變時觸發

介紹完了FileReader的API之後,我們想一下如何實現檔案上傳後顯示圖片的縮圖

思路其實也特別簡單,就是檔案上傳之後,我們獲取到上傳的檔案物件,然後建立一個FileReader去讀取我們上傳的檔案,讀取成功之後我們的檔案內容會儲存在FileReader中的result中,然後我們建立一個img元素去顯示我們讀取的檔案內容就可以了

核心程式碼如下:

handleChange = event => {
    event.preventDefault()
    const { files } = this.state
    Array.prototype.forEach.call(this.fileInput.files, file => {
      const reader = new FileReader()
      reader.readAsDataURL(file)
      reader.onload = event => {
        file.src = reader.result
        this.setState({
          files: [...files, file]
        })
        this.fileInput.value = ''
      }
    })
  }
複製程式碼

其中因為img元素是可以直接顯示base64編碼的圖片的,所以我們在讀取檔案的時候呼叫的是readAsDataURL,檔案讀取成功後,fileReader中的result的值就是data:URL格式的檔案內容,我們可以直接將它賦值給img元素的src屬性,檔案讀取成功會觸發onload事件,所以我們的操作都必須寫在其回撥函式裡面

URL

使用了FileReader來讀取檔案不知道你有沒疑問,我的檔案就在我本地,我什麼還要讀取它,轉成一個base64那麼長的字串,不能直接提供一個地址給img元素的src屬性嗎?答案是可以的,借住URL物件我們可以實現,這也是我推薦用的方式

先來了解下URL物件,MDN上是這樣介紹它的

The URL interface represents an object providing static methods used for creating object URLs.

When using a user agent where no constructor has been implemented yet, it is possible to access such an object using the Window.URL properties (prefixed with Webkit-based browser as Window.webkitURL).

URL介面用來建立URL物件,該介面提供了一些靜態方法

當我們的環境沒有實現URL的建構函式時,我們可以通過Window.URL(Window.webkitURL)來返回一個URL例項

說白了這個URL其實就是一個工具類,用來處理我們的url的,如獲取host,pathname,hash等引數,我們用到的倒不是這個,我們這裡用到的是URL中的兩個靜態方法createObjectURL和revokeObjectURL

createObjectURL傳入一個File或者Blob物件,返回一個DOMString,這個字串可以用來展示我們的內容

revokeObjectURL用來銷燬通過createObjectURL建立的DOMString

具體該怎麼做呢?直接看程式碼

handleChange = event => {
    event.preventDefault()
    const { files } = this.state
    Array.prototype.forEach.call(this.fileInput.files, file => {
        const src = URL.createObjectURL(file)
        file.src = src
        this.setState({
            files: [...files, file]
        })
        this.fileInput.value = ''
    })
  }
複製程式碼

程式碼就一句話:const src = URL.createObjectURL(file),返回的src直接可以賦值給img的src屬性,給FileReader不知道方便了多少

createObjectURL返回的字串長這個樣子的:blob:http://localhost:3000/81e8eaa9-3041-4c93-bd16-913f578ece42

關於URL其它的一些屬性和方法在此次demo中暫時用不到就不列舉出來了,感興趣的同學可以取MDN瞭解下,傳送門

圖片刪除

圖片刪除這個功能就很簡單了,點選圖片上方的刪除按鈕,傳入對應的index到刪除方法,然後根據index在全域性的files物件中找到對應的file過濾掉,返回一個新的陣列

核心程式碼如下:

handleDelete = event => {
    event.preventDefault()
    event.stopPropagation()
    const { target: { dataset: { index } } } = event
    const { files } = this.state
    
    const newFiles = files.filter((file, index2) => {
      if (index2 === +index) {
        URL.revokeObjectURL(file.src)
        return false
      }
      return true
    })
    this.setState({
      files: newFiles
    })
}
複製程式碼

這裡記住以下刪除圖片的同時,呼叫URL.revokeObjectURL方法刪除對應的URL例項,節省記憶體,當然你不這樣做也沒什麼問題

拖拽檔案上傳 & 拖拽檔案刪除

把拖拽檔案上傳和拖拽檔案刪除放在一起說是因為它們兩個功能都需要用到HTML5提供的Drag和Drop API,我們們先來學習下這兩個API

拖放事件

關於拖放事件有些是在被拖動元素上觸發的,而有些則是在放置目標上觸發的

當我們拖動某個元素時,會依次觸發:

  • dragstart
  • drag
  • dragend

這三個事件都是在被拖動元素上觸發的。當拖動開始時會先觸發dragstart事件,然後在拖動的過程中會持續觸發drag事件,當拖動停止時(無論被拖動元素是否放到了有效的放置目標)都會觸發dragend事件,這三個事件類似滑鼠的移動事件mousestart,mousemove,mouseend

當某個元素被拖動到放置目標上,會依次觸發:

  • dragenter
  • dragover
  • dragleave 或 drop

這三個事件都是在放置目標上觸發的。當元素進入放置目標時會觸發dragenter事件,當元素在放置目標上移動時會持續觸發dragover事件,當元素移出放置目標時會觸發dragleave事件,當元素被放到了放置目標中會觸發drop事件而不是dragleave事件,這幾個事件(除drop)也類似滑鼠的移動事件mouseenter,mouseover,mouseleave

阻止預設行為。雖然所有的元素都支援drop事件,但是這些元素預設是不允許放置的,這個時候當我們在放置目標上鬆開滑鼠是不會觸發drop事件的,我們可以通過event.preventDefault()來阻止預設的行為,如下:

droptarget.ondragenter = event => {
    event.preventDefault()
}
droptarget.ondragover = event => {
    event.preventDefault()
}
複製程式碼

另外在一些瀏覽器中,當我們移動圖片到放置目標上,鬆開的時候會開啟這張圖片,如果移動的是超連結,則會開啟這個頁面。我們有時候需要阻止這種預設的行為,可以這樣做

droptarget.ondrop = event => {
    event.preventDefault()
}
複製程式碼

dataTransfer物件

dataTransfer物件用來在拖動的過程中從被拖動元素向放置目標傳遞資料,這個物件有兩個方法setData和getData

setData有兩個引數,第一個是MIME型別,第二個則是我們要儲存的值

event.dataTransfer.setData('text/plain', 'msg')
event.dataTransfer.setData('text/uri-list', 'http://baidu.com')
複製程式碼

getData只有一個引數,就是setData中我們傳的第一個引數

event.dataTransfer.getData('text/plain')
event.dataTransfer.setData('text/uri-list')
複製程式碼

setData我們一般在dragstart中去使用,而getData只能在drop事件中去使用,這個務必記住

dropEffect & effectAllowed

dropEffect和effectAllowed是dataTransfer的兩個屬性,用來確定個被拖動的元素以及作為放置目標的元素能夠接收什麼操作

dropEffect必須搭配effectAllowed才有效果,我們必須在dragstart中設定這兩個屬性的值

effectAllowed的取值如下:

  • uninitialized
  • none
  • copy
  • link
  • move
  • copyLink
  • copyMove
  • linkMove
  • all

dropEffect的取值如下:

  • none
  • move
  • copy
  • link

draggable

除了圖片,連結,文字之外的元素預設是不可以拖動的,我們需要新增draggable屬性就可以讓這個元素變得可以拖動

其它成員

dataTransfer除了上述的方法和屬性,還有以下方法和屬性:

  • addElement(element)
  • clearData(format)
  • setDragImage(element, x, y)
  • types

拖拽檔案上傳的實現思路:我們在ondrop中拿到dataTransfer,檔案就存放到其files屬性中,然後使用FormData物件和XHR2把資料傳遞給伺服器,核心程式碼如下:

handleDrop = event => {
    event.preventDefault()
    event.stopPropagation()
    const { files } = this.state
    Array.prototype.forEach.call(event.dataTransfer.files, file => {
      const src = URL.createObjectURL(file)
      file.src = src
      this.setState({
        files: [...files, file]
      })
      this.fileInput.value = ''
    })
}
handleUpload = event => {
    event.preventDefault()
    const { files } = this.state 
    if (files.length === 0) {
      this.fileInput.click()
      return
    }
    const formData = new FormData()
    files.forEach((file, index) => {
      formData.append(`img${index+1}`, file)
    })
    // xhr2上傳檔案 或者 fetch
    const xhr = new XMLHttpRequest()
    xhr.timeout = 3000
    xhr.open('POST', 'upload')
    xhr.upload.onprogress = event => {
      if (event.lengthComputable) {
        const percent = event.loaded / event.total
        console.log(percent)
      }
    }
    xhr.onload = () => {
      if (xhr.status === 200 && xhr.readyState === 4) {
        alert('檔案上傳成功')
      } else {
        alert('檔案上傳失敗')
      }
    }
    // xhr.send(formData)
    alert('檔案上傳成功')
    this.setState({
      files: []
    })
    this.fileInput.value = ''
}
複製程式碼

拖拽檔案刪除的實現思路:在ondragstart中把拖動的檔案的索引放到dataTransfer中,然後在ondrop中取出索引,根據索引值在全域性的檔案列表中進行刪除,核心程式碼如下:

handleDustDrop = event => {
    event.preventDefault()
    const { dataTransfer } = event
    const index = dataTransfer.getData('text/plain')
    const { files } = this.state
    let deleteFile 
    const newFiles = files.filter((file, index2) => {
      if (index2 === +index) {
        deleteFile = file.name
        URL.revokeObjectURL(file.src)
        return false
      }
      return true
    })
    this.setState({
      files: newFiles,
      deleteFile
    })
    event.currentTarget.style.borderColor = '#cccccc'
}
複製程式碼

圖片預覽

圖片預覽的功能也非常簡單,跟刪除差不多,點選對應的圖片傳入index,然後從全域性的files中找到對應的file,將其src屬性的值賦值給一個預覽的img元素的src屬性即可

核心程式碼如下:

showPreview = event => {
    const { currentTarget: { dataset: { index } } } = event
    const { files } = this.state
    this.setState({
      preview: true,
      previewImg: {
        name: files[+index].name,
        src: files[+index].src
      }
    })
}
hidePreview = event => {
    this.setState({
      preview: false,
      previewImg: null
    })
}
複製程式碼

圖片顯示之後,給外層容器繫結一個點選事件,單擊讓預覽圖片隱藏

最後

檢視demo

檢視原始碼,歡迎star

你們的打賞是我寫作的動力

微信
支付寶

相關文章