前端实现多文件上传及请求并发控制(七)

1,411 阅读6分钟

如何实现选择多个文件

通过input元素

通常情况下一个<input type="file">元素只能选择一个文件,如果我们需要让一个file类型的input可以选择多个文件,只需要在其元素上添加一个multiple属性即可。

<input type="file" multiple>
<script>
    let selectFile = document.querySelector('input');
    selectFile.addEventListener('change',(e)=>{
        console.log(e.target.files)
    })
</script>

此时可以点击选择框以选择多个文件,我们可以通过input元素上的files获取到一个FileList对象里面包含了我们选中文件的信息。

通过拖拽API

要使用拖拽API,我们首先就要得到一个可拖拽目标,在html中我们可以将一些容器变成可拖拽目标,例如我们将一个div变为可拖拽目标

<div class="box"></div>
<script>
    let box = document.querySelector('.box');
    // 拖拽放手
    box.addEventListener('drop',(e)=>{
        e.preventDefault();
        console.log('drop');
    })
    // 拖拽进入,类似于mouseenter
    box.addEventListener('dragenter',(e)=>{
        e.preventDefault();
        console.log('dragenter');
    })
    // 拖拽覆盖,类似于mouseover
    box.addEventListener('dragover',(e)=>{
        e.preventDefault();
        console.log('dragover');
    })
</script>

将一个元素变为可拖动目标我们只需要阻止拖拽的默认行为即可,当我们将一个容器变成一个可拖动目标后我们就可以利用其drop事件。

box.addEventListener('drop',(e)=>{
    e.preventDefault();
    console.log('drog',e.dataTransfer.files);
})

当我们将本地的文件拖拽到该div上并放手时,我们就可以通过拖拽事件对象上的dataTransfer中的files属性获取到所拖拽的文件列表了,该filesinput.files类似是一个FileList

以上两种方式都可以获取到所需的文件列表,同时我们要知道的是<input type="file">本身就是一个可拖动目标,并且将文件拖拽到input上时会将文件存储在input元素的files属性上。当我们获取到了文件列表就可以进行多文件上传了。

请求并发控制

多文件上传的两种方式

在了解并发之前我们可以想想为什么要并发,在我们进行多文件上传时通常有两种上传方式,

一种是将所有文件统一形成一个请求进行上传,另一种是将所有的文件都当成一个个单文件进行上传。

  • 合并上传

    在上一篇中(JS获取本地文件并进行网络传输(六) - 掘金 (juejin.cn))我们了解到了JS中的FormData对象,利用其实例上的append方法就可以将多个文件形成一个个字段进行上传了,例如:

    function mergeUpload(files){
        const xhr = new XMLHttpRequest();
        xhr.open('POST','http://127.0.0.1:5000/upload');
        const form = new FormData();
        for(let i=0;i<files.length;i++){
            form.append('name'+i,files[i]); 
            // 这里的name(第一个参数)具体要根据接口要求。
        }
        xhr.send(form);
        xhr.onload = ()=>{
            console.log(xhr.responseText); 
        }
    }
    
  • 并发上传

    并发就是将所有的文件都当成单文件上传了,实现与上篇中的代码几乎没有改动。

    function singleUpload(file,name){
        const xhr = new XMLHttpRequest();
        xhr.open('POST','http://127.0.0.1:5000/upload');
        const form = new FormData();
        form.append(name,file);
        xhr.send(form);
        xhr.onload = ()=>{
            console.log(xhr.responseText); 
        }
    }
    

在这两种多文件上传的模式中我们通常会使用的是第二种并发上传的方式,这主要是因为如果使用合并上传单次的请求数据量过大,若在请求发送过程中失败的话会导致所有文件都得重新发送,代价过大,而使用并发上传,若有文件上传失败只需对该文件重新上传即可,并且其可以控制每一个文件的请求过程(如得知请求进度,针对单独文件进行取消请求等),但其带来的问题并是若文件数量较多的话我们需要对其进行并发数量的控制。

并发队列的实现

我们知道所有网络请求都是异步请求,而如今JS异步的处理方式大多数是基于Promise,所以我们需要通过Promise进行实现。

首先我们要做的自然是先把上传函数封装为Promise

function singleUpload(file,name){
    return new Promise((resolve,reject)=>{
        const xhr = new XMLHttpRequest();
        xhr.open('POST','http://127.0.0.1:5000/upload');
        const form = new FormData();
        for(let i=0;i<files.length;i++){
            form.append('file'+i,files[i]);
        }
        xhr.send(form);
        xhr.onload = ()=>{
            resolve(xhr.responseText);
        }
        xhr.onerror = ()=>{
            reject(xhr.statusText);
        }
    })
}

然后就是实现我们的控制并发函数parallelTask,既然是要控制并发其参数自然是一个任务列表tasks与最大并发数量maxLen,同时我们得到的返回值应该是一个Promise,所以我们基本函数签名就完成了。

/**
 * 
 * @param {Array} tasks 
 * @param {Number} maxLen 
 * @return {Promise} 
 */
function parallelTask(tasks,maxLen = 3){
    return new Promise((resolve,reject)=>{
        
    })
}

随后我们就要根据需求进行函数实现了。

  • 如何控制并发

    这个只需要将小于并发数量的请求发出即可,不过要注意一些特殊情况比如任务数小于并发数。

    for(let i=0;i<maxLen;i++){
        if(i>=tasks.length){
            break;
        }
        // 执行单个异步任务
    }
    
  • 将处于并发数内的任务运行

    我们可以编写一个函数来控制任务的运行,同时这个时候我们还需要知道我们现在运行到哪个任务了,所以我们可以利用一个变量来记录。

    let taskIndex = 0; // 初始为第一个任务也就是下标0
    function run(){
        const task = tasks[taskIndex]; // 取出当前任务
        task(); // 执行
        taskIndex++; // 指向下一个需运行的任务
    }
    
  • 何时运行下一个任务

    由于我们第一次for循环已经将任务数达到了并发量,所以下一个任务执行就必须等待处于并发队列中的任务完成。因为我们的task为一个Promise,所以我们就可以利用其进行下一个任务的执行,同时我们还要考虑任务是否全部完成,仅当还有未运行任务是才运行下一个任务。

    function run(){
        const task = tasks[taskIndex];
        task().then(()=>{
            if(taskIndex<tasks.length){
                run(); // 当一个任务完成就运行下一个任务
            }
        })
        taskIndex++;
    }
    
  • 并发队列何时全部完成

    要知道任务是否全部完成我们就可以使用一个变量taskCount来存储当前任务完成的数量,这样就可以判断任务是否全部完成了。

    let taskIndex = 0;
    let taskCount = 0;
    function run(){
        const task = tasks[taskIndex];
        task().then((value)=>{
            taskCount++;
            if(taskCount === tasks.length){
                resolve(); // 任务完成返回一个完成的Promise
            }
            if(taskIndex<tasks.length){
                run();
            }
        })
        taskIndex++;
    }
    

这样我们基本的一个控制并发函数就完成了,我们还可以对其进行一些优化和完,

/**
 * 
 * @param {Array} tasks - 为一个二维数组第一项为任务函数,第二项为任务参数
 * @param {Number} maxLen - 最大并发数量
 * @return {Promise} 
 */
function parallelTask(tasks,maxLen = 3){
    return new Promise((resolve,reject)=>{
        let taskIndex = 0; // 当前要执行的任务
        let taskCount = 0; // 当前完成的任务数
        let result = []; // 任务完成结果
        function run(){
            const task = tasks[taskIndex][0];
            const args = tasks[taskIndex][1];
            task(...args).then((value)=>{
                taskCount++;
                result.push(value);
                if(taskCount === tasks.length){
                    resolve(result);
                }
                if(taskIndex<tasks.length){
                    run();// 运行下一个任务
                }
            })
            taskIndex++;
        }
        for(let i=0;i<maxLen && i<tasks.length;i++){
            // 执行任务
            run();
        }
    })
}

利用这个函数再结合上面的上传文件函数就可以做到一个简单的请求并发控制了。通过这个函数我们可以进行多文件的并发同时控制并发数的上传。