前端图片上传方案&大文件上传

165 阅读4分钟

图片上传

基于FORM-DATA实现文件上传

 <input type="file" class="upload_inp" accept=".png,.jpg,.jpeg">
<div class="upload_button_box">
       <button class="upload_button select">选择文件</button>
       <button class="upload_button upload">上传到服务器</button>   
 </div>
.upload_box .upload_inp {
  display: none;
}

点击button 触发input的change

 upload_button_select.addEventListener('click', function () {
        if (upload_button_select.classList.contains('disable') || upload_button_select.classList.contains('loading')) return;
        upload_inp.click();
    });

FORM-DATA 请求头的设置

instance.defaults.headers['Content-Type'] = 'multipart/form-data';

使用表单上传文件时,multipart/form-data不会对参数编码,请求体被分割成多部分,让服务器知道如何拆分它接受的参数。每部分使用 --boundary分割,相当于&,boundary的值是----Web**PLx。

监听上传文件

   // 监听用户选择文件的操作
    upload_inp.addEventListener('change', function () {
        // 获取用户选中的文件对象
        //   + name:文件名
        //   + size:文件大小 B
        //   + type:文件的MIME类型
        let file = upload_inp.files[0];
        if (!file) return;

        // 限制文件上传的格式「方案一」
        /* if (!/(PNG|JPG|JPEG)/i.test(file.type)) {
            alert('上传的文件只能是 PNG/JPG/JPEG 格式的~~');
            return;
        } */

        // 限制文件上传的大小
        if (file.size > 2 * 1024 * 1024) {
            alert('上传的文件不能超过2MB~~');
            return;
        }

        _file = file;

        // 显示上传的文件
        upload_tip.style.display = 'none';
        upload_list.style.display = 'block';
        upload_list.innerHTML = `<li>
            <span>文件:${file.name}</span>
            <span><em>移除</em></span>
        </li>`;
    });

上传文件到服务器

    upload_button_upload.addEventListener('click', function () {
        if (upload_button_upload.classList.contains('disable') || upload_button_upload.classList.contains('loading')) return;
        if (!_file) {
            alert('请您先选择要上传的文件~~');
            return;
        }
  ;
        // 把文件传递给服务器:FormData / BASE64
        // 请求头:instance.defaults.headers['Content-Type'] = 'multipart/form-data';
        let formData = new FormData();
        formData.append('file', _file);
        formData.append('filename', _file.name);
        instance.post('/upload_single', formData).then(data => {
            if (+data.code === 0) {
                alert(`文件已经上传成功~~,您可以基于 ${data.servicePath} 访问这个资源~~`);
                return;
            }
            return Promise.reject(data.codeText);
        }).catch(reason => {
            alert('文件上传失败,请您稍后再试~~');
        }).finally(() => {
      
        });
    });

完整代码

(function () {
    let upload = document.querySelector('#upload1'),
        upload_inp = upload.querySelector('.upload_inp'),
         upload_button_select = upload.querySelector('.upload_button.select'),
        upload_button_upload = upload.querySelector('.upload_button.upload')
  

    // 上传文件到服务器

    upload_button_upload.addEventListener('click', function () {
        if (upload_button_upload.classList.contains('disable') || upload_button_upload.classList.contains('loading')) return;
        if (!_file) {
            alert('请您先选择要上传的文件~~');
            return;
        }
    
        // 把文件传递给服务器:FormData / BASE64
        // 请求头:instance.defaults.headers['Content-Type'] = 'multipart/form-data';
        let formData = new FormData();
        formData.append('file', _file);
        formData.append('filename', _file.name);
        instance.post('/upload_single', formData).then(data => {
            if (+data.code === 0) {
                alert(`文件已经上传成功~~,您可以基于 ${data.servicePath} 访问这个资源~~`);
                return;
            }
            return Promise.reject(data.codeText);
        }).catch(reason => {
            alert('文件上传失败,请您稍后再试~~');
        }).finally(() => {
         
        });
    });


    // 监听用户选择文件的操作
    upload_inp.addEventListener('change', function () {
        // 获取用户选中的文件对象
        //   + name:文件名
        //   + size:文件大小 B
        //   + type:文件的MIME类型
        let file = upload_inp.files[0];
        if (!file) return;

        // 限制文件上传的格式「方案一」
        /* if (!/(PNG|JPG|JPEG)/i.test(file.type)) {
            alert('上传的文件只能是 PNG/JPG/JPEG 格式的~~');
            return;
        } */

        // 限制文件上传的大小
        if (file.size > 2 * 1024 * 1024) {
            alert('上传的文件不能超过2MB~~');
            return;
        }

        _file = file;

        // 显示上传的文件
        upload_tip.style.display = 'none';
        upload_list.style.display = 'block';
        upload_list.innerHTML = `<li>
            <span>文件:${file.name}</span>
            <span><em>移除</em></span>
        </li>`;
    });

    // 点击选择文件按钮,触发上传文件INPUT框选择文件的行为
    upload_button_select.addEventListener('click', function () {
        if (upload_button_select.classList.contains('disable') || upload_button_select.classList.contains('loading')) return;
        upload_inp.click();
    });
})();

base64上传

把选择的文件读取成为BASE64

   const changeBASE64 = file => {
        return new Promise(resolve => {
            let fileReader = new FileReader();
            fileReader.readAsDataURL(file);
            fileReader.onload = ev => {
                resolve(ev.target.result);
            };
        });
    };

上传文件

 upload_inp.addEventListener('change', async function () {
        let file = upload_inp.files[0],
            BASE64,
            data;
        if (!file) return;
        if (file.size > 2 * 1024 * 1024) {
            alert('上传的文件不能超过2MB~~');
            return;
        }
        upload_button_select.classList.add('loading');
        BASE64 = await changeBASE64(file);
        try {
            data = await instance.post('/upload_single_base64', {
                file: encodeURIComponent(BASE64),
                filename: file.name
            }, {
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                }
            });
            if (+data.code === 0) {
                alert(`恭喜您,文件上传成功,您可以基于 ${data.servicePath} 地址去访问~~`);
                return;
            }
            throw data.codeText;
        } catch (err) {
            alert('很遗憾,文件上传失败,请您稍后再试~~');
        } finally {
            upload_button_select.classList.remove('loading');
        }
    });

文件预览&生成文件hash(MD5)值避免同名不同图片覆盖

文件预览

文件预览,就是把文件对象转换为BASE64,赋值给图片的SRC属性即可

 upload_inp.addEventListener('change', async function () {
        let file = upload_inp.files[0],
            BASE64;
        if (!file) return;
        BASE64 = await changeBASE64(file);
        upload_abbre_img.src = BASE64;
      
    });
    
    // 把选择的文件读取成为BASE64
    const changeBASE64 = file => {
        return new Promise(resolve => {
            let fileReader = new FileReader();
            fileReader.readAsDataURL(file);
            fileReader.onload = ev => {
                resolve(ev.target.result);
            };
        });
    };

生成文件hash值(MD5)避免同名不同图片覆盖

使用SparkMD5。根据文件buffer格式生成hash,是根据内容生成hash值,不是根据名字生成

npm install --save spark-md5

把文件转为buffer,生成md5

   const changeBuffer = file => {
        return new Promise(resolve => {
            let fileReader = new FileReader();
            //文件转为buffer,
            fileReader.readAsArrayBuffer(file);
            fileReader.onload = ev => {
                let buffer = ev.target.result,
                    spark = new SparkMD5.ArrayBuffer(),
                    HASH,
                    suffix;
                spark.append(buffer);
                HASH = spark.end();
                suffix = /\.([a-zA-Z0-9]+)$/.exec(file.name)[1];
                resolve({
                    buffer,
                    HASH,
                    suffix,
                    filename: `${HASH}.${suffix}`
                });
            };
        });
    };

上传

 upload_button_upload.addEventListener('click', async function () {
        if (checkIsDisable(this)) return;
        if (!_file) {
            alert('请您先选择要上传的文件~~');
            return;
        }
        changeDisable(true);
        // 生成文件的HASH名字
        let {
            filename
        } = await changeBuffer(_file);
        let formData = new FormData();
        formData.append('file', _file);
        formData.append('filename', filename);
        instance.post('/upload_single_name', formData).then(data => {
            if (+data.code === 0) {
                alert(`文件已经上传成功~~,您可以基于 ${data.servicePath} 访问这个资源~~`);
                return;
            }
            return Promise.reject(data.codeText);
        }).catch(reason => {
            alert('文件上传失败,请您稍后再试~~');
        }).finally(() => {
            changeDisable(false);
            upload_abbre.style.display = 'none';
            upload_abbre_img.src = '';
            _file = null;
        });
    });

完整代码

(function () {
    let upload = document.querySelector('#upload3'),
        upload_inp = upload.querySelector('.upload_inp'),
        upload_button_select = upload.querySelector('.upload_button.select'),
        upload_button_upload = upload.querySelector('.upload_button.upload'),
        upload_abbre = upload.querySelector('.upload_abbre'),
        upload_abbre_img = upload_abbre.querySelector('img');
    let _file = null;

    // 验证是否处于可操作性状态
    const checkIsDisable = element => {
        let classList = element.classList;
        return classList.contains('disable') || classList.contains('loading');
    };

    // 把选择的文件读取成为BASE64
    const changeBASE64 = file => {
        return new Promise(resolve => {
            let fileReader = new FileReader();
            fileReader.readAsDataURL(file);
            fileReader.onload = ev => {
                resolve(ev.target.result);
            };
        });
    };
    const changeBuffer = file => {
        return new Promise(resolve => {
            let fileReader = new FileReader();
            fileReader.readAsArrayBuffer(file);
            fileReader.onload = ev => {
                let buffer = ev.target.result,
                    spark = new SparkMD5.ArrayBuffer(),
                    HASH,
                    suffix;
                spark.append(buffer);
                HASH = spark.end();
                suffix = /\.([a-zA-Z0-9]+)$/.exec(file.name)[1];
                resolve({
                    buffer,
                    HASH,
                    suffix,
                    filename: `${HASH}.${suffix}`
                });
            };
        });
    };

    // 把文件上传到服务器
    const changeDisable = flag => {
        if (flag) {
            upload_button_select.classList.add('disable');
            upload_button_upload.classList.add('loading');
            return;
        }
        upload_button_select.classList.remove('disable');
        upload_button_upload.classList.remove('loading');
    };
    upload_button_upload.addEventListener('click', async function () {
        if (checkIsDisable(this)) return;
        if (!_file) {
            alert('请您先选择要上传的文件~~');
            return;
        }
        changeDisable(true);
        // 生成文件的HASH名字
        let {
            filename
        } = await changeBuffer(_file);
        let formData = new FormData();
        formData.append('file', _file);
        formData.append('filename', filename);
        instance.post('/upload_single_name', formData).then(data => {
            if (+data.code === 0) {
                alert(`文件已经上传成功~~,您可以基于 ${data.servicePath} 访问这个资源~~`);
                return;
            }
            return Promise.reject(data.codeText);
        }).catch(reason => {
            alert('文件上传失败,请您稍后再试~~');
        }).finally(() => {
            changeDisable(false);
            upload_abbre.style.display = 'none';
            upload_abbre_img.src = '';
            _file = null;
        });
    });


    // 文件预览,就是把文件对象转换为BASE64,赋值给图片的SRC属性即可
    upload_inp.addEventListener('change', async function () {
        let file = upload_inp.files[0],
            BASE64;
        if (!file) return;
        _file = file;
        upload_button_select.classList.add('disable');
        BASE64 = await changeBASE64(file);
        upload_abbre.style.display = 'block';
        upload_abbre_img.src = BASE64;
        upload_button_select.classList.remove('disable');
    });
    upload_button_select.addEventListener('click', function () {
        if (checkIsDisable(this)) return;
        upload_inp.click();
    });
})();

多文件上传

 <input type="file" class="upload_inp" multiple>
        <div class="upload_button_box">
              <button class="upload_button select">选择文件</button>
              <button class="upload_button upload">上传到服务器</button>
        </div>

循环发送请求

 upload_button_upload.addEventListener('click', async function () {
      
        if (_files.length === 0) {
            alert('请您先选择要上传的文件~~');
            return;
        }
    
        // 循环发送请求
        let upload_list_arr = Array.from(upload_list.querySelectorAll('li'));
        _files = _files.map(item => {
            let fm = new FormData;

            fm.append('file', item.file);
            fm.append('filename', item.filename);
            return instance.post('/upload_single', fm, {
                onUploadProgress(ev) {
                    // 检测每一个的上传进度
                    if (curSpan) {
                        curSpan.innerHTML = `${(ev.loaded/ev.total*100).toFixed(2)}%`;
                    }
                }
            }).then(data => {
                if (+data.code === 0) {
                    if (curSpan) {
                        curSpan.innerHTML = `100%`;
                    }
                    return;
                }
                return Promise.reject();
            });
        });

        // 等待所有处理的结果
        Promise.all(_files).then(() => {
            alert('恭喜您,所有文件都上传成功~~');
        }).catch(() => {
            alert('很遗憾,上传过程中出现问题,请您稍后再试~~');
        }).finally(() => {
            changeDisable(false);
            _files = [];
            upload_list.innerHTML = '';
            upload_list.style.display = 'none';
        });
    });

上传进度条

instance.post('/upload_single', fm, {
                onUploadProgress(ev) {
                    // 检测每一个的上传进度
                    if (curSpan) {
                        curSpan.innerHTML = `${(ev.loaded/ev.total*100).toFixed(2)}%`;
                    }
                }
            })

大文件的切片上传和断点续传

切片上传

思路:

  1. 获取已上传文件的分片信息,服务端返回文件分片数量,每个分片大小和文件唯一的 fileKey(断点续传基于这个实现)
  2. 大文件切片,分别上传服务端
  3. 结算文件分片,后端会将各分片拼接成一个完整的文件并返回

获取分片信息

 let file = upload_inp.files[0];
        if (!file) return;

        // 获取文件的HASH
        let already = [],
            data = null,
            {
                HASH,
                suffix
            } = await changeBuffer(file);

        // 1.获取已经上传的切片信息
        try {
            data = await instance.get('/upload_already', {
                params: {
                    HASH
                }
            });
            if (+data.code === 0) {
                already = data.fileList;
            }
        } catch (err) {}

实现文件切片处理

 // 「固定数量 & 固定大小」,1024=1kb
        let max = 1024 * 100,
        //多少切片
            count = Math.ceil(file.size / max),
            index = 0,
            chunks = [];
        //超过限定100个切片,规定每个切片的大小
        if (count > 100) {
            max = file.size / 100;
            count = 100;
        }
        while (index < count) {
            //用file上的slice方法切
            //file.slice(0,1024),file.slice(1024,2048)
            chunks.push({
                file: file.slice(index * max, (index + 1) * max),
                filename: `${HASH}_${index+1}.${suffix}`
            });
            index++;
        }

把每个切片上传到服务器

 chunks.forEach(chunk => {
            // 已经上传的无需在上传
            if (already.length > 0 && already.includes(chunk.filename)) {
                complate();
                return;
            }
            let fm = new FormData;
            fm.append('file', chunk.file);
            fm.append('filename', chunk.filename);
            instance.post('/upload_chunk', fm).then(data => {
                if (+data.code === 0) {
                    complate();
                    return;
                }
                return Promise.reject(data.codeText);
            }).catch(() => {
                alert('当前切片上传失败,请您稍后再试~~');
                clear();
            });
        });

上传成功的处理

结算文件分片,后端会将各分片拼接成一个完整的文件并返回

 const complate = async () => {
            // 管控进度条
            index++;//传成功1个切片加1
            upload_progress_value.style.width = `${index/count*100}%`;

            // 3.当所有切片都上传成功,我们合并切片
            if (index < count) return;
            upload_progress_value.style.width = `100%`;
            try {
                data = await instance.post('/upload_merge', {
                    HASH,
                    count
                }, {
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded'
                    }
                });
                if (+data.code === 0) {
                    alert(`恭喜您,文件上传成功,您可以基于 ${data.servicePath} 访问该文件~~`);
                    clear();
                    return;
                }
                throw data.codeText;
            } catch (err) {
                alert('切片合并失败,请您稍后再试~~');
                clear();
            }
        };

断点续传

定义:假设一个文件被切为10片,当上传3片的时候,停止了,下一次再上传的时候,请求服务器,已经上传成功的切片有3片,然后就从第七片开始继续传,不需要再从新开始传完整的10片。

获取分片信息

 let file = upload_inp.files[0];
        if (!file) return;

        // 获取文件的HASH
        let already = [],
            data = null,
            {
                HASH,
                suffix
            } = await changeBuffer(file);

        // 1.获取已经上传的切片信息
        try {
            data = await instance.get('/upload_already', {
                params: {
                    HASH
                }
            });
            if (+data.code === 0) {
                already = data.fileList;
            }
        } catch (err) {}

切片上传时片判断是否已传过

 chunks.forEach(chunk => {
            // 已经上传的无需在上传
            if (already.length > 0 && already.includes(chunk.filename)) {
                complate();
                return;
            }
          ...
        });

完整代码

(function () {
    let upload = document.querySelector('#upload7'),
        upload_inp = upload.querySelector('.upload_inp'),
        upload_button_select = upload.querySelector('.upload_button.select'),
        upload_progress = upload.querySelector('.upload_progress'),
        upload_progress_value = upload_progress.querySelector('.value');

    const checkIsDisable = element => {
        let classList = element.classList;
        return classList.contains('disable') || classList.contains('loading');
    };

    const changeBuffer = file => {
        return new Promise(resolve => {
            let fileReader = new FileReader();
            fileReader.readAsArrayBuffer(file);
            fileReader.onload = ev => {
                let buffer = ev.target.result,
                    spark = new SparkMD5.ArrayBuffer(),
                    HASH,
                    suffix;
                spark.append(buffer);
                HASH = spark.end();
                suffix = /\.([a-zA-Z0-9]+)$/.exec(file.name)[1];
                resolve({
                    buffer,
                    HASH,
                    suffix,
                    filename: `${HASH}.${suffix}`
                });
            };
        });
    };

    upload_inp.addEventListener('change', async function () {
        let file = upload_inp.files[0];
        if (!file) return;
        upload_button_select.classList.add('loading');
        upload_progress.style.display = 'block';

        // 获取文件的HASH
        let already = [],
            data = null,
            {
                HASH,
                suffix
            } = await changeBuffer(file);

        // 1.获取已经上传的切片信息
        try {
            data = await instance.get('/upload_already', {
                params: {
                    HASH
                }
            });
            if (+data.code === 0) {
                already = data.fileList;
            }
        } catch (err) {}

        // 2.实现文件切片处理 
        // 「固定数量 & 固定大小」,1024=1kb
        let max = 1024 * 100,
            count = Math.ceil(file.size / max),//多少切片
            index = 0,
            chunks = [];
        if (count > 100) {//超过限定100个切片,
            max = file.size / 100;
            count = 100;
        }
        while (index < count) {
            //用file上的slice方法切
            //file.slice(0,1024),file.slice(1024,2048)
            chunks.push({
                file: file.slice(index * max, (index + 1) * max),
                filename: `${HASH}_${index+1}.${suffix}`
            });
            index++;
        }

        // 上传成功的处理
        index = 0;
        const clear = () => {
            upload_button_select.classList.remove('loading');
            upload_progress.style.display = 'none';
            upload_progress_value.style.width = '0%';
        };
        const complate = async () => {
            // 管控进度条
            index++;//传成功1个切片加1
            upload_progress_value.style.width = `${index/count*100}%`;

            // 3.当所有切片都上传成功,我们合并切片
            if (index < count) return;
            upload_progress_value.style.width = `100%`;
            try {
                data = await instance.post('/upload_merge', {
                    HASH,
                    count
                }, {
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded'
                    }
                });
                if (+data.code === 0) {
                    alert(`恭喜您,文件上传成功,您可以基于 ${data.servicePath} 访问该文件~~`);
                    clear();
                    return;
                }
                throw data.codeText;
            } catch (err) {
                alert('切片合并失败,请您稍后再试~~');
                clear();
            }
        };

        // 把每一个切片都上传到服务器上
        chunks.forEach(chunk => {
            // 已经上传的无需在上传
            if (already.length > 0 && already.includes(chunk.filename)) {
                complate();
                return;
            }
            let fm = new FormData;
            fm.append('file', chunk.file);
            fm.append('filename', chunk.filename);
            instance.post('/upload_chunk', fm).then(data => {
                if (+data.code === 0) {
                    complate();
                    return;
                }
                return Promise.reject(data.codeText);
            }).catch(() => {
                alert('当前切片上传失败,请您稍后再试~~');
                clear();
            });
        });
    });

    upload_button_select.addEventListener('click', function () {
        if (checkIsDisable(this)) return;
        upload_inp.click();
    });
})();