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

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

image

查看demohtml

查看源碼,歡迎starreact

咱們先來回顧下文件上傳有幾種方式git

form表單

這個是HTML5出來以前廣泛的文件上傳方式,它是經過頁面的form表單進行上傳的,代碼以下github

<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>
複製代碼

注意下當中的幾個屬性:web

  • action:接受請求的URL
  • enctype:請求的編碼類型,默認是application/x-www-form-urlencoded,文件上傳時設置爲multipart/form-data
  • method:請求的方法,文件上傳時設置爲POST
  • multiple:可讓咱們一次選擇多個文件
  • accept:設置上傳的文件類型

另外將咱們的input標籤的type設置爲file,點擊以後就能夠打開系統的文件管理器,單擊上傳按鈕就能夠把咱們選擇的文件發送到服務器了ajax

FormData & XHR2

除了使用form表單來提交數據外,咱們還能夠本身構建表單數據進行提交,其中FormData用來建立表單數據,是屬於HTML5的東西,XHR2用來發送請求到服務器json

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

大家的打賞是我寫做的動力

微信
支付寶
相關文章
相關標籤/搜索