I have always wanted to do the function of uploading pictures, and today I finally realized this wish. During the production process, I also reviewed HTML5 File, Drop, Drag, URL, and FileReader, and won all the games

To view the demo

View the source code, welcome star

Let’s review a few ways to upload files

Form form

This is the common file upload method before HTML5 came out, it is through the page form to upload, the code is as follows

<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>Copy the code

Note the following attributes:

  • Action: URL to accept the request
  • Enctype: Request encoding type, default is Application/X-www-form-urlencoded, file upload set to multipart/form-data
  • Method: indicates the request method. Set this parameter to POST when uploading files
  • Multiple: Allows us to select multiple files at once
  • Accept: Sets the file type to be uploaded

In addition, set the type of our input tag to File, click it to open the file manager of the system, and click the upload button to send the selected file to the server

FormData & XHR2

In addition to using form forms to submit data, we can also build our own form data for submission, where FormData is used to create form data, which is HTML5 stuff, and XHR2 is used to send requests to the server

FormData object API:

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

That’s how I used it in the demo

const formData = new FormData()
files.forEach((file, index) => {
  formData.append(`img${index+1}`, file)
})
Copy the code

XHR2 is used to send requests and ajax implementation is based on it. It is because of XHR2 that it is possible to upload files through Ajax. XHR2 has the following characteristics compared to XHR:

  • You can set timeout
  • You can use FormData objects to manage data
  • Binary files can be uploaded
  • You can cross domain
  • You can obtain the progress of data transmission

For the use of XHR2, here is a demo, and for more details you can go to MDN, portal

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('File uploaded successfully')}else {
    alert('File upload failed')
  }
}
xhr.send(formData)
Copy the code

Fetch

Fetch is a new HTTP request method, which has replaced XHR2. It is also my favorite one, because it is not too cool to write code with Promise and Async/Await. You can use IT by 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('File uploaded successfully', respData);
}).catch(function(e) {
    console.log('File upload failed', e);
});

Copy the code

Back to the topic, let’s first analyze the core functions of the DEMO I made, and then explain how to implement each function in detail. The whole DEMO was written based on React

I have divided the functions:

  • Select file upload
  • Image thumbnail
  • Image deletion
  • Drag files to upload
  • Drag and drop files to delete
  • Preview picture

Select file upload

Here’s how it works: click on an Input, and a file selector pops up. We can select multiple images. When the selection is complete, the change event on the Input tag is triggered

The selected image data is stored in the Input files property. The value of the Files property is an array-like FileList object, so we cannot use the Array instance method directly

The core code is as follows:

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 = ' '})}Copy the code

Which I pass an Array. The prototype. The forEach. Call to invoke the forEach method of the Array code one line this. Last the fileInput. Value = ‘is to solve can’t upload a photo with, Because input internally checks to see if the file we uploaded is the same as the last one, the onchange event will not be triggered if it is the same

Image thumbnail

How to upload an image and display it? I’ve looked at some of the information here, one through FileReader, the other through URLS, so let’s talk about them separately

FileReader

What is FileReader, I quote from 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 reads files asynchronously from your computer. You can use File or Blob objects to specify which files to read

The File object can come from a FileList object returned after the input element selects a File, from a DataTransfer object returned after the Drag and Drop operations, or from a call to mozGetAsFile() using HTMLCanvasElement

FileReader attributes, functions, and events are listed here

  • attribute
    • Error: indicates an error occurred during file reading. Read-only
    • ReadState: 0- Data has not been loaded, 1- data is being loaded, 2- All read requests have been completed, read only
    • Result: The contents of the file will have a value only after the file has been read. The format of the data depends on the method called
  • function
    • Abort: Interrupts the read operation and readyState changes to 2 on return
    • ReadAsArrayBuffer: Reads the contents of a file and returns the format ArrayBuffer
    • ReadAsBinaryString: Reads the contents of a file in binary format
    • ReadAsDataURL: reads the file content and returns the format data:URL
    • ReadAsText: Reads the contents of a file and returns a format string
  • The event
    • Onabort: Triggered when the read is interrupted
    • Onerror: Triggered when a read error occurs
    • Onload: Triggered when the read is complete
    • Onloadstart: Triggered when reading starts
    • Onloadend: Triggered at the end of the read, success or failure
    • Onprogress: Triggered when the read progress changes

Now that we’ve introduced the FileReader API, let’s think about how to display a thumbnail image after uploading a file

After the file is uploaded, we get the uploaded file object and create a FileReader to read the uploaded file. After the file is successfully read, the content of the file will be saved in the Result of the FileReader. Then we create an IMG element to display the contents of the file we read

The core code is as follows:

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 =' '}})}Copy the code

The img element can directly display base64 encoded images, so we call readAsDataURL when reading the file. After the file is read successfully, the result value in fileReader is the file content in data:URL format. We can assign it directly to the SRC attribute of the IMG element. The onload event is triggered when the file is successfully read, so all our operations must be written in its callback function

URL

If you have any questions, I need to read my file locally, convert it to a base64 long string, but cannot provide an address directly to the SRC attribute of the IMG element. The answer is yes, we can do it by using URL objects, and that’s the way I recommend it

Let’s take a look at the URL object, which is described in 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).

The URL interface is used to create URL objects and provides static methods

When our environment does not implement the URL constructor, we can return an instance of the URL via window.url (window.webkitURL)

RevokeObjectURL revokeObjectURL createObjectURL revokeObjectURL createObjectURL revokeObjectURL createObjectURL revokeObjectURL

CreateObjectURL passes in a File or Blob object and returns a DOMString that can be used to display our content

RevokeObjectURL destroys the DOMString created by createObjectURL

So how do you do that? Look directly at the code

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 = ' '})}Copy the code

Const SRC = url.createObjecturl (file) const SRC = url.createObjecturl (file); const SRC = url.createObjecturl (file)

CreateObjectURL the returned string looks like: a blob: http://localhost:3000/81e8eaa9-3041-4c93-bd16-913f578ece42

Some other attributes and methods about URL are not listed in this demo. Interested students can take MDN to understand the portal

Image deletion

Image delete this function is very simple, click the delete button above the picture, pass in the corresponding index to delete method, and then find the corresponding file in the global files object according to index filter, return a new array

The core code is as follows:

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
    })
}
Copy the code

RevokeObjectURL revokeObjectURL revokeObjectURL revokeObjectURL revokeObjectURL revokeObjectURL revokeObjectURL revokeObjectURL

Drag and drop file upload & Drag and drop file delete

Drag file upload and Drag file delete are put together because they both require the Drag and Drop apis provided by HTML5. Let’s take a look at these two apis first

Drag and drop event

Some drag-and-drop events are triggered on the element being dragged, while others are triggered on the drop target

When we drag an element, it fires in sequence:

  • dragstart
  • drag
  • dragend

All three events are triggered on the dragged element. The dragStart event is triggered when the drag starts, and the drag event continues to be triggered during the drag process. The dragend event is triggered when the drag stops (regardless of whether the dragged element has been placed on a valid drop target). These three events are similar to the mouse movement events mouseStart, Mousemove, and mousemove. mouseend

When an element is dragged to the drop target, it fires in sequence:

  • dragenter
  • dragover
  • Dragleave or drop

All three events are triggered on the placement target. A DragEnter event is emitted when an element enters a drop target, a dragover event is emitted continuously when an element moves on a drop target, a Dragleave event is emitted when an element is placed on a drop target, These events (except drop) are similar to the mouse movement events mouseEnter, mouseover, and mouseleave

Block default behavior. Although all elements support drop, these elements are not allowed to be placed by default. In this case, releasing the mouse over the drop target will not trigger the drop, so we can prevent the default by event.preventDefault() :

droptarget.ondragenter = event => {
    event.preventDefault()
}
droptarget.ondragover = event => {
    event.preventDefault()
}
Copy the code

In some browsers, when we move an image to the placement target, releasing it opens the image, or if we move a hyperlink, the page opens. We sometimes need to prevent this default behavior, and we can do that

droptarget.ondrop = event => {
    event.preventDefault()
}
Copy the code

The dataTransfer object

The dataTransfer object, which is used to pass data from the dragged element to the placed target during a drag, has two methods, setData and getData

SetData takes two parameters, the first is the MIME type and the second is the value we want to save

event.dataTransfer.setData('text/plain'.'msg')
event.dataTransfer.setData('text/uri-list'.'http://baidu.com')
Copy the code

GetData has only one argument, which is the first argument we pass in setData

event.dataTransfer.getData('text/plain')
event.dataTransfer.setData('text/uri-list')
Copy the code

Keep in mind that setData is usually used in dragstart and getData is only used in drop events

dropEffect & effectAllowed

DropEffect and Effectalhoward are two attributes of the dataTransfer that determine what actions can be received by the dragged element and by the placed element

DropEffect must be paired with Effectalhoward to be effective, and we must set the values of these two properties in dragstart

Effectalhoward has the following values:

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

The values of dropEffect are as follows:

  • none
  • move
  • copy
  • link

draggable

Elements other than images, links, and text are not dragable by default. We need to add the draggable attribute to make this element dragable

Other members

In addition to the above methods and attributes, dataTransfer has the following methods and attributes:

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

We get dataTransfer in onDrop, store the file in its files property, and then use FormData object and XHR2 to transfer the data to the server. The core code is as follows:

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 upload file or 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('File uploaded successfully')}else {
        alert('File upload failed')
      }
    }
    // xhr.send(formData)
    alert('File uploaded successfully')
    this.setState({
      files: []
    })
    this.fileInput.value = ' '
}
Copy the code

Implementation idea of drag file deletion: in onDragstart, put the index of the dragged file into dataTransfer, and then take out the index in onDrop, and delete it in the global file list according to the index value. The core code is as follows:

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'
}
Copy the code

Preview picture

The image preview function is also very simple, similar to deleting. Click the corresponding image to pass in index, then find the corresponding file from the global files, and assign its SRC attribute value to the SRC attribute of a preview IMG element

The core code is as follows:

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
    })
}
Copy the code

After the image is displayed, bind a click event to the outer container. Click to hide the preview image

The last

To view the demo

View the source code, welcome star

Your rewards are my motivation to write