前端文件上传攻略

205 阅读4分钟

前言

在实际工作中经常遇到文件上传,想要比较清楚的掌握,不妨花点时间瞅瞅整篇文章,话不多说,开整

前端:Vue.js Element-Ui

后端:node.js Express multiparty

思路

以下涉及到的知识点:

  • 文件上传原理
  • 单文件上传 + 上传进度
  • 多文件上传 + 上传进度
  • 大文件上传之分片上传
  • 大文件上传之断点续传
  • 拖拽上传

文件上传对于用户体验:

----网络波动时文件上传(能否续传)

----请求超时处理

---- 服务器数据处理能力

文件上传原理

  1. 根据http协议的规范和定义,完成请求体消息体的封装和消息体的解析,将二进制内容保存到文件
  2. 文件上传服务器请求头类型['Content-Type'] = 'multipart/form-data', 同时method必须为post`方法

multipart互联网上的混合资源,就是资源由多种元素组成,form-data表示可以使用HTML Forms 和 POST 方法上传文件,具体的定义可以参考RFC 7578。

  1. 解析:客户端发送请求将文件上传至服务器,服务器收到请求消息体,对消息体进行解析

使用技术

前后端通信常用数据结构axios二次封装

基于FileReader实现文件读取和相关处理

基于sparkMd5实现文件名自生成

文件上传的两种经典方案:FormData & BASE64

后端至少提供三种接口

  1. 检查文件接口(文件是否已经上传过,是否需要续传)
  2. 上传文件接口(上传文件类型FormData & BASE64)
  3. 合并文件分片接口

axios二次封装

image.png

文件上传公共方式:

----文件格式为FormData

let formData = new FormData();

formData.append('file', _file);

formData.append('filename', _file.name);

 upload_button_upload.addEventListener('click', function () {
        if (upload_button_upload.classList.contains('disable') || upload_button_upload.classList.contains('loading')) return;
        if (!_file) {
            alert('请您先选择要上传的文件~~');
            return;
        }
        changeDisable(true);
        // 把文件传递给服务器:FormData / BASE64
        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(() => {
            clearHandle();
            changeDisable(false);
        });
    });

----文件格式为BASE64

let fileReader = new FileReader();

fileReader.readAsDataURL(file);

fileReader.onload = ev => { resolve(ev.target.result); };

// 把选择的文件读取成为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');
       }
   });

文件大小限制

对于文件上传大小限制,可以使用 file.size

文件类型限制

上传文件类型限制处理两种方式:

image.png

使用正则表达式:

image.png

针对相同文件取不同文件名,前端自行处理,避免重复上传

image.png

进度管控

image.png

此处采用改变进度条样式:

 // 文件上传中的回调函数 xhr.upload.onprogress
                onUploadProgress(ev) {
                    let {
                        loaded,
                        total
                    } = ev;
                    upload_progress.style.display = 'block';
                    upload_progress_value.style.width = `${loaded/total*100}%`;
                }

多文件上传

需要注意:给每个文件添加一个唯一标识key,便于对该文件进行处理,比如移除该文件等

 // 获取唯一值
    const createRandom = () => {
        let ran = Math.random() * new Date();
        return ran.toString(16).replace('.', '');
    };
    upload_inp.addEventListener('change', async function () {
        _files = Array.from(upload_inp.files);
        if (_files.length === 0) return;
        // 我们重构集合的数据结构「给每一项设置一个位置值,作为自定义属性存储到元素上,后期点击删除按钮的时候,我们基于这个自定义属性获取唯一值,再到集合中根据这个唯一值,删除集合中这一项」
        _files = _files.map(file => {
            return {
                file,
                filename: file.name,
                key: createRandom()
            };
        });
        // 绑定数据
        let str = ``;
        _files.forEach((item, index) => {
            str += `<li key="${item.key}">
                <span>文件${index+1}${item.filename}</span>
                <span><em>移除</em></span>
            </li>`;
        });
        upload_list.innerHTML = str;
        upload_list.style.display = 'block';
    });

拖拽上传

image.png

大文件上传

文件切片

  1. 文件转二进制流格式,使用流可以进行切割成多份
  2. 固定每个切片大小,切片数量
  3. 按顺序唯一标记每个切片,便于后期合并切片
  4. 发送切片合并请求

image.png

前后端分工:

前端:

  • 文件格式校验
  • 文件切片、md5计算
  • 发起检查请求,把当前文件的hash发送给服务端,检查是否有相同hash的文件
  • 上传进度计算
  • 上传完成后通知后端合并切片
// 生成文件名
 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}`
                });
            };
        });
    };

进行切片 file.slice()

image.png

前端分片完成,向后端发送请求: image.png

当网络较差,出现上传一半失败,为了不重头开始,采用断点续传

断点续传

  • 为每一个文件切割块添加不同的标识
  • 当上传成功的之后,记录上传成功的标识
  • 当我们暂停或者发送失败后,可以重新发送没有上传成功的切割文件

实现方式:

方式一: 在客户端本地保存已经上传成功的分片,使用spark-md5生成文件hash,重新上传的时候,进行和本地分段 hash 值的对比,如果相同的话则跳过,继续下一个分段的上传

注:生成hash会耗时,且本地存储不安全

image.png

image.png

方式二:服务端已经保存了部分片段,客户端上传前从服务器获取已经上传的分片信息,客户端对比hash,跳过已经上传的部分

从服务器获取 getUploadedFromServer(fileHash)

方式三: 对比是否上传交由后端自行处理(跟后端沟通好就行)

后端:

  • 检查接收到的hash是否有相同的文件,并通知前端当前hash是否有未完成的上传
  • 接收切片
  • 合并所有切片