preface

Uploading images to create thumbnails is a common requirement, and there are many articles on the web. But most of them just throw out a bunch of code without much explanation, without noting the points to note, without explaining the pros and cons, and without extending the scenario.

What I’ve written here expands this article not only on the need to generate thumbnails, but also on a complete practical process for validating uploaded files, deleting selected files, and submitting uploaded files.

Because many online simply solve a generated thumbnail solution, but ignore the application of the actual scene, there are still some shortcomings in many cases, here to do a small optimization and do the next few methods of comparison, there are also some attention points mentioned, to help you avoid pits. I read a lot of articles that just say generate thumbnails, but don’t tell you how to send data together.

A good memory is better than a bad pen. People will forget it when they are old. It is better to write it down before my old man goes bald.

demand

First of all, the requirements should be clear, and the plan can be determined according to the requirements:

Click the upload tool, and the dialog box for uploading files will pop up. Single or multiple files can be selected. After this selection, the luo diagram of the file will be generated (this paper mainly aims at picture format files), and the selected files will be added on the basis of the original selection. Finally, upload each selected file together.

It is important to note that not every selection in this requirement overwrites the last selection. Instead, it keeps the results of the last selection and submits the results of each selection. To delete a selected file, delete the thumbnail to delete the selected file.

Point description of requirement details:

  1. Record the results of each selection and submit them together
  2. Each selection can be multiple
  3. Validates the selected file
  4. Generate thumbnails based on selected files (random or based on selection order)
  5. Click on the thumbnail for a preview (not expanded in the article, because it’s easy to control CSS styles)
  6. Deleting a thumbnail represents deleting the selected file

Generate thumbnails

To generate a thumbnail, all you have to do is show the “graph.” There are two ways to display “images”, one is to use the IMG tag to display the image directly, and the other is to use background-image style to display the image. So this generated thumbnail right here, you can go either way.

Here is an example of an img tag:

/** * generate preview container * @param {String} url - the url of the file * @param {Function} cb - The Function to execute when the image is loaded */
 function createPreviewWrap (url, cb) {
    let imgWrap = document.createElement('div');
    let image = new Image();
    imgWrap.className = 'img-wrap';
    image.src = url;
    cb && (image.onload = cb);
    // I put it in imgWrap to get more control over the style of the thumbnails
    // In fact, background-image rendering gives you more control over how the image is displayed
    imgWrap.appendChild(image);
    // Generate delete thumbnail icon, please add your own style
    const deleteIcon = document.createElement('i');
    deleteIcon.onclick = deleteFile; The deleteFile function is explained later in the respective schema
    imgWrap.appendChild(deleteIcon);
    // img-container is the label used to place the thumbnail, such as div
    document.getElementById('img-container').appendChild(imgWrap);
}
Copy the code

But in order to be able to display the “graph”, the first requirement of both ideas is to know the “source” of the image, that is, the path of the image. Therefore, to achieve the problem of generating thumbnails, it is transformed into the problem of generating image paths.

The label used to upload the file is

<input type="file" />
Copy the code

So, you’re dealing with file types (subclasses of Blob types), and there are two ways to generate urls for file types

  • FileReader
  • URL.createObjectURL

Using FileReader

With FileReader, the generated url is a Base64 encoded URL (Data URL), so the length is relatively large.

Basic usage:

var fileReader = new FileReader(); Create a new FileReader
fileReader.readAsDataURL(blob); // The bloB is read as a data URL
 // This. Result is the Data Url of the file
fileReader.onload = function() {
    console.log(this.result);
};
Copy the code

Now that you know the basics, it’s easy to generate urls for uploaded files:

* @param {FileList} files - Select a list of uploaded files */
function createThumbnailRandom (files) {
    for (let i = 0; i < files.length; i++) {
        let fileReader = new FileReader();
        fileReader.readAsDataURL(files[i]);
        fileReader.onload = function() {
            createPreviewWrap(this.result); }; }}Copy the code

Since you can select multiple files when uploading files, files in the above method is a FileList type, an array of classes, and you need to iterate through it to generate urls one by one. Notice that my comment says “generate by read speed”, because it says that fileReader reads asynchronously, so this method does not control the read order.

If you want to follow the order, you can use the following method:

* @param FileReader * @param {FileList} files - Select the list of files * @param {Number} index - Files Subscript */ to be processed
function createThumbnail (fileReader, files, index) {
    let i = index || 0;
    if (i >= files.length) {
        return;
    }
    fileReader.readAsDataURL(files[i]);
    fileReader.onload = function() {
        createPreviewWrap(this.result);
        createThumbnail(fileReader, files, ++i);
    };
}
Copy the code

I’m going to do it recursively and I’m just going to read the next file after each read, and I’m going to control the order.

Note that input of type file, when you upload a file, is double-selected. The default order of FileList is in ascending order of names.

We combine the above two methods into one method:

@param {FileList} files - List of files to upload * @param {Number} type-1: displays in sequence 2: displays in speed */
function fileReaderPreview (files, type) {
    // Internet Explorer 9 and below does not support FileReader
    if(! FileReader) { alert('Your browser does not support uploads, please change or upgrade your browser! ');
    }
    let type = type || 1;
    if (type === 1) {
        let fileReader = new FileReader();
        // Display thumbnails in the order of the files in the file selector
        createThumbnail(fileReader, files);
    } else if (type === 2) {
        createThumbnailRandom(files)
    }
}
Copy the code

Using createObjectURL

Grammar:

window.URL.createObjectURL(blob)
Copy the code

Parameter is Blob type and returns Objcet URL (Blob URL)

This method is simple and straightforward, without saying more, directly look at the code:

@param {FileList} files - Select a list of files to upload */
function objectURLPreview (files) {
    CreateObjectURL is not supported under Internet Explorer 9
    for (let i = 0; i < files.length; i++) {
        const url = window.URL.createObjectURL(files[i]);
        createPreviewWrap(url, function (){
            // Free url memory
            // If background-image is used to render the image, the new image object is also used to load the image
            // Listen for the onload event and assign the URL to background-image, then free the memoryURL.revokeObjectURL(url); }); }}Copy the code

summary

Since there are two ways, which one is better? Let’s do a quick comparison.

With:

  • Only Internet Explorer 10 or later is supported

Vision:

The execution time

  • FileReader is executed asynchronously, asynchronously when a file is read
  • CreateObjcetURL is executed synchronously

The memory of

  • The url generated by FileReader is a Base64 encoded address, which generally takes up more memory than createObjcetURL. But it automatically frees memory when it is no longer useful (the browser’s memory reclamation mechanism)
  • The memory created by createObjcetURL will not be released until the page is refreshed/closed or revokeObjcetURL is executed

There’s still a bit of uncertainty, but when I look up some people have reported that FileReader doesn’t generate preview images on some Android phones, but createObjectURL does, and createObjectURL is a little better. I have not yet verified this point, so please look at it dialectically.

Verify (limit) uploaded files

This article focuses on uploading image types and then generating thumbnails. For other types of file uploads, they are basically the same, except that thumbnail images may need extra processing, such as finding a fixed picture to represent a class of files, etc. Video uploading to generate screenshots is also a knowledge worth sharing, but it is not included in this article.

Anyway, we can put some restrictions on uploading files in HTML

<! -- Use label instead of input.file as visual upload interaction trigger to facilitate style unification and beautification -->
<label>Select Upload file<! The accept attribute only filters the file types displayed in the file selector by default, but does not actually prevent uploads of other types.
    <input type="file" id="file" multiple accept="image/*">
</label>
Copy the code

For Unique file Type specifiers, see Unique File Type specifiers

As noted in the comments, restrictions in JS are the real restrictions

@param {FileList} files - Select a list of uploaded files */
function verifyType (files) {
    for (let i = 0; i < files.length; i++) {
        // The type attribute is determined by the file name extension, so if you change the file name extension, the type value will also change
        // Therefore, it is not recommended to use type as the only judgment condition
        if (!/^image\//.test(files[i].type)) {
            return false; }}return true;
}
Copy the code

Select upload

With the above two foundations in mind, let’s handle the upload selection operation to satisfy the requirement of “multiple selection submit”.

First of all, we need to know that upload with input type as file will overwrite the previously selected content every time you open the file selection box for file selection. If you do not do any processing and submit data, only the last selected file will be submitted. So we need to have a variable that keeps track of every file we select.

<label>Select Upload file<input type="file" id="file" multiple accept="image/*">
</label>
Copy the code
// Record all files uploaded
var uploadFiles = [];

// Listen for upload selection
document.getElementById('file').addEventListener('change', upload, false);

/** * select */ after uploading the file
function upload (ev) {
    let e = ev || event;
    const files = e.target.files;
    if(! files.length) {return;
    }
    // Check the uploaded file type
    if(! verifyType(files)) { alert('Please upload file in image format');
        return;
    }

    Array.prototype.push.apply(uploadFiles, files);
    
    // Generate a preview image, select objectURL here
    objectURLPreview(files);
}
Copy the code

Submit data

Everything is in place, and the last step, of course, is to submit the data to the background. Without this step, everything is meaningless.

When it comes to submitting data, the traditional way is to submit data using a form form.

Use the form form directly

Simple version of

From simple to difficult, although the program is not the main content of this article, but still a brief introduction, after all, you search the network information, there is the recommendation of this program, but did not put forward the deficiencies of the program. I’ll write it as a warning.

Along with some small knowledge points, you can pay attention to.

<! Enctype ="multipart/form-data" encType ="multipart/form-data" -->
<! Select * from 'post' where 'get' = 'post';
<form action="http://example.com" method="POST" enctype="multipart/form-data">
    <label>Upload a file<! The form element must be named, otherwise the data will not be submitted to the background.
        <input type="file" id="uploadFiles" name="uplpoadFiles" multiple accept="image/*" onchange="upload()">
    </label>
    <! -- The default type attribute of a button is submit, but for compatibility with IE (default is Button), it is better to explicitly write type as submit.
    <! If type is button, but you bind onclick="submit()", even if you have a function called submit, you still treat it as a form submission, -->
    <! Type ="submit";
    <button type="submit">submit</button>
</form>
Copy the code

This is one of the simplest HTML submission functions that can be implemented using forms.

Note, however, that an input can only record the last batch of files selected. For example, if you click “Upload file”, a selection window will pop up. After selecting a batch of files, click “OK”, and the window will close. Then you click “Upload file” again, repeat the process, and so on. The input with the id uploadFiles will be submitted only as the last batch of files selected in the above procedure.

In order to cooperate with such behaviour, we need to modify the above the upload function, just delete one line of code Array. The prototype. Push. Apply (uploadFiles, enclosing files). It is good.

But this is not what we want, we want to generate thumbnails after each selection, and then commit each batch of selected files. At this point, we need to upgrade the above code.

Modified and upgraded version

An input can only represent a batch of files, so if you want to commit a batch of files, you should create several new input files. As long as the selected input is hidden after each batch of files is selected and the preview image is generated, a new input is created, each time.

Ugly words to say in the former, in the case of allowing an input multiple selection, because the behavior of the file upload input is if you choose wrong or go back on your word, it can be re-selected, the latter one overwrite the previous one, itself is not equipped with delete the selected file function. So one disadvantage of this scheme is that you can’t delete the selected files. This is obviously unacceptable, and the next best thing is to limit each input to a radio file so that the corresponding input is deleted when the thumbnail is deleted.

<form id="myForm" action="http://example.com" method="POST" enctype="multipart/form-data">
    <label>Upload a file<input type="file" id="upload1" name="uplpoadFiles[]" accept="image/*" onchange="upload()">
    </label>
    <button id="submitBtn" type="submit">submit</button>
</form>
Copy the code

Above is the HTML section, we select the upload file, trigger the upload method, add the input hide and add.

/** * select */ after uploading the file
function upload (ev) {
    let e = ev || event;
    const files = e.target.files;
    if(! files.length) {return;
    }
    // Check the uploaded file type
    if(! verifyType(files)) { alert('Please upload file in image format');
        return;
    }

    // Hide and generate a new input
    e.target.parentNode.style.display = 'none';
    const form = document.querySelector('#myForm');
    const label = document.createElement('label');
    label.innerText = 'Upload file';
    const input = document.createElement('input');
    input.type = 'file';
    input.name = 'uplpoadFiles[]';
    input.accept = 'image/*';
    input.onchange = upload;
    label.appendChild(input);
    form.insertBefore(label, document.querySelector('#submitBtn'));
    
    // Generate a preview image, where the function is adjusted and explained below
    objectURLPreview(files, e.target.parentNode);
}
Copy the code

Deletes selected files and thumbnails

Next, if you delete the thumbnail, you need to delete the corresponding input. How do we find the corresponding input? We make a simple adjustment to createPreviewWrap and objectURLPreview above, Pass the label node (e.target.parentNode in the upload method above) and thumbnail node as parameters and delete them.

/** * createObjectURL thumbnail * @param {FileList} files - Select the list of uploaded files * @param label- The label of the package input to be deleted */
function objectURLPreview (files, label) {
    for (let i = 0; i < files.length; i++) {
        const url = window.URL.createObjectURL(files[i]);
        createPreviewWrap(url, label, function (){ // Major changes hereURL.revokeObjectURL(url); }); }}/** * Generate preview container * @param {String} url - file url * @param {String} label - Input corresponding label * @param {Function} cb - Execute function */ when image load is complete
 function createPreviewWrap (url, label, cb) {
    let imgWrap = document.createElement('div');
    let image = new Image();
    imgWrap.className = 'img-wrap';
    image.src = url;
    cb && (image.onload = cb);
    imgWrap.appendChild(image);
    const deleteIcon = document.createElement('span');
    deleteIcon.onclick = (a)= >{deleteFile(label, imgWrap); }// The main change is here
    imgWrap.appendChild(deleteIcon);
    document.getElementById('img-container').appendChild(imgWrap);
}
Copy the code

The corresponding node deletion function

/** * Delete corresponding input label and thumbnail * @param label - Corresponding input label * @param imgWrap - Thumbnail to be deleted */
function deleteFile (label, imgWrap) {
    document.querySelector('#myForm').removeChild(label);
    document.getElementById('img-container').removeChild(imgWrap);
}
Copy the code

summary

Summarize the disadvantages of the scheme:

  • You can select only one file at a time
  • Frequent DOM manipulation may cause redrawing or rearrangement

Throw a demo

Send the request submission with XMLHttpRequest

In the solution here, we can build on the above (excluding the content of the form submission solution) and meet all the requirements without settling for anything less.

Traditionally, forms are used to submit the content of uploaded files. So if you don’t want to submit the form directly in HTML, you can use the FormData object to indicate that you have a piece of form data, so you can send the request to submit the form data using XMLHttpRequest instead of writing the form tag.

The HTML section could look like this:

<div id="img-container"></div>
<div>
    <label>Select Upload file<input type="file" id="file" multiple accept="image/*">
    </label>
    <button onclick="submitFormData()">submit</button>
</div>
Copy the code

The submit button binds a method to submit the uploaded data:

function submitFormData () {
    if (uploadFiles.length === 0) {
        alert('Please select file');
        return;
    }
    // FFormData Is not supported under Internet Explorer 10
    let formData = new FormData();
    uploadFiles.forEach(item= > {
        formData.append('uplpoadFiles[]', item);
    });
    let xhr = new XMLHttpRequest();
    xhr.open('POST'.'http://example.com');
    xhr.send(formData);
    xhr.onload = function () {
        if (this.status === 200 || this.status == 304) {
            alert('Upload successful');
        } else {
            alert('Upload failed'); }}}Copy the code

Deletes selected files and thumbnails

Similarly, deleting under this scenario requires tweaking the two functions that generate thumbnails, again using objectURLPreview as an example.

We need to find the corresponding element in the uploadFiles array that we want to delete, and then delete it, and here we use the array subscript to make sure we find it. (The main adjustment is in the comments)

function objectURLPreview (files) {
    const index = uploadFiles.length - files.length; // Find the base value used to calculate the subscripts for this batch of files
    for (let i = 0; i < files.length; i++) {
        const url = window.URL.createObjectURL(files[i]);
        // Base value + The subscript of this batch is the subscript in the entire uploadFiles array
        createPreviewWrap(url, index + i, function (){ URL.revokeObjectURL(url); }); }}function createPreviewWrap (url, index, cb) {
    let imgWrap = document.createElement('div');
    let image = new Image();
    imgWrap.className = 'img-wrap';
    image.src = url;
    cb && (image.onload = cb);
    imgWrap.appendChild(image);
    const deleteIcon = document.createElement('span');
    deleteIcon.onclick = (a)= >{deleteFile(index, imgWrap); }// The main change is here
    imgWrap.appendChild(deleteIcon);
    document.getElementById('img-container').appendChild(imgWrap);
}
Copy the code

The delete function is

function deleteFile (index, imgWrap) {
    uploadFiles.splice(index, 1);
    document.getElementById('img-container').removeChild(imgWrap);
}
Copy the code

Finally, drop a demo to see a complete example

conclusion

No matter which solution in the whole article, compatibility is required in IE10 contains more than 10 to work properly.

Please do not reprint without permission